A Custom View Engine with Dynamic View Location

    Introduction:

          One of the nice feature of ASP.NET MVC framework is its pluggability. This means you can completely replace the default view engine(s) with a custom one. One of the reason for using a custom view engine is to change the default views location and sometimes you need to change the views location at run-time. For doing this, you can extend the default view engine(s) and then change the default views location variables at run-time.  But, you cannot directly change the default views location variables at run-time because they are static and shared among all requests. In this article, I will show you how you can dynamically change the views location without changing the default views location variables at run-time.

 

    Description:

          Let's say you need to synchronize the views location with controller name and controller namespace. So, instead of searching to the default views location(Views/ControllerName/ViewName) to locate views, this(these) custom view engine(s) will search in the Views/ControllerNameSpace/ControllerName/ViewName folder to locate views.

          First of all create a sample ASP.NET MVC 3 application and then add these custom view engines to your application,

 

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

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

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

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

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

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

        protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
        {
            var nameSpace = controllerContext.Controller.GetType().Namespace;
            return base.CreatePartialView(controllerContext, partialPath.Replace("%1", nameSpace));
        }

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

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

    }



    public class MyWebFormViewEngine : WebFormViewEngine
    {
        public MyWebFormViewEngine() : base()
        {
            MasterLocationFormats = new[] {
                "~/Views/%1/{1}/{0}.master",
                "~/Views/%1/Shared/{0}.master"
            };

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

            ViewLocationFormats = new[] {
                "~/Views/%1/{1}/{0}.aspx",
                "~/Views/%1/{1}/{0}.ascx",
                "~/Views/%1/Shared/{0}.aspx",
                "~/Views/%1/Shared/{0}.ascx"
            };

            AreaViewLocationFormats = new[] {
                "~/Areas/{2}/Views/%1/{1}/{0}.aspx",
                "~/Areas/{2}/Views/%1/{1}/{0}.ascx",
                "~/Areas/{2}/Views/%1/Shared/{0}.aspx",
                "~/Areas/{2}/Views/%1/Shared/{0}.ascx",
            };

            PartialViewLocationFormats = ViewLocationFormats;
            AreaPartialViewLocationFormats = AreaViewLocationFormats;
        }

        protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
        {
            var nameSpace = controllerContext.Controller.GetType().Namespace;
            return base.CreatePartialView(controllerContext, partialPath.Replace("%1", nameSpace));
        }

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

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

    }

 

          Here, I am extending the RazorViewEngine and WebFormViewEngine class and then appending /%1 in each views location variable, so that we can replace /%1 at run-time. I am also overriding the FileExists, CreateView and CreatePartialView methods. In each of these method implementation, I am replacing /%1 with controller namespace. Now, just register these view engines in Application_Start method in Global.asax.cs file,

 

    protected void Application_Start()
    {
	ViewEngines.Engines.Clear();
	ViewEngines.Engines.Add(new MyRazorViewEngine());
	ViewEngines.Engines.Add(new MyWebFormViewEngine());
	................................................
	................................................
    }

 

          Now just create a controller and put this controller's view inside Views/ControllerNameSpace/ControllerName folder and then run this application. You will find that everything works just fine.

 

    Summary:

          ASP.NET MVC uses convention over configuration to locate views. For many applications this convention to locate views is acceptable. But sometimes you may need to locate views at run-time. In this article, I showed you how you can dynamically locate your views by using a custom view engine. I am also attaching a sample application. Hopefully you will enjoy this article too.

 

15 Comments

  • hi.
    Nice one, but what is your idea for managing masterpages?

  • @tilde,
    IMHO, I think MasterPages/LayOuts should place in the Shared folder.

  • I don't see that it gives us

  • I had the same reaction as Chris van Dam, I am also working on an MVC solution that uses multitenant architecture and found overriding the RazorViewEngine a godsend for having seperate pages for each tennant.

    Thanks a lot.

  • Just an update. I found an issue when I released my solution to a live environment that the view engine caches the cirtual path/directory of the view for each action on a controller. So when I went to one tennant's about page and then switched to the about page for another tennant where the about page for the second tennant is not in a tennant specific folder, it would try to pick it from a tennant folder. This caused a 404 error and so I resolved this by overriding the FindView method in the custom view engine like below:

    public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
    {
    return base.FindView(controllerContext, viewName, masterName, false);
    }

    As you can see in the override I'm not doing anything special apart from calling the base implementation but making sure the paramter useCache is set to false. This ensures that the ViewEngine looks for specific views each time rather than using a cached path.

    Hope this helps someone else.

  • @The Lone Webbie,
    Thanks for your tip.

  • Dear Imran,
    Excellent short note.
    Can you share your thoughts on how will the described way will work in the multi-tenant scenario where the views and controllers can be overridden by different tenants and each tenant will be in different assembly instead of the main web project assembly.

  • @Farooq, the location of controller is not a problem in MVC. But, if you need to move your views into a separate assembly then you can use this, http://blog.davidebbo.com/2011/06/precompile-your-mvc-views-using.html. The above scenario will only work for dynamic view location.

  • I want to know what is the formula of view locations formatting string? In other words when we see "~/views/{0}/{1}", what would be used for {0} and what for {1}?

    I searched almost for 4 hours, and couldn't find anything. Can you help me please?

  • @Saeed, {0} is viewName and {1} is controllerName. See MVC 3 source, ViewLocation class.

  • Awesome! You saved me. Thank Imran

  • Your article is very nice and help me allot.Thanks :)

  • Very Nice article, Thank You for sharing.

    But ihave a question when you use Iview Dynamic view so the Image and the css path are mapped to the route.

    I want to make those html element paths the same of view path.. Do you have a solution for that?

  • @Enes, please post specific question on forums.asp.net or stackoverflow.com

  • I am trying to use this approach but my Html.Action and Html.RenderAction throw exception : Error executing child request for handler 'System.Web.Mvc.HttpHandlerUtil+ServerExecuteHttpHandlerAsyncWrapper'.
    Any idea why?

Comments have been disabled for this content.