SharePoint 2013 Razor View WebPart





5.00/5 (3 votes)
A SharePoint WebPart that hosts Razor files (.cshtml, .vbhtml)
Introduction
Razor syntax is a very easy, powerful and less verbose format. This is a proof of concept to utilize this powerful format to be hosted in a SharePoint WebPart!
The idea is very simple; to have a SharePoint WebPart that can host a razor view (.cshtml, .vbhtml) and render its output.
Background
We can compile razor files (.cshtml, .vbhtml) during the runtime using System.Web.Compilation.BuildManager
. This will require adding cshtml and vbhtml build providers to the application web.config file.
<add extension=".cshtml" type="System.Web.WebPages.Razor.RazorBuildProvider,
System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add extension=".vbhtml" type="System.Web.WebPages.Razor.RazorBuildProvider,
System.Web.WebPages.Razor, Version=3.0.0.0,Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
So, now all we need is to use the BuildManager
to compile a razor file and render it in the normal rendering method of the SharePoint WebPart.
Using the Code
The render method of the WebPart will look like the following:
protected override void Render(HtmlTextWriter writer)
{
// Check that there is a file specified
if (string.IsNullOrWhiteSpace(RazorViewVirtualPath))
{
writer.Write(string.Format("Please specify a razor file
to render in the WebPart settings.
<a href=\"javascript:ShowToolPane2Wrapper
('Edit', this,'" + this.ID + "')\">Open tool pane</a>"));
return;
}
Type razorViewType;
try
{
// Compile the razor file into a type (This should be cached to improve the performance)
razorViewType = BuildManager.GetCompiledType(RazorViewVirtualPath);
if (null == razorViewType)
{
throw new Exception("Unable to compile the razor view.");
}
}
catch (Exception e)
{
writer.Write(string.Format("Something went wrong
while compiling the razor view at {0}: {1}", RazorViewVirtualPath, e.Message));
return;
}
// Create an object of this type
var page = Activator.CreateInstance(razorViewType) as WebPageBase;
page.ExecutePageHierarchy(new WebPageContext
(new HttpContextWrapper(this.Context), page, null), writer, page);
base.Render(writer);
}
That's all! for the concept. The compilation of Razor is working now but it gives a compilation error!
It was required to add "System.Web.WebPages
" to the assemblies in the application web.config to overcome these compilation errors during the razor file compilation.
<add assembly="System.Web.WebPages, Version=3.0.0.0,
Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
But adding the "System.Web.WebPages
" assembly automatically adds an HttpModule
to the pipeline which in turn required me to implement a custom VirtualPathProvider
to handle some special cases that was making the default SPVirtualPathProvider
crashing!
So, I wrote this custom VirtualPathProvider
to handle those special cases as follows:
internal sealed class SPRazorViewVirtualPathProvider : VirtualPathProvider
{
public override string CombineVirtualPaths(string basePath, string relativePath)
{
return Previous.CombineVirtualPaths(basePath, relativePath);
}
public override string GetCacheKey(string virtualPath)
{
return Previous.GetCacheKey(virtualPath);
}
public override VirtualDirectory GetDirectory(string virtualDir)
{
return Previous.GetDirectory(virtualDir);
}
public override VirtualFile GetFile(string virtualPath)
{
return Previous.GetFile(virtualPath);
}
public override string GetFileHash
(string virtualPath, System.Collections.IEnumerable virtualPathDependencies)
{
return Previous.GetFileHash(virtualPath, virtualPathDependencies);
}
public override bool FileExists(string virtualPath)
{
// This is a workaround for System.Web.WebPages
// module initialization checking for precompiledapp which will
// not work with SPVirtualPathProvider
if (virtualPath.ToLower().EndsWith("precompiledapp.config"))
return false;
return Previous.FileExists(virtualPath);
}
public override System.Web.Caching.CacheDependency GetCacheDependency
(string virtualPath, System.Collections.IEnumerable virtualPathDependencies, System.DateTime utcStart)
{
if (virtualPath.ToLower().StartsWith("~/_appstart."))
virtualPath = virtualPath.TrimStart('~');
return Previous.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart);
}
public override bool DirectoryExists(string virtualDir)
{
try
{
return Previous.DirectoryExists(virtualDir);
}
catch (Exception e)
{
return false;
}
}
}
I registered this SPRazorViewVirtualPathProvider
to the HostingEnvironment
via a custom HttpModule
:
internal sealed class SPRazorViewHttpModule : IHttpModule
{
private static bool razorViewVirtualPathProviderInitialized = false;
private static object _lock = new object();
public void Init(HttpApplication context)
{
if (razorViewVirtualPathProviderInitialized)
return;
lock (_lock)
{
var razorViewVirtualPathProvider = new SPRazorViewVirtualPathProvider();
HostingEnvironment.RegisterVirtualPathProvider(razorViewVirtualPathProvider);
razorViewVirtualPathProviderInitialized = true;
}
}
public void Dispose()
{
}
}
To add the SPRazorViewHttpModule
to the application pipeline, I used the PreApplicationStartMethod
assembly attribute to register the module via DynamicModuleUtility.RegisterModule
during the application starting.
I added the solution assembly to the web.config assemblies:
<add assembly="SPRazorView, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=64bd6e273698a7b0">
I packaged the whole solution along with all the required web.config modification in a SharePoint Package (WSP) to make things easier.