Web UI Migrations with ASP.NET MVC

Introduction

We recently faced a challenge in the project that I’ve been working on, to start working on the Web UI migration to a brand new Look and Feel UI. This sounds great, cause this is not just a minor tweak of visual elements… this is a fully blown rearchitecture of the Front End of our Single-Page Web Application.

The Challenge

Well, we all want the rearchitecture cause it will allow us to migrate/move from a mix of technologies into new (better planned for) frameworks (namely, we are moving from Knockout.js and Davis.js into AngularJS amongst others). However, we need to keep support and maintenance to the current UI Framework while we develop and test the new UI toolset.

Why is this a challenge, you ask? Well, while I would normally use a Source Control branching model for this new “Big Feature”, our current TFS Source Control in conjunction with some complex Continuous Integration environment makes it impossible to effectively use Branches.

So how do we achieve isolated development of the new UI in our ASP.NET MVC-based Web Application?

The Proposed Solution

Given the fact that we are only changing the Front End Framework set, the backend API services should remain (ideally) the same. So how about we find a way to put together the new UI while maintaining the old UI? I got the idea from Imran Baloch’s Blog… We create a Custom View Engine to dynamically render a View based on a UIVersion flag!!

I recommend you read this post “A Custom View Engine with Dynamic View Location” as it goes into details that I don’t really want to duplicate here (nor claim them my own original invention).

The Concepts

So we want to enable our set of Controllers, Models, ViewModels and Backend in general to be able to render different Views depending on the target UIVersion of our Web Application.

  • We need our Controllers to implement a UIVersion property that determines which Version of the Views the ViewEngine should render.
  • We need to make up a convention on how to name our Views so that we can create multiple Views for different Versions. For the sake of this post, we’ll use Index.v2.cshtml for UIVersion 2, and Index.cshtml as default.
  • We need to override our ViewEngine (RazorViewEngine in this post, but most of this can be analogously applied to the WebFormViewEngine) to select the right View to render depending on the UIVersion that the Controller determines.

The Code

Before we start, you can find a working copy of the code snippets listed here at Prosoft’s GitHub Blog Repository.

So, to achieve the first objective (to have our Controllers implement a UIVersion property), we simply need to create a base Controller class (if don’t already have one) and have all of our Controllers inherit from it (instead of the Controller class provided by the MVC Framework).

File /Controllers/BaseController.cs

///
/// Our Base Controller Class that we'll use as base of all of our Controllers.
/// We'll define common behavior here.
///
public abstract class BaseController : Controller
{
    public virtual int UIVersion
    {
        get { return 1; }
    }
}

Now that we have a base controller, and we have made all of our Controllers inherit from our BaseController, we’ll create a UIVersion 2 View file that we will represent our new UI to implement.

File /Views/Home/Index.v2.cshtml (we are assuming that a /Views/Home/Index.cshtml already exists)

@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Version 2 of the Home page</title>
</head>
<body>
    <div>
        This is the Version 2 of the home page.
    </div>
</body>
</html>

And the 3rd and final objective, we need to create a Custom ViewEngine that will understand the UIVersion property (when available) and render the correct View for it. For this, we follow the steps in this post, and our final MyRazorViewEngine will look something like this:

