Extending ASP.NET MVC View Engine to Render Device Specific Views - Part Two

In the previous post, we looked at a few ways to take advantage of ASP.NET MVC conventions and extendibility in order to render appropriate views for different devices. In this article, we look beyond that and seek alternative avenues to extend ASP.NET MVC view engine to better manage multi-device view response.

Background

Our approach to extend ASP.NET MVC view engine in the previous post will meet most use cases. In certain circumstances, we need more control and flexibility. One fallacy of that approach is potential clutter of number of files in the application folders. Looking at the solution file structure below, we can see that for only three views, we have nine views, all in the same folder. The situation would be worse if we had a couple of more view types for other views (e.g. iPad mini specific views).

Another issue arises when we want to force rendering of specific views at certain times. For example, if the views in our solution have been designed to fit both a normal computer as well as an iPad, we would want to render full views when the requesting device is an iPad. With our previous approach, if there are no iPad specific views, the mobile view will automatically be picked up as iPad is marked as a mobile device in http request and ASP.NET MVC's view engine will choose "[viewName].Mobile.cshtml" by default .

This problem can be solved.

The Good Stuff

In this new approach, we will not name the view differently anymore. Rather, we keep them in their respective folders. This way, there is a neater and less messy file structure as well as no reliance on default ASP.NET MVC default behaviour for mobile devices. Our new file structure looks like this:

Now that's much cleaner! But how do we get our custom view engine to pick up the right views? Well, as it happens, that's not too difficult. We can actually override the locations through which ASP.NET MVC engine looks for a given view when a request is received. All we need to do now, is point the view engine to these new convention of ours:

class MyViewEngine : RazorViewEngine //or WebFormViewEngine  
{      
  public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
      {
    ViewEngineResult result;
    if (controllerContext.HttpContext.Request.UserAgent.ToLower().Contains(“ipad”))
    {
      ViewLocationFormats = new[]
      {
        "~/Views/{1}/iPad/{0}.cshtml"
      };
    }
    else if (controllerContext.HttpContext.Request.Browser.IsMobileDevice)
    {
      ViewLocationFormats = new[]
      {
        "~/Views/{1}/Mobile/{0}.cshtml"
      };
    }
    else
    {
      ViewLocationFormats = new[]
      {
        "~/Views/{1}/ {0}.cshtml"
      };        
    }

    return base.FindView(controllerContext, viewName, masterName, false);
  }
}

There are three points worth mentioning here: 1) ViewLocationFormats does not need to be declared anywhere in our code, it is defined in RazorViewEngine (as well as WebFormsViewEngine) class, from which we are inheriting. 2) When trying to return the view in FindView method of base class, {0} is replaced by the view name and {1} by the controller name. 3) This is a cool approach.

I would suggest cautious to be taken when using this approach specifically if implementing to an existing application. As you can see in the example above, our application does not change the MVC convention of Views/[ControllerName]/[ActionMethodName] for full view. This is done to ensure backwards compatibility as well as preventing the next developer from going insane running into mysterious bugs.

Summary

In this post, we looked into further customising ASP.NET MVC view engine to manipulate the locations in which views are sought. This helps us achieve a more organised file structure and more control and flexibility over what's rendered for various devices.