Building on the work I have done previous (see Using MEF with MVC Controllers), I’ve created a revised architecture of MEF + MVC using .NET 4.0. I’m (mostly) happy with the outcome.

One of the things I wanted to get right, was allowing true modularity using MEF. As you already know, its now relatively painless to do modular code with ASP.NET (thanks to standard .NET activation, or using something like MEF for automatic composition), but one of the challenges is not how the code plugs in, but the views themselves. This was always difficult with WebForms because of the way the .aspx pages are compiled (seperately from the code). The other pain is the way you may have had to organise your files in the file system, they were also disconnected from the libraries because these had to exist in the \bin directory of your application.

Extensible MVC with MEF can address these:

a) Libraries used with MEF catalogs can be in any directory accessible by the application, and
b) Routes do not use filenames, so the location of your .aspx views doesn’t matter (as long as MVC can access them).

So, how do we go about making a truly modular MVC implementation? We start with what something similar to before, so lets build a base implementation. This has evolved a bit since my early version, namely the application work is now in a seperate assembly, MefMvcFramework.

public class Application : HttpApplication {

Within this application, we declare our two imports:

  [ImportMany] private IEnumerable<Lazy<IRouteRegistrar, IRouteRegistrarMetadata>> RouteRegistrars;
  [Import] private ImportControllerFactory ControllerFactory;

Our IRouteRegistrar instances allow us to dynamically register our routes, and our ImportControllerFactory handles the creation of controller instances used by MVC. I’ve switched to using Lazy<,> for the registrars as we don’t really need to use a PartFactory<,> (read: PartCreator<,>) instance to manage the dynamic instantiation of instances, there is no need for it, Lazy<,> allows us to access our metadata without the overhead of dynamic instantiation. Also, PartFactory<,> is geared to creating new instances, whereas Lazy<,> persists a single instance, using lazy loading.

Next, let’s flesh out our Application_Start() method:

  protected void Application_Start()
  {
      // Perform any tasks required before composition.
      PreCompose();

      // Compose the application.
      Compose();

      // Set the controller factory.
      ControllerBuilder.Current.SetControllerFactory(ControllerFactory);

      // Set the view engine that supports imported areas.
      ViewEngines.Engines.Add(new AreaViewEngine());

      // Initialises the application.
      Initialise();

      // Register MVC routes.
      RegisterRoutes();
  }

I’ve added additional extension points to the application start method as it allows us finer control over how our application is being initialised in relation to composition. PreCompose(), Compose(), Initialise() and RegisterRoutes() are virtual methods, so we can optional override them in our specific implementation. They’re all pretty self-explanitory, so I won’t go through what each of them does.

What you will notice though, is that I’ve added a custom ViewEngine. This is to allow us to use custom locations for our views. I haven’t built anything particularly special, we just override the view location formats in the constructor:

public class AreaViewEngine : WebFormViewEngine
{
    #region Constructor
    /// <summary>
    /// Initialises a new instance of <see cref="AreaViewEngine" />.
    /// </summary>
    public AreaViewEngine()
    {
        MasterLocationFormats = new[]
                                {
                                    "~/Areas/{1}/Views/{0}.master",
                                    "~/Views/{1}/{0}.master",
                                    "~/Views/Shared/{0}.master"
                                };

        ViewLocationFormats = new[]
                                {
                                    "~/Areas/{1}/Views/{0}.aspx",
                                    "~/Views/{1}/{0}.aspx",
                                    "~/Views/Shared/{0}.aspx"
                                };

        AreaPartialViewLocationFormats = new[]
                                {
                                    "~/Areas/{1}/Views/{0}.ascx",
                                    "~/Views/{1}/{0}.ascx",
                                    "~/Views/Shared/{0}.ascx"
                                };
    }
    #endregion
}

With this view engine, I’m instructing MVC to look in \Areas for our view content, or more specifically \Areas\<<controller-name>>\ for the content, i.e, if I created a BlogController, it can now look in \Areas\Blog\ for views as well as the standard views.

My project structure now looks as such:

I haven’t done anything specific to the ASP.NET MVC 2 Application project yet, I’ve simply added a required Areas folder (make sure you do this!), and changed our global application class, it’s now much leaner:

using MefMvcFramework.Web;

public class MvcApplication : Application
{
    #region Methods
    /// <summary>
    /// Initialises the application.
    /// </summary>
    protected override void Initialise()
    {

    }
    #endregion
}

I like that. Because the application class is seperate, we can re-use it in multiple web projects, with minimal implementation in the specific web projects that use it. You’ll notice there is no default route registration, thats because we now take advantage of MEF with our application, we make an instance of IRouteRegistrar for our default routes:

[Export(typeof(IRouteRegistrar)), ExportMetadata("Order", 100)]
public class DefaultRouteRegistrar : IRouteRegistrar
{
    #region Methods
    /// <summary>
    /// Registers any routes to be ignored by the routing system.
    /// </summary>
    /// <param name="routes">The collection of routes to add to.</param>
    public void RegisterIgnoreRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
        routes.IgnoreRoute("{resource}.ico/{*pathInfo}");
    }

