Theme based Razor View Engine.

In one of our project there was a need to load different razor views (.cshtml) based on Theme (a value on the site settings) say In Multi site environment a component should render (UI) differently based on site theme.

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, so here Solution is to create custom view engine with logic to render view based on Theme.

Note: Organize Views under folders specific to theme, example

ThemeA specific Banner view at /Areas/Views/Component/ThemeA/Banner.cshtml.

ThemeB specific Banner view at /Areas/Views/Component/ThemeB/Banner.cshtml.

First we have to store the Theme value for a site- There are many ways to do this, the option we used is to Write a Pipline (Url resolver/Context Resolver) to parse the request before it reaches the MVC view engine and store the Theme value in HttpContext

HttpContext.Current.Items[“theme”]=“ThemeA”

Now Create a custom view engine, Inherit from RazorViewEngine and overwrite FindPartialView and FindView functions

[code language=”C#”]
public class ThemeRazorViewEngine : RazorViewEngine
{

//Below are the default paths (with or without Area along with Shared) MVC razor view engine looks for views and partial views
private readonly string[] _areaLocationFormat = new string[] { "~/Areas/{2}/Views/{1}/{0}.cshtml", "~/Areas/{2}/Views/{1}/{0}.vbhtml","~/Areas/{2}/Views/Shared/{0}.cshtml", "~/Areas/{2}/Views/Shared/{0}.vbhtml" };

private readonly string[] _viewLocationFormat = new string[] { "~/Views/{1}/{0}.cshtml", "~/Views/{1}/{0}.vbhtml","~/Views/Shared/{0}.cshtml", "~/Views/Shared/{0}.vbhtml" };

public ThemeRazorViewEngine()
{
base.set_AreaViewLocationFormats(this._areaLocationFormat);
base.set_AreaMasterLocationFormats(this._areaLocationFormat);
base.set_AreaPartialViewLocationFormats(this._areaLocationFormat);
base.set_ViewLocationFormats(this._viewLocationFormat);
base.set_MasterLocationFormats(this._viewLocationFormat);
base.set_PartialViewLocationFormats(this._viewLocationFormat);
base.set_FileExtensions(new string[] { "cshtml", "vbhtml" });
}

public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
{
string theme = HttpContext.Current.Items["theme"] //Get Theme Value stored in context
if (theme == null)//if no theme set, let it load from original location
{
base.set_AreaPartialViewLocationFormats(this._areaLocationFormat);
base.set_PartialViewLocationFormats(this._viewLocationFormat);
}
else
{
base.set_AreaPartialViewLocationFormats(this.GetLocationFormatByTheme(theme, true).Concat<string>(this._areaLocationFormat).ToArray<string>());
base.set_PartialViewLocationFormats(this.GetLocationFormatByTheme(theme, false).Concat<string>(this._viewLocationFormat).ToArray<string>());
}
return base.FindPartialView(controllerContext, partialViewName, false);
}

public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
string theme = HttpContext.Current.Items["theme"] //Get Theme Value stored in context
if (theme == null)//if no theme set, let it load from original location
{
base.set_AreaPartialViewLocationFormats(this._areaLocationFormat);
base.set_PartialViewLocationFormats(this._viewLocationFormat);
base.set_AreaMasterLocationFormats(this._areaLocationFormat);
base.set_MasterLocationFormats(this._viewLocationFormat);
}
else
{
base.set_AreaViewLocationFormats(this.GetLocationFormatByTheme(theme, true).Concat<string>(this._areaLocationFormat).ToArray<string>());
base.set_ViewLocationFormats(this.GetLocationFormatByTheme(theme, false).Concat<string>(this._viewLocationFormat).ToArray<string>());
base.set_AreaMasterLocationFormats(this.GetLocationFormatByTheme(theme, true).Concat<string>(this._areaLocationFormat).ToArray<string>());
base.set_MasterLocationFormats(this.GetLocationFormatByTheme(theme, false).Concat<string>(this._viewLocationFormat).ToArray<string>());
}
return base.FindView(controllerContext, viewName, masterName, false);
}

private string[] GetLocationFormatByTheme(string theme, bool isArea)
{
List<string> strs = new List<string>();
if (!isArea)
{
//Append the theme value to the path so view engine will look for a view/partial view at that location
//For example look for ThemeA specific view at Views/{1}/ThemeA/{0}.
strs.AddRange(this.GetPaths("~/Views/{1}/", theme, "/{0}.cshtml"));
strs.AddRange(this.GetPaths("~/Views/{1}/", theme, "/{0}.vbhtml"));
strs.AddRange(this.GetPaths("~/Views/Shared/", theme, "/{0}.cshtml"));
strs.AddRange(this.GetPaths("~/Views/Shared/", theme, "/{0}.vbhtml"));
}
else
{
strs.AddRange(this.GetPaths("~/Areas/{2}/Views/{1}/", theme, "/{0}.cshtml"));
strs.AddRange(this.GetPaths("~/Areas/{2}/Views/{1}/", theme, "/{0}.vbhtml"));
strs.AddRange(this.GetPaths("~/Areas/{2}/Views/Shared/", theme, "/{0}.cshtml"));
strs.AddRange(this.GetPaths("~/Areas/{2}/Views/Shared/", theme, "/{0}.vbhtml"));
}
return strs.ToArray();
}
[/code]

Now add this custom view engine to the view engine collection in App_Start/

[code language=”C#”]
public void Process(PipelineArgs args)
{
ViewEngines.get_Engines().Clear();//Clear all engines and add only the custom view engine
ViewEngines.get_Engines().Add(new ThemeRazorViewEngine());
}
[/code]

Now we are all set, if a Banner is requested from ThemeA specific site, a view will be loaded from /Areas/Views/Component/ThemeA/Banner.cshtml.

if a Banner is requested from ThemeB specific site, a view will be loaded from /Areas/Views/Component/ThemeB/Banner.cshtml. 

If there is a need to create Banner view specific to ThemeC, all we have to is create a view under folder ThemeC.

Happy Coding.