Custom Theme MVC View Engine for Sitecore Multisite Implementation

Custom MVC view engine for extending theming in the MVC application is a well discussed topic. You will find it in many discussion forums and in many blog posts. The problem in question is, how can you extend the theme of your MVC application from a base theme, so that if you want to extend certain pages in your application, the child page should override the base page. The most well known solution is to create your own View Engine with ordered view and partial view locations and Insert your View Engine as the first item in the View Engine collection. This way MVC will find your View Engine first and find the views as you desire. Here are some code example.

    public class CustomViewEngine : RazorViewEngine
    {
        public CustomViewEngine()
        {
            var themeName = ConfigurationManager.AppSettings["ThemeName"];
            var baseThemeName = ConfigurationManager.AppSettings["BaseThemeName"];
            Assert.ArgumentNotNullOrEmpty(baseThemeName, "BaseThemeName");
            if (!String.IsNullOrWhiteSpace(themeName))
            {
                ViewLocationFormats = new string[] { "~/Themes/" + themeName + "/Views/{1}/{0}.cshtml", "~/Themes/" + themeName + "/Views/Shared/{0}.cshtml", "~/Themes/" + baseThemeName + "/Views/{1}/{0}.cshtml", "~/Themes/" + baseThemeName + "/Views/Shared/{0}.cshtml" };
                PartialViewLocationFormats = new string[] { "~/Themes/" + themeName + "/Views/{1}/{0}.cshtml", "~/Themes/" + themeName + "/Views/Shared/{0}.cshtml", "~/Themes/" + baseThemeName + "/Views/{1}/{0}.cshtml", "~/Themes/" + baseThemeName + "/Views/Shared/{0}.cshtml" };
            }
            else
            {
                ViewLocationFormats = new string[] { "~/Themes/" + baseThemeName + "/Views/{1}/{0}.cshtml", "~/Themes/" + baseThemeName + "/Views/Shared/{0}.cshtml" };
                PartialViewLocationFormats = new string[] { "~/Themes/" + baseThemeName + "/Views/{1}/{0}.cshtml", "~/Themes/" + baseThemeName + "/Views/Shared/{0}.cshtml" };
            }
        }
    }

If the ThemeName is not empty, then ViewLocationFormats and PartialViewLOcationFormats contains the ThemeName folders path first on the order and then the BaseThemeName folder path. That ensures, when there is a view available in the ThemeName path, that will be rendered, otherwise, the view from the BaseThemeName path will be rendered.

Now we have to add this custom view engine to the View Engine collection.

ViewEngines.Engines.Insert(0,new CustomViewEngine());

Usually you will do this in global.asax Application_Start event method or  WebActivator’s PreApplicationStartMethod.

This works fine, when you have developed a base website and then you customize that website for many implementation. But, at a time you have only one site.

Now, consider this situation, when you have to implement multiple sites in the same Sitecore instance and each site can have a different theming from the base theme. In this case, you will have multiple theme folders and those folders can contain the same views or partial views. If you add all these theme folders in the view location, MVC will render the one that is on top in the order, not the one that you intend it to do. How to solve this problem?

First of all, we need to know what’s the correct theme for a site in the multisite scenario. This is not difficult. We just put the theme name in the SiteDefinition.config in the site node as custom attribute ‘themName’, like below.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <sites>
      <site name="site1" patch:before="site[@name='website']" 				 enableTracking="true" 				 hostName="site1.com"          virtualFolder="/" 				 physicalFolder="/" 				 rootPath="/sitecore/content/site1" 				 startItem="/home" 				 database="web" 				 domain="extranet" 				 allowDebug="true" 				 cacheHtml="true" 				 htmlCacheSize="50MB" 				 registryCacheSize="0" 				 viewStateCacheSize="0" 				 xslCacheSize="25MB" 				 filteredItemsCacheSize="10MB" 				 enablePreview="true" 				 enableWebEdit="true" 				 enableDebugger="true" 				 disableClientData="false" 				 cacheRenderingParameters="true" 				 renderingParametersCacheSize="10MB"          themeName="site1theme"     />
      <site name="site2" patch:before="site[@name='website']" 				 enableTracking="true" 				 hostName="site2.com"          virtualFolder="/" 				 physicalFolder="/" 				 rootPath="/sitecore/content/site2" 				 startItem="/home" 				 database="web" 				 domain="extranet" 				 allowDebug="true" 				 cacheHtml="true" 				 htmlCacheSize="50MB" 				 registryCacheSize="0" 				 viewStateCacheSize="0" 				 xslCacheSize="25MB" 				 filteredItemsCacheSize="10MB" 				 enablePreview="true" 				 enableWebEdit="true" 				 enableDebugger="true" 				 disableClientData="false" 				 cacheRenderingParameters="true" 				 renderingParametersCacheSize="10MB"          themeName="site2theme"     />
    </sites>
  </sitecore>