    /// <summary>
    /// Registers any routes to be used by the routing system.
    /// </summary>
    /// <param name="routes">The collection of routes to add to.</param>
    public void RegisterRoutes(RouteCollection routes)
    {
        routes.MapRoute("Default", "{controller}/{action}", new { controller = "Home", action = "Index" });
    }
    #endregion
}

If you ensure that is done, we should be able to run the application and get our (now familiar) bland ASP.NET MVC example site. Next though, it’s time to add a new area. We going to do this by adding a new project to the solution, and changing some specifics of how it is building/outputting files. Although this won’t matter for production (you’ll hopefully have a much finer control of the build process), it does show how we can easily drop in a new module without having to recompile the main website. I’ve created a solution folder \Areas and added a new project, MefMvcFramework.Blog with a few files, here is my new project structure:

What we want, is to change the output location of the project to be in the areas folder of the main website, so open up the project properties, selet the Build tab and change the output path (do this for All Configurations, not just Debug), and change the output to be the \Areas\Blog folder of the main website.

We also need to instruct the views to be copied to the output, so select any views, and goto the Properties window, and change the Copy to Output property to Copy if newer. This means when we build the application, the Index.aspx view should end up at \Areas\Blog\Views\Index.aspx.

Right then, on to the controller, it’s not doing anything fancy, just showing us the default view:

[Export(typeof(IController)), ExportMetadata("Name", "Blog")]
public class BlogController : Controller
{
    #region Actions
    /// <summary>
    /// Displays the blog index.
    /// </summary>
    public ActionResult Index()
    {
        return View();
    }
    #endregion
}

Hopefully, building the solution and running should demonstrate that the BlogController has been loaded dynamically though MEF and is handling any requests for route /Blog. If you run this url, you should see something like:

That’s awesome, we now have a completely modular framework for ASP.NET MVC thanks to MEF. But wait, I want to go one step further. What I want to achieve is automatically adding a blog link to the navigation. Now we shouldn’t hard code this in the page, because the Blog module might not be available. What we can do is add it through the concept of verbs. So I’ve created an interface:

public interface IActionVerb
{
    #region Properties
    /// <summary>
    /// Gets the name of the verb.
    /// </summary>
    string Name { get; }

    /// <summary>
    /// Gets the action.
    /// </summary>
    string Action { get; }

    /// <summary>
    /// Gets the controller.
    /// </summary>
    string Controller { get; }
    #endregion
}

Now, what I’m going to do with that, is allow MEF to automatically import verbs, and we can then grab them based on category, so here is a sample verb I’ve added for the Blog:

