Sunday, February 1, 2009

Mixing webforms with MVC

MVC offers a lot of nice features but switching from regular ASP.NET webforms to MVC on exisiting projects is not easy. Out of the box, MVC is very rigid about where you place your views (.aspx). There is little support for user controls and no support at all for custom controls. This makes mixing webforms with MVC next to impossible without breaking existing functionality.

With a couple of additions to MVC though you're pretty much free to do what you want. The functionality introduced in this post enables mixing of MVC and WebForms pages. You can reuse exisiting controls, both user controls and custom controls. You can even go as far as rendering pages from just a master page and controls.

This post will show you how to compose pages from controls using the same mechanisms ASP.NET uses when rendering regular pages from .aspx files. First we'll look at the way master pages and regular .aspx pages interact and then I'll show you how to uses this interaction to do the same from an MVC controller. For some background information the internals of pages you may also want to check an earlier post on the subject.

Master pages in a nutshell
Master pages in ASP.NET use content placeholders to indicate where the page may be customized. Normally, each page (.aspx) declares content within one or more content controls. At runtime (or when precompiling) ASP.NET will generate code that registers the contents with each template container. When the page is being processed during a web request, it instantiates the contents of each of the content controls in the templates available from the master page. The key method is in the Page.AddContentTemplate( string name, ITemplate template ) method. In pages compiled from an .aspx file this method will be invoked while building the page control structure. This is done from an override of the  Page.FrameworkInitialize method, containing most of the generated code for the page. We can use that to hook up whatever content we want to render in a master page.

Implementation
The code below shows a ViewPage that supports this mechanism:

class TemplateViewPage : System.Web.Mvc.ViewPage
{
  protected override void FrameworkInitialize()
  {
    base.FrameworkInitialize();
    foreach ( var template in Templates )
    {
      AddContentTemplate( template.Key, template.Value );
    }
  }

  public Dictionary<string, Template> Templates
  {
    get{ return _templates; }
  }

  Dictionary<string, Template> _templates = new Dictionary<string, Template>();
}
This class enables us to setup a template for each content placeholder in a master page. The Template class is a straight up implementation of the ITemplate interface that supports multiple controls to be added as either a virtual path or a control instance.
class Template : ITemplate
{
  public void AddControl( string virtualPath )
  {
    if ( string.IsNullOrEmpty( virtualPath ) )
      throw new ArgumentNullException( "virtualPath" );

    if ( virtualPath[ 0 ] != '~' && virtualPath[ 0 ] != '/' )
    {
      _items.Add( new TemplateItem { _virtualPath = string.Format( "~/{0}", virtualPath ) } );
    }
    else
    {
      _items.Add( new TemplateItem { _virtualPath = virtualPath } );
    }
  }

  public void AddControl( Control controlInstance )
  {
    if ( null == controlInstance )
      throw new ArgumentNullException( "controlInstance" );

    _items.Add( new TemplateItem { _control = controlInstance } );
  }

  void ITemplate.InstantiateIn( Control container )
  {
    foreach ( var item in _items )
    {
      var control = item._control;
      if ( null == control )
      {
        control = System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath( item._virtualPath, typeof( Control ) ) as Control;
      }

      container.Controls.Add( control );
      }
  }

  List<TemplateItem> _items = new List<TemplateItem>();
      

  private class TemplateItem
  {
    internal Control _control;
    internal string _virtualPath;   
  }
}
With these classes we have the plumming setup to render a page using a master page and inject any control we want into that. Next, this needs to be hooked up to MVC. The way to do this is create a custom ActionResult.
public class TemplateResult : ActionResult, IView
{
  public string Theme
  {
    get { return _page.Theme; }
    set { _page.Theme = value; }
  }

  public string MasterPageFile
  {
    get { return _page.MasterPageFile; }
    set
    {
      if ( string.IsNullOrEmpty( value ) )
         throw new ArgumentNullException( "value" );
      _page.MasterPageFile = value;
    }
  }

  public void AddControl( string templateName, string virtualPath )
  {
    if ( string.IsNullOrEmpty( templateName ) )
      throw new ArgumentNullException( "templateName" );

    if ( string.IsNullOrEmpty( virtualPath ) )
      throw new ArgumentNullException( "virtualPath" );

    Template template;
    if ( !_page.Templates.TryGetValue( templateName, out template ) )
    {
      template = new Template();
      _page.Templates.Add( templateName, template );
    }
    template.AddControl( virtualPath );
  }

  public void AddControl( string templateName, Control controlInstance )
  {
    if ( string.IsNullOrEmpty( templateName ) )
      throw new ArgumentNullException( "templateName" );

    if ( null == controlInstance )
      throw new ArgumentNullException( "controlInstance" );

    Template template;
    if ( !_page.Templates.TryGetValue( templateName, out template ) )
    {
      template = new Template();
      _page.Templates.Add( templateName, template );
    }
    template.AddControl( controlInstance );
  }

  public override void ExecuteResult( ControllerContext context )
  {
    if ( string.IsNullOrEmpty( MasterPageFile ) )
      throw new InvalidOperationException( 
        "MasterPageFile must not be null or empty." );

    var viewContext = new ViewContext( context, this, 
      context.Controller.ViewData, 
      context.Controller.TempData );
    
    Render( viewContext, context.HttpContext.Response.Output );
  }

