Skip to content

Conventional asset hooks

rauhryan edited this page Mar 6, 2012 · 3 revisions

#Conventional asset hooks


Article by Ryan Rauh about how we are adding conventional asset hooks to pages in our application

Conventional Option

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

Given the following controller

public class FooController
{
    [UrlRegistryCategory("Bar")]
    public FooViewModel Foo(FooRequest foo ) { /* le code */ }
}

Will generate the following hooks

  • {FooViewModel}.js
  • {FooViewModel-FooController}.js
  • {FooViewModel-Foo}.js
  • {FooViewModel-Bar}.js
  • {FooViewModel}.css
  • {FooViewModel-FooController}.css
  • {FooViewModel-Foo}.css
  • {FooViewModel-Bar}.css

Why all the different hooks?

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)

Getting assets on the page

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

Alternate option to add asset extensions

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);
    }

Example usage

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");
        }
    }
}

Differences between Option 1 and Option 2

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.

FubuMVC policy that wires it all up

    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;
        }
    }