Friday, April 20, 2012

Reusing views between areas in ASP.NET MVC

ASP.NET MVC Area's are great for organizing functionality that logically goes together. However, the default view location logic is somewhat limited.

If I want to use a view from one area in a controller for another, I can specify it's path explicitly. This works fine. However, when the view uses EditorTemplates or DisplayTemplates in the corresponding subfolders, the view engine will not be able to locate these because it will try to resolve these independently from the view that is being rendered based on the executing controller and area.

What's going wrong?

The ViewEngine is in charge of resolving views and templates based on the controller name, the view name and the area name. By default the WebFormViewEngine considers the following paths:

~/Areas/[area]/Views/[controller]/[view].aspx
~/Areas/[area]/Views/[controller]/[view].ascx
~/Areas/[area]/Views/Shared/[view].aspx
~/Areas/[area]/Views/Shared/[view].ascx

It then falls back to non-area based views :

~/Views/[controller]/[view].aspx
~/Views/[controller]/[view].ascx
~/Views/Shared/[view].aspx
~/Views/Shared/[view].ascx

As you can see, the view engine expects the view and any EditorTemplate or DisplayTemplate to be located in either the area of the controller or in the main views folder.

So, one possible solution for sharing views and templates between areas is to move the files to the main views folder. However, since my main views folder is crowded enough as it is, I prefer not to do that.

Since the default view engine uses simple path generation based on the area, controller and view names it's not going to be trivial to force it to look for templates in the folders relative to the currently rendering view. That context is simply not available.

Solution

An alternate approach is to expand on the Shared folder pattern by introducing a Shared area. We need to tweak the view engine a bit to get that supported:

public class WebFormViewEngine : System.Web.Mvc.WebFormViewEngine
{
  public WebFormViewEngine()
     : this( null )
  {
  }

  public WebFormViewEngine( IViewPageActivator viewPageActivator ) : base( viewPageActivator )
  {
     AreaViewLocationFormats = AreaViewLocationFormats
        .Union( new[] { "~/Areas/Shared/Views/{1}/{0}.aspx", "~/Areas/Shared/Views/{1}/{0}.ascx" } )
        .ToArray();

     AreaPartialViewLocationFormats = AreaViewLocationFormats;
  }
}

That's all there is to it. We've added some additional search paths for the view engine to consider. It will now look in the Shared area folder for views and templates.
During application startup, you do need to replace the default WebFormsViewEngine with this customized version:

ViewEngines.Engines.Remove( ViewEngines.Engines.First( e => e is System.Web.Mvc.WebFormViewEngine ) );
ViewEngines.Engines.Insert( 0, new WebFormViewEngine() );

If you're using Razor views, you can apply a similar fix to the RazorViewEngine.

TL;DR

Reusing views from one area in a controller for another area is possible by specifying the full path to the view. Doing so however breaks the Editor- and DisplayTemplates because they are resolved independently from the view that is being rendered.
The solution is to introduce a Shared area. Move the views and templates there and then tweak the view engine to also consider the shared area when resolving the views and templates.

Further reading