Overriding View Rendering CSHTML Paths in Sitecore
I was inspired to write this feature after being trained in another .Net product: Insite Commerce. I liked how it handled theming of sites and thought it would be easy to do something similar in Sitecore.
In Sitecore, component details are all stored in the rendering configuration item. These include hard coded paths to the physical cshtml file that gets loaded when a component is added to the page. In multi-site solutions where the same components are used in every site, you may end up creating many similar components in order to control the markup that gets rendered on a page. It also forces us to use different placeholder settings for each site, or have a lot of duplicate components, making it very difficult for content authors to figure out which component to use.
Lucky for us, Sitecore is very customizable. Turns out we can even override the way Sitecore determines the path to view rendering cshtml files. This enables us to keep one rendering configuration and use convention to allow us to create a set of override cshtml files for a particular site.
An example convention can inject the name of the site into the path. As an example, if we store all our global component renderings here:
/Global/Components/
We can create a site specific folder
/Themes/SiteA/Components/
/Themes/SiteA/Components/
And provide alternate "cshtml" files for sites. The nice thing about this approach is we can make the logic smart enough to check if it exists and only use it if it's defined, otherwise fall back to the master rendering location.
To affect this behavior in Sitecore, we need to implement two classes and one configuration patch file. The code is actually pretty straight forward. To override the View Render behavior, we need to create our own "ViewRenderer" and extend the "Render" Methodto set the new ViewPath before calling the base implementation. Code will look like this (with basic caching implemented to speed things up):
public class SiteViewRenderer : ViewRenderer
{
public override void Render(TextWriter writer)
{
if (Context.Site != null)
{
string vrKey = Context.Site.Name + ViewPath;
//This cache is cleared on publish by resource pipeline
//cache is used to avoid expensive check for path on every request.
var newViewPath = HttpRuntime.Cache.Get(vrKey);
if (newViewPath == null)
{
newViewPath = ViewPath;
string filePath = ViewPath.Replace(@"XXXBASEPATHXXX", @"/Themes/" +
Context.Site.Name + @"/Components");
if (HostingEnvironment.VirtualPathProvider.FileExists(filePath))
{
newViewPath = filePath;
}
HttpRuntime.Cache.Insert(vrKey, newViewPath);
}
ViewPath = newViewPath.ToString();
}
base.Render(writer);
}
}
To actually get Sitecore to use this as the ViewRenderer, we actually need to implement one more class. This class has to inherit from Sitecore's GetViewRenderer class and initialize our custom ViewRenderer.
public class GetSiteViewRenderer : GetViewRenderer
{
public override void Process(GetRendererArgs args)
{
base.Process(args);
var viewRendering = args.Result as ViewRenderer;
if (viewRendering != null)
{
args.Result = new SiteViewRenderer
{
ViewPath = viewRendering.ViewPath,
Rendering = viewRendering.Rendering
};
}
}
}
Then to get this to actually work, we need to patch in our custom GetSiteViewRenderer into the mvc getRenderer pipleine:
<mvc.getRenderer>
<processor type="XXXFullyQualifiedNamespaceXXX.GetSiteViewRenderer, XXXAssemblyName" patch:instead="processor[@type='Sitecore.Mvc.Pipelines.Response.GetRenderer.GetViewRenderer, Sitecore.Mvc']"/>
</mvc.getRenderer>
Now if all you need to do is override the markup, you can copy the component cshtml to the right path for the site and it should just work.