Displaying Custom ASP.NET MVC Views per Deployment

Have you ever been asked to deploy a second (or third or fourth) instance of an ASP.NET MVC app but with minor customizations to the views? Maybe it’s something as simple as a logo or color change, but more likely it is much more elaborate customizations that require completely different layouts and view structure.

One option would be to add application settings for each individual view variation then add conditional logic to your razor views so they can render differently based on those settings. This might work for very simple cases but it will quickly becomes unwieldy.

I have found an option using a custom view engine that is surprisingly simple. Here’s how it works.

View Folder Structure

We will implement a custom view engine that will extend the existing View folder layout to support custom views per deployment. By default, everything works just like the base MVC rendering engine. If you need to customize a view for a particular deployment, just add views in a sub-folder with a name that is unique to that deployment.

In the example below, there is a custom Contact.cshtml view for deployments using Client1 and _Client2. _There is also a default Contact.cshtml that is used for any other deployments. The custom views are optional. The flexibility here I great because it allows us to put anything we want in the custom Client1 and Client2 views.

Note that the custom views are completely optional. In the example above, there are no customizations for the About.cshtml and Index.cshtml pages. All deployments will use the default views.

Custom View Engine

To make this all work, we need to implement a custom view engine that knows to look for custom views before falling back to the default views.

This is actually much easier than I had anticipated. The constructor for our custom view engine takes in a string indicating the name of the folders that would contain the custom views. Using that folder name, all it needs to do is specify new view location formats for the RazorViewEngine base class.

public class CustomViewEngine : RazorViewEngine
{
    public CustomViewEngine(string customViewName)
    {
        base.AreaViewLocationFormats = new string[] {
            "~/Areas/{2}/Views/{1}/" + customViewName + "/{0}.cshtml",
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/" + customViewName + "/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml"

        };

        base.AreaMasterLocationFormats = new string[] {
            "~/Areas/{2}/Views/{1}/" + customViewName + "/{0}.cshtml",
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/" + customViewName + "/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml"

        };

        base.AreaPartialViewLocationFormats = new string[] {
            "~/Areas/{2}/Views/{1}/" + customViewName + "/{0}.cshtml",
            "~/Areas/{2}/Views/{1}/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/" + customViewName + "/{0}.cshtml",
            "~/Areas/{2}/Views/Shared/{0}.cshtml"
        };

        base.ViewLocationFormats = new string[] {
            "~/Views/{1}/" + customViewName + "/{0}.cshtml",
            "~/Views/{1}/{0}.cshtml",
            "~/Views/Shared/" + customViewName + "/{0}.cshtml",
            "~/Views/Shared/{0}.cshtml",               
        };

        base.PartialViewLocationFormats = new string[] {
            "~/Views/{1}/" + customViewName + "/{0}.cshtml",
            "~/Views/{1}/{0}.cshtml",
            "~/Views/Shared/" + customViewName + "/{0}.cshtml",
            "~/Views/Shared/{0}.cshtml"
        };

        base.MasterLocationFormats = new string[] {
            "~/Views/{1}/" + customViews + "/{0}.cshtml",
            "~/Views/{1}/{0}.cshtml",
            "~/Views/Shared/{1}/" + customViewName + "/{0}.cshtml",
            "~/Views/Shared/{0}.cshtml"
        };
    }
} 

Note that I did not include *.vbtml because my app only uses cshtml files.

In web.config, add an app setting to specify which custom view folder you want to use:

  <appSettings>
    <add key="CustomViewName" value="Client1"/>
  </appSettings>

 

And finally, set the view engine in the Global.asax application start method

ViewEngines.Engines.Clear();
string customViewName = ConfigurationManager.AppSettings["CustomViewName"];
ViewEngines.Engines.Add(new CustomViewEngine(customViewName));

 

Now, by changing a setting in Web.config, we can show custom views in different deployments of the application.

Keeping it manageable

This can very quickly become difficult to manage. One trick is to use partial views for the portion of a page that might be different between deployments. For example, if the top navigation header is different per deployment, then I would extract the navigation header to a partial view and create custom version of that partial.

This way, my base _Layout.cshtml stays simple and is the same for all deployments.

<body>
    @Html.Partial("_NavHeader")
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>

    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")
    @RenderSection("scripts", required: false)
</body>

…and I am able to override the navigation header as needed.

Conclusion

Now we have a simple approach that allows us to have customized views for individual deployments of our ASP.NET MVC application. Keep in mind that this approach only addresses the problem of customizing views between deployments. It does not in any way address the problem of having custom application logic in your controllers or services.