Web Application Extensibility with FubuMVC (Part 2)

Last time I talked a bit about how FubuMVC provides a new extensibility mechanism to add more stuff into a FubuMVC application, and why following the Open/Closed Principle of software design for extensibility is valuable.  As promised, I’d like to continue by looking at a new way in FubuMVC to “inject” custom content into existing screens for customer specific functionality.

We’re wrapping up a rollout to a new client that included adding displays of data from their proprietary systems into some of our existing screens .  Roughly speaking, our requirements for content extension (as decided by me) are to:

  1. Specify where and what content gets injected into our existing screens
  2. Avoid the need to create customer specific forks/branches of the code.  I know I’m harping on this point, but it is really that important
  3. Use any possible FubuMVC mechanism for creating the content – i.e. I should be able to just inject string content directly, use a user control, a Spark control, or even to invoke a partial request
  4. Automatically discover the available extensions per screen and content area

 

Injecting Content 

We don’t use a lot of the tabbed screen metaphor, but there is a little bit here and there and it turns out to be a somewhat cheap way of sticking big chunks of new content into a screen without severe damage to the layout.  Inside our screen for displaying cases (we’re a CRM company for services and support), we inject custom tabs into the screen by specifying that extensions go into the tab headers*:

<asp:Content ID="Content6" ContentPlaceHolderID="OtherHeaders" runat="server">
    <%= this.RenderPartialFor(x => x.Case).Using<ShowChildCaseTab>().If(Model.Case.HasChildCase) %>
    <%= this.RenderPartialFor(x=> x.Case).Using<ShowWorkOrdersTab>() %>
    <% this.WriteExtensions("tab-header"); %>
</asp:Content>

and matching body content at another spot in the screen (Note to self:  move this into the master page layout):

<asp:Content ContentPlaceHolderID="EntityDetail" runat="server">
   

    <%this.WriteExtensions("tab-body"); %>
</asp:Content>

The code above is used to specify where a group of content extensions are to be placed in the screen.  The extension method WriteExtensions(category) is in FubuMVC.UI.dll assembly and hangs off the common IFubuPage<T> interface that all Fubu views would implement.  When this method is called, behind the scenes FubuMVC finds all the content extensions that are registered for the ViewModel type of the current page and the specified category passed into the method (“tag-header” or “tab-body” in the samples above).

 

Declaring Content

A content extension is just a class that implements this tiny interface:

    public interface IContentExtension<T> where T : class

    {

        IEnumerable<object> GetExtensions(IFubuPage<T> page);

    }

IContentExtension<T> is just another Double Dispatch trick.  In real usage it would be something simple like this:

    public class SampleExtension : IContentExtension<EditCaseViewModel>
    {
        public IEnumerable<object> GetExtensions(IFubuPage<EditCaseViewModel> page)
        {
            // This sample just renders a user control
            yield return page.RenderPartial().For(page.Model.Logs).Using<LogHistory>();
       
            // Build Html directly
            yield return new HtmlTag("div").Text("some text").AddClass("some_class").Id("someId");

            // or return Html like this
            yield return "<div>Some text</div>";

            // or execute a "partial request"
        }
    }

The “Double Dispatch” trickery allows us to use anything in the entire FubuMVC arsenal for creating content.  Your content extension class just needs to take in the object for the containing page (IFubuPage<T>), and return one or many objects that represent content that you want written into the containing page.  In a WebForms view, each call to “yield return page.***()” is the equivalent to “<%=this.***()%>” written directly into the view itself.  The IFubuPage<T> itself would have references to literally every piece of context you should need to specify the extension content (the model of the page and a reference to the proper service locator scoped to the web request).

 

That’s it for the screen itself, so now let’s talk about registration.

 

Registering Content Extensions

All the ingredients of a FubuMVC application need to be declared in a FubuRegistry (or in an IFubuRegistryExtension), and content extensions are no different.  Assuming that you have a reference to the FubuMVC.UI.dll (I think we’re going to give up and merge the assemblies), you register content extensions with code like this:

    // This is a base class we use to declare custom tabs in our screens.
    // This class allows you to avoid some duplication by declaring the
    // tab header and tab content together to avoid mismatch names
    // and the "Shotgun Surgery" problem
    public abstract class TabExtension<T> : IFubuRegistryExtension where T : class
    {
        private readonly string _id;
        private readonly Func<string> _tabText;

        protected TabExtension(string id, Func<string> tabText)
        {
            _id = id;
            _tabText = tabText;
        }

        public string Id
        {
            get { return _id; }
        }

        protected abstract object content(IFubuPage<T> page);

        private string getBodyContent(IFubuPage<T> page)
        {
            return "<div id={0}>".ToFormat(Id) + content(page) + "</div>";
        }

        public void Configure(FubuRegistry registry)
        {
            // The Extensions() method exposes the DSL syntax
            // to register content extensions in your application
            // Content extensions are registered by the ViewModel type
            // and a category -- T is the ViewModel, "tab-header" and "tab-body"
            // are categories
            registry.Extensions()
                .For<T>("tab-header", page => new HtmlTag("li", li =>
                {
                    li.Add("a").Attr("href", "#").Id(_id + "-tab").Attr("rel", "#" + _id).Text(_tabText());
                }))
                .For<T>("tab-body", getBodyContent);
        }
    }

 

If you read my previous post, you recognize that IFubuRegistryExtension is just a way to add more stuff to the FubuRegistry of an application.  Again going back to the previous post, all we need to do is express the content extensions in an IFubuRegistryExtension and let StructureMap go scoop them up for us out of the customer extension assemblies.

 
 

Other Stuff We’re Doing for Extensibility

 

This is just one of the ways we’ve achieved extensibility in our application architecture.  Here are some other things we’re doing to add extensibility to our application at customer sites:

  • Extension properties
  • Model based navigation.  All that means is that you render navigation content like menus based on a model of the available actions in the system (including security and business logic rules) instead of hard coding the navigation controls.  Nothing radically new, but this does work especially well with the composition-based model approach to architecture that Chad Myers was talking about in this post.
  • Josh Flanagan added the ability to “suck in” custom JavaScript and CSS files from a customer override folder on a page by page / control by control (or master page by master page) basis.  The strategy I outlined in this post is great for the common places we expect customizations, but this ability to load the custom JS/CSS files has been invaluable for weird, unexpected customizations.  All I can say is that dynamic language programming is liberating and jQuery is awesome.
  • Use StructureMap assembly scanning in our Fluent NHibernate setup to add custom entities from customer specific assemblies
  • Using StructureMap assembly scanning to add customer specific services and strategies.  The StructureMap wiring (or the IoC tool of your choice) is relatively simple, but the key is in how you compose responsibilities in your system.
  • Repurpose localization to change onscreen terminology and messages for customers. 
  • New conditions and actions in our rules engine.  I *will* blog about our rules engine work at Dovetail at some point (promised my boss I would).  The “rules engine” piece isn’t really that interesting, but it’s yet another example of how composition based designs lend themselves to easier extensibility.
  • We’re proof of concepts with embedding IronRuby scripts in some places in our application for extensions that look good so far

 

I might get around to blogging more about some of these things, depending on interest and my own availability.

 

* Yeah, we’re stuck on the WebForms view engine.  Trust me, I’ve regretted it for a long time, but the spikes we did didn’t make it look like it was worth the large investment to switch to Spark midstream.  Our HtmlHelpers in FubuMVC and the Dovetail specific stuff is much more sophisticated than what you see in MSMVC apps, but maybe we wouldn’t have gone down that path if we’d used Spark instead.

About Jeremy Miller

Jeremy is the Chief Software Architect at Dovetail Software, the coolest ISV in Austin. Jeremy began his IT career writing "Shadow IT" applications to automate his engineering documentation, then wandered into software development because it looked like more fun. Jeremy is the author of the open source StructureMap tool for Dependency Injection with .Net, StoryTeller for supercharged acceptance testing in .Net, and one of the principal developers behind FubuMVC. Jeremy's thoughts on all things software can be found at The Shade Tree Developer at http://codebetter.com/jeremymiller.
This entry was posted in FubuMVC. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • http://codebetter.com/members/jmiller/default.aspx Jeremy D. Miller

    @corey,

    1.) This is something that Chad and I fundamentally disagree on. I say that it’s perfectly fine to “micro” generate html markup programmatically and he’s more of your mindset. All the same though, it doesn’t matter because you *can* use any possible FubuMVC mechanism to define the content extensions including:

    WebForms controls
    Spark templates
    Programmatic HtmlTag creation
    Partial invocation of another action
    An HtmlHelper
    Whatever we add to Fubu later…

    2.) I can’t speak to the Spark mechanics about constructing views and the templating, but from the FubuMVC architecture perspective, Areas/Slices/extensibility is a pain because of the way WebForms loads content and the goofy VirtualPathProvider stuff. I’d much rather be dealing only with view engines that have no hard coupling to the ASP.Net runtime.

  • http://blog.coreycoogan.com corey coogan

    Another great post on FUBU, which looks like it really kicks ass. A couple questions for you, if you don’t mind.

    1. I still get a bit of a nasty feeling when I see HTML being generated from code. Perhaps it’s unfounded, but I’ve seen many cases where it was regrettable. Anyway, is there any way to achieve your goals with a more templated approach? Where you or your client could change the HTML without a build/deploy?

    2. I’ve heard many folks speak out against the webforms view engine, and I haven’t done enough MSMVC to experience why it sucks so bad. Spark is definitely awesome, but I’m curious to know why you’ve regretted the webform VE and what kind of pains it has caused.

    Thanks,
    Corey