  public void Render( ViewContext viewContext, System.IO.TextWriter writer )
  {
    _page.ViewData = viewContext.ViewData;
    _page.AppRelativeTemplateSourceDirectory = "~/";
    _page.AppRelativeVirtualPath = "~/";
    _page.RenderView( viewContext );
  }

  TemplateViewPage _page = new TemplateViewPage();
}
This class provides a controller with the means to create a view and execute it. For a simple HelloWorld example the controller would look like this:
public class HelloWorldController : Controller
{
  public void Default()
  {
    TemplateResult result = new TemplateResult { MasterPageFile = "~/masterpage.master" };
    view.AddControl( "Body", new LiteralControl( "Hello world!" ) );
    return result;
  }
}

This would require a master page named materpage.master with at least one content placeholder named Body.

Real world applications
For real world applications you'd probably declare extension methods for Controller that setup the MasterPageFile and Theme as used by the site. I've found that these classes make it much easier to migrate from pure webforms to MVC. It allows you to use MVC for new developments while enabling reuse of existing controls.

[Update: 1 May 2009]
Source code is now available for download. Please read my post on Alanta.Mvc.

ASP.NET MVC strong typed routing

One of the more questionable practices in the ASP.NET MVC framework is that all routing to controllers (and views) is based on names; plain text strings. I'm assuming this is because the MVC framework is heavily influenced by existing MVC frameworks like Ruby on Rails, but I'm not so sure whether it's a good idea.

One of the first things I learned back in the days when I got started in software development is never to rely on strings; use strong typing wherever possible because the compiler can check types. It can't check strings.

ASP.NET MVC seems to violate this rule and instead promotes unit testing to validate routing decisions. While I'm convinced that unit testing is a good thing, I'm not at all comfortable with writing a unit test for something that a compiler can check. For example, I have a controller class named My.VeryCoolController. If I want to route requests to that controller I could set that up like this:
RouteTable.Routes.MapRoute( "coolstuff", "docoolstuff.aspx", new { controller = "VeryCool", action ="HelloWorld" } );
So the controller parameter refers to the VeryCoolController class by it’s name. Where I come from this is a very bad practice! In fact, pretty much all of the coding standards I've ever seen will explicitly forbid this. So, why not do this:
RouteTable.Routes.mapRoute<my.verycoolcontroller>( "coolstuff", "docoolstuff.aspx", new { action = "helloworld" );

In my opinion this is much more robust; the code won't even compile if I break it somehow.

Implementation
Since the ASP.NET MVC framework is completely setup to work with names in stead of type I've created some classes that do support routing based on types. This requires the following functionality:

  1. Extension methods to define a route using the controller’s type
  2. A route handler that can spawn a strong typed HttpHandler for the controller
  3. An HttpHandler that invokes the specified controller
An extension method for mapping a route using a controller type would look like this :
public static Route MapRoute<T>( this RouteCollection routes, string name, string url, object defaults, object constraints ) 
  where T : Controller, new() 
  { 
     Route route = new Route( url, new MvcRouteHandler<t>() ) 
       { 
         Defaults = new RouteValueDictionary( defaults ), 
         Constraints = new RouteValueDictionary( constraints ) 
        }; 

     // For compatibility with MvcHandler 
     route.Defaults.Add( "controller", typeof( T ).Name.Replace( "Controller", "" ) ); 
     routes.Add( name, route ); 
     return route; 

  } 
  
Note that all error handling has been removed from this snippet. Nothing spectacular here; this is pretty much what the stock extension methods shipped with ASP.NET MVC do. One important thing to note here is that setting a value for controller is required; otherwise this like RedirectToRouteResult will not work as you'd expect. Next up, implementing a custom route handler. Not too dificult either:
internal class MvcRouteHandler<TController> : System.Web.Routing.IRouteHandler 
  where TController : System.Web.Mvc.Controller, new() 
  { 
    public IHttpHandler GetHttpHandler( RequestContext requestContext ) 
    { 
      return new MvcHttpHandler( 
        new TController(), 
	requestContext ); 
    } 
  }
The code above instantiates a new controller of the required type and hands it over to an MvcHttpHandler which we'll create next. Before we do though, note that creating the controller like this requires it to have default (parameterless) constructor. If you need some kind of dependency injection you could delegate controller creation to an Inversion-of-Control container. Finally, the MvcHttpHandler mentioned above:
internal class MvcHttpHandler : IHttpHandler, 
System.Web.SessionState.IRequiresSessionState 
{ 
  public MvcHttpHandler( System.Web.Mvc.IController controller, System.Web.Routing.RequestContext context ) 
  { 
    _controller = controller; 
    _context = context; 
  } 

  public bool IsReusable 
  { 
    get { return false; } 
  } 

  public void ProcessRequest( HttpContext context ) 
  { 
    _controller.Execute( _context ); 
  } 

  System.Web.Mvc.IController _controller; 
  System.Web.Routing.RequestContext _context; 
}

A couple of things to point out here. First off, to support session state the handler must inherit from IRequiresSessionState. Secondly, notice that the Execute method does not use the HttpContext instance passed into the ProcessRequest method. Instead the RequestContext required by the controller is passed in to the constructor by the route handler and stored as a field. This also means that IsReusable must return false; the handler has state specific to a request and is therefore not reusable.

And then what?
That's it. The controller has all the mechanisms needed to dynamically invoke the method associated with the request. All of MVC's routing magic will work with this setup. I'll post a zip with the full code shortly.

Sources are available as part of the Alanta.Mvc project.