File /ViewEngines/UIVersionedRazorViewEngine.cs

    public class UIVersionedRazorViewEngine : RazorViewEngine
    {
        public UIVersionedRazorViewEngine()
            : base()
        {
            AreaViewLocationFormats = new[] {
                "~/Areas/{2}/Views/{1}/{0}.v%UIVersion%.cshtml",
                "~/Areas/{2}/Views/{1}/{0}.v%UIVersion%.vbhtml",
                "~/Areas/{2}/Views/Shared/{0}.v%UIVersion%.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.v%UIVersion%.vbhtml",
                "~/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Areas/{2}/Views/{1}/{0}.vbhtml",
                "~/Areas/{2}/Views/Shared/{0}.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.vbhtml"
            };

            AreaMasterLocationFormats = new[] {
                "~/Areas/{2}/Views/{1}/{0}.v%UIVersion%.cshtml",
                "~/Areas/{2}/Views/{1}/{0}.v%UIVersion%.vbhtml",
                "~/Areas/{2}/Views/Shared/{0}.v%UIVersion%.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.v%UIVersion%.vbhtml",
                "~/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Areas/{2}/Views/{1}/{0}.vbhtml",
                "~/Areas/{2}/Views/Shared/{0}.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.vbhtml"
            };

            AreaPartialViewLocationFormats = new[] {
                "~/Areas/{2}/Views/{1}/{0}.v%UIVersion%.cshtml",
                "~/Areas/{2}/Views/{1}/{0}.v%UIVersion%.vbhtml",
                "~/Areas/{2}/Views/Shared/{0}.v%UIVersion%.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.v%UIVersion%.vbhtml",
                "~/Areas/{2}/Views/{1}/{0}.cshtml",
                "~/Areas/{2}/Views/{1}/{0}.vbhtml",
                "~/Areas/{2}/Views/Shared/{0}.cshtml",
                "~/Areas/{2}/Views/Shared/{0}.vbhtml"
            };

            ViewLocationFormats = new[] {
                "~/Views/{1}/{0}.v%UIVersion%.cshtml",
                "~/Views/{1}/{0}.v%UIVersion%.vbhtml",
                "~/Views/Shared/{0}.v%UIVersion%.cshtml",
                "~/Views/Shared/{0}.v%UIVersion%.vbhtml",
                "~/Views/{1}/{0}.cshtml",
                "~/Views/{1}/{0}.vbhtml",
                "~/Views/Shared/{0}.cshtml",
                "~/Views/Shared/{0}.vbhtml"
            };

            MasterLocationFormats = new[] {
                "~/Views/{1}/{0}.v%UIVersion%.cshtml",
                "~/Views/{1}/{0}.v%UIVersion%.vbhtml",
                "~/Views/Shared/{0}.v%UIVersion%.cshtml",
                "~/Views/Shared/{0}.v%UIVersion%.vbhtml",
                "~/Views/{1}/{0}.cshtml",
                "~/Views/{1}/{0}.vbhtml",
                "~/Views/Shared/{0}.cshtml",
                "~/Views/Shared/{0}.vbhtml"
            };

            PartialViewLocationFormats = new[] {
                "~/Views/{1}/{0}.v%UIVersion%.cshtml",
                "~/Views/{1}/{0}.v%UIVersion%.vbhtml",
                "~/Views/Shared/{0}.v%UIVersion%.cshtml",
                "~/Views/Shared/{0}.v%UIVersion%.vbhtml",
                "~/Views/{1}/{0}.cshtml",
                "~/Views/{1}/{0}.vbhtml",
                "~/Views/Shared/{0}.cshtml",
                "~/Views/Shared/{0}.vbhtml"
            };
        }

        protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
        {
            return base.CreatePartialView(controllerContext, ContextualizePath(controllerContext, partialPath));
        }

        protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
        {
            var nameSpace = controllerContext.Controller.GetType().Namespace;
            return base.CreateView(controllerContext, ContextualizePath(controllerContext, viewPath), ContextualizePath(controllerContext, masterPath));
        }

        protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
        {
            var nameSpace = controllerContext.Controller.GetType().Namespace;
            return base.FileExists(controllerContext, ContextualizePath(controllerContext, virtualPath));
        }

        protected string ContextualizePath(ControllerContext controllerContext, string path)
        {
            var UIVersion = 1;
            if (controllerContext.Controller is BaseController)
            {
                UIVersion = (controllerContext.Controller as BaseController).UIVersion;
            }
            return path.Replace("%UIVersion%", UIVersion.ToString());
        }
    }

Make sure you register your customized ViewEngine with the MVC Framework as explained in this post.

We are almost done now. The only pending step is to setup our Controller to use whatever logic you want to determine the UIVersion. In this example, I’ve overriden the UIVersion property (though I could’ve just modified the BaseController) to get the UI Version from the Query String parameter “UIVersion”.

File /Controllers/HomeController.cs

    public class HomeController : BaseController
    {
        public override int UIVersion
        {
            get 
            {
                var UIVersion = 1;
                Int32.TryParse(this.ControllerContext.HttpContext.Request.QueryString["UIVersion"], out UIVersion);
                return UIVersion;
            }
        }

        public ActionResult Index()
        {
            return View();
        }

    }

Testing the Code

Our solution is ready to be put to the test. All we need to do is navigate to the Home path ~/ in our browser. Then, navigate to ~/?UIVersion=2. You should see the difference.

Multi-Versioned UI

Conclusion

Hope this works out useful for you. Really, this approach just unveils the power of the ASP.NET MVC Framework to extend its functionality and achieve whatever you want! Keep in mind that you can use this ViewEngine feature overriding for parallel UI development, or even to allow users to experience different View Look & Feel based on whatever parameter you prefer (maybe you have to show a different UI experience to different clients on the same UI).

Anyway, be sure to leave me feedback below if you liked this post (and even if you didn’t, but with constructive criticism in mind ;)).