-
Notifications
You must be signed in to change notification settings - Fork 151
Conventional asset hooks
#Conventional asset hooks
Article by Ryan Rauh about how we are adding conventional asset hooks to pages in our application
Dovetail CRM now has the ability to conventionally extend any page in the app with the FubuMVC asset pipeline. Each page declares a conventional hook to extend with the asset graph.
The name are build conventionally as follows:
The pattern is {
Action.OutputType.Name
-
HandlerType.Name|Method.Name|UrlCategory
}
. js|css
public class FooController
{
[UrlRegistryCategory("Bar")]
public FooViewModel Foo(FooRequest foo ) { /* le code */ }
}
{FooViewModel}.js
{FooViewModel-FooController}.js
{FooViewModel-Foo}.js
{FooViewModel-Bar}.js
{FooViewModel}.css
{FooViewModel-FooController}.css
{FooViewModel-Foo}.css
{FooViewModel-Bar}.css
Consider the following scenario
public class FooController
{
[UrlRegistryCategory("Edit")]
public FooViewModel Edit(FooRequest foo ) { /* le code */ }
[UrlRegistryCategory("New")]
public FooViewModel New(FooRequest foo ) { /* le code */ }
}
Will generate the following hooks
-
{FooViewModel}.js
(you want the script on both new and edit) -
{FooViewModel-FooController}.js
(you want the script on both new and edit) -
{FooViewModel-New}.js
(you want the script on new only) -
{FooViewModel-Edit}.js
(you want the script on edit only) -
{FooViewModel}.css
(you want the style on both new and edit) -
{FooViewModel-FooController}.css
(you want the style on both new and edit) -
{FooViewModel-New}.css
(you want the style on new only) -
{FooViewModel-Edit}.css
(you want the style on edit only)
To get an asset on the page. Create a file that matches the conventional hook name that is the specificity that you require and place it in the proper folder under the Content
folder in the application or bottle
If none of the conventional hooks are specific enough there is an alternate method to adding asset extensions using IAssetExtension
public interface IAssetExtension
{
bool Matches(BehaviorChain chain);
void Extend(IAssetRequirements requirements);
}
namespace DovetailCRM.Packages.HR.Demo.Agent
{
public class ViewSiteAssetExtension : IAssetExtension
{
public bool Matches(BehaviorChain chain)
{
return !chain.FirstCall().Method.Name.Contains("New") &&
chain.FirstCall().OutputType().CanBeCastTo<EditSiteViewModel>();
}
public void Extend(IAssetRequirements requirements)
{
requirements.Require("viewsite.demo.extension.css");
}
}
}
The first option allows customers and developers add assets to pages without the need to recompile the app or deploy any binaries. There are some concerns with the possibility of "Magic String" extension problems or things biting us like F2 type renaming.
The second option has more effort involved by could be considered the "safer" option because of things like compiler warnings and resharper renaming. Option 2 is also able to access anything needed at runtime through dependency injection. Things like "only require this asset if current user has foo permission" are all possible with this option.
Both methods should include an integration test that will fail if the hook no longer works.
public class AssetExtensionPolicy : IConfigurationAction
{
public void Configure(BehaviorGraph graph)
{
graph.Behaviors
.Each(c => c.Prepend(new AssetExtensionNode(c,typeof(AssetExtensionBehavior))));
graph.Behaviors
.Each(c =>
{
if(c.HasOutputBehavior())
{
var assets = generateAssetHookNames(c);
assets.Each(
a =>
graph.Observer.RecordCallStatus(c.FirstCall(),
"Adding asset hook named: {0}".ToFormat(a)));
c.Prepend(new ConventionalAssetExtensionNode(assets.ToArray()));
}
});
}
private IEnumerable<string> generateAssetHookNames(BehaviorChain c)
{
var extensions = new[] {"js", "css"};
var suffixes = new[] {"", c.FirstCall().HandlerType.Name, c.FirstCall().Method.Name, c.UrlCategory.Category};
var prefixes = new[] {c.FirstCall().OutputType().Name};
var assets =
extensions
.SelectMany(extension =>
prefixes
.SelectMany(prefix => suffixes,
(p, s) =>
{
if (s.IsEmpty()) return "{" + p + "}";
return "{" + p + "-" + s + "}";
}).Distinct(),
(ext, key) => string.Join(".", key, ext));
return assets;
}
}
public class AssetExtensionNode : BehaviorNode
{
private readonly BehaviorChain _chain;
private readonly Type _behaviorType;
public AssetExtensionNode(BehaviorChain chain, Type behaviorType)
{
_chain = chain;
_behaviorType = behaviorType;
}
protected override ObjectDef buildObjectDef()
{
var objectDef = new ObjectDef
{
Type = _behaviorType
};
objectDef.DependencyByValue(_chain);
return objectDef;
}
public override BehaviorCategory Category
{
get { return BehaviorCategory.Wrapper;}
}
}
public class ConventionalAssetExtensionNode : BehaviorNode
{
private readonly string[] _assetNames;
public ConventionalAssetExtensionNode(string[] assetNames)
{
_assetNames = assetNames;
}
protected override ObjectDef buildObjectDef()
{
var objectDef = new ObjectDef
{
Type = typeof(ConventionalAssetExtensionBehavior)
};
Action<IAssetRequirements> requirementsAction =
a => a.UseAssetIfExists(_assetNames);
objectDef.DependencyByValue(requirementsAction);
return objectDef;
}
public override BehaviorCategory Category
{
get { return BehaviorCategory.Wrapper; }
}
}
public class ConventionalAssetExtensionBehavior : BasicBehavior
{
private readonly Action<IAssetRequirements> _requirementsAction;
private readonly IAssetRequirements _assetRequirements;
public ConventionalAssetExtensionBehavior(Action<IAssetRequirements> requirementsAction,IAssetRequirements assetRequirements)
: base(PartialBehavior.Executes)
{
_requirementsAction = requirementsAction;
_assetRequirements = assetRequirements;
}
protected override DoNext performInvoke()
{
_requirementsAction(_assetRequirements);
return DoNext.Continue;
}
}
public class AssetExtensionBehavior : BasicBehavior
{
private readonly IAssetRequirements _requirements;
private readonly IEnumerable<IAssetExtension> _extensions;
private readonly BehaviorChain _chain;
public AssetExtensionBehavior(IAssetRequirements requirements, IEnumerable<IAssetExtension> extensions, BehaviorChain chain) : base(PartialBehavior.Executes)
{
_requirements = requirements;
_extensions = extensions;
_chain = chain;
}
protected override DoNext performInvoke()
{
if(_chain.HasOutputBehavior())
{
_requirements.UseAssetIfExists();
}
_extensions.Each(x =>
{
if(x.Matches(_chain)) {x.Extend(_requirements);}
});
return DoNext.Continue;
}
}