</configuration>

We need to override two methods FindView and FindPartialView in our custom view engine. In these methods, we need to read the themName for the site in the current sitecore context and return the appropriate view ViewEngineResult.

    public class CustomViewEngine : RazorViewEngine
    {
        public CustomViewEngine()
        {
            var baseThemeName = ConfigurationManager.AppSettings["BaseThemeName"];
            Assert.ArgumentNotNullOrEmpty(baseThemeName, "BaseThemeName");
                ViewLocationFormats = new string[] { "~/Themes/" + baseThemeName + "/Views/{1}/{0}.cshtml", "~/Themes/" + baseThemeName + "/Views/Shared/{0}.cshtml" };
                PartialViewLocationFormats = new string[] { "~/Themes/" + baseThemeName + "/Views/{1}/{0}.cshtml", "~/Themes/" + baseThemeName + "/Views/Shared/{0}.cshtml" };
        }

        public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
        {
            if (controllerContext == null)
                throw new ArgumentNullException(nameof(controllerContext));
            if (string.IsNullOrEmpty(viewName))
                throw new ArgumentException("viewName");
            var currentSite = Sitecore.Context.Site;
            var themeName = currentSite.Properties["themeName"];
            string controllerName = controllerContext.RouteData.GetRequiredString("controller");
            if (!string.IsNullOrEmpty(themeName) && !viewName.Contains("/"))
            {
                var viewPath = $"~/themes/{themeName}/views/{controllerName}/{viewName}.cshtml";
                //If the view file doesn't exists in the folder look at the shared folder
                var absolutePath = HttpContext.Current.Server.MapPath(viewPath);
                if (!System.IO.File.Exists(absolutePath))
                {
                    viewPath = $"~/themes/{themeName}/views/shared/{viewName}.cshtml";
                    absolutePath = HttpContext.Current.Server.MapPath(viewPath);
                    if (!System.IO.File.Exists(absolutePath))
                    {
                        throw new Exception(string.Format("View {0} doesn't exists.", viewName));
                    }
                }
                return new ViewEngineResult(this.CreateView(controllerContext, viewPath, string.Empty), (IViewEngine)this);
            }
            return base.FindView(controllerContext, viewName, masterName, useCache);
        }

        public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
        {
            if (controllerContext == null)
                throw new ArgumentNullException(nameof(controllerContext));
            if (string.IsNullOrEmpty(partialViewName))
                throw new ArgumentException("partialViewName");
            var currentSite = Sitecore.Context.Site;
            var themeName = currentSite.Properties["themeName"];
            string controllerName = controllerContext.RouteData.GetRequiredString("controller");
            if (!string.IsNullOrEmpty(themeName) && !partialViewName.Contains("/"))
            {
                var partilaViewPath = $"~/themes/{themeName}/views/{controllerName}/{partialViewName}.cshtml";
                //If the view file doesn't exists in the folder look at the shared folder
                var absolutePath = HttpContext.Current.Server.MapPath(partilaViewPath);
                if (!System.IO.File.Exists(absolutePath))
                {
                    partilaViewPath = $"~/themes/{themeName}/views/shared/{partialViewName}.cshtml";
                    absolutePath = HttpContext.Current.Server.MapPath(partilaViewPath);
                    if (!System.IO.File.Exists(absolutePath))
                    {
                        throw new Exception(string.Format("View {0} doesn't exists.", partialViewName));
                    }
                }
                return new ViewEngineResult(this.CreatePartialView(controllerContext, partilaViewPath), (IViewEngine)this);
            }
            return base.FindPartialView(controllerContext, partialViewName, useCache);
        }

    }

That’s it! Happy coding.

Advertisements

About Himadri Chakrabarti

I am a software developer architect and a Sitecore MVP. My professional interest is everything and anything related to Software Architecture, .NET, Sitecore, Node.js, NoSQL etc. Outside of my profession, I am a hobbyist photographer. Link to my photography site http://himadriphotography.com/
This entry was posted in MVC, Sitecore and tagged , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s