[Export(typeof(IActionVerb)), ExportMetadata("Category", "Navigation")]
public class BlogVerb : IActionVerb
{
    #region Properties
    /// <summary>
    /// Gets the name.
    /// </summary>
    public string Name
    {
        get { return "My Blog"; }
    } 

    /// <summary>
    /// Gets the action.
    /// </summary>
    public string Action
    {
        get { return "Index"; }
    }

    /// <summary>
    /// Gets the controller.
    /// </summary>
    public string Controller
    {
        get { return "Blog"; }
    }
    #endregion
}

We’re going to be mapping verbs to actions on controllers, so let’s make some changes to our application class, by adding a static field:

  private static IEnumerable<Lazy<IActionVerb, IActionVerbMetadata>> ActionVerbs;

Now, MEF can’t compose static instances, and unfortunately we can’t rely on an instance member either, as an instance of the application class is created with each request (but the Application_Start() method is only executed once for the lifetime of the web application). So what do we do? We create the static field, but don’t mark it as an import, instead, we can manually assign to this field during composition:

  protected virtual void Compose()
  {
      var container = CreateCompositionContainer();
      if (container == null)
          return;

      container.ComposeParts(this);
      ActionVerbs = container.GetExports<IActionVerb, IActionVerbMetadata>();
  }

Thats the important part done, now lets modify the default view files to support this, firstly, let’s change our MasterPage:

  <%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>

  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml">
  <head runat="server">
      <title><asp:ContentPlaceHolder ID="TitleContent" runat="server" /></title>
      <link href="../../Content/Site.css" rel="stylesheet" type="text/css" />
  </head>

  <body>
      <div class="page">

          <div id="header">
              <div id="title">
                  <h1>My MVC Application</h1>
              </div>

              <div id="logindisplay">
                  <% Html.RenderPartial("LogOnUserControl"); %>
              </div> 

              <% Html.RenderPartial("NavigationItems"); %>
          </div>

          <div id="main">
              <asp:ContentPlaceHolder ID="MainContent" runat="server" />

              <div id="footer">
              </div>
          </div>
      </div>
  </body>
  </html>

I’ve taken out the original menu, and added a RenderPartial instruction, which accepts our NavigationItems.ascx view, which we’ll go ahead and create now:

  <%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
  <%@ Import Namespace="MefMvcApplication" %>
  <div id="menucontainer">

      <ul id="menu">
          <li><%: Html.ActionLink("Home", "Index", "Home")%></li>
          <li><%: Html.ActionLink("About", "About", "Home")%></li>
          <% foreach (var verb in MvcApplication.GetVerbsForCategory("Navigation")) { %>
              <li><%: Html.ActionLink(verb.Name, verb.Action, verb.Controller)%></li>
          <% } %>
      </ul>

  </div>

But how do we get to our verbs? We need to add another method to our application class, GetVerbsForCategory(string):

  public static IEnumerable<IActionVerb> GetVerbsForCategory(string category)
  {
      Throw.IfArgumentNullOrEmpty(category, "category");

      return ActionVerbs
          .Where(l => l.Metadata.Category.Equals(category, StringComparison.InvariantCultureIgnoreCase))
          .Select(l => l.Value);
  }

And that should be it! Run the application again, you should see a My Blog link in the navigation, all handled dynamically!

There are still a few things I might try and work out, but I am now quite satisfied with this implementation. You can pull the Blog module out of the system and it will simply not be loaded when the application restarts. We can now also dynamically update our modules without worrying about our parent ASP.NET MVC application. I hope you enjoy it, let me know what you think.

Download VS2010 Project

Digg This
Reddit This
Stumble Now!
Buzz This
Vote on DZone
Share on Facebook
Bookmark this on Delicious
Kick It on DotNetKicks.com
Shout it
Share on LinkedIn
Bookmark this on Technorati
Post on Twitter
Google Buzz (aka. Google Reader)

Tags ___ , , ,