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.
The razor file can be saved in SharePoint content database, and can be edited from SharePoint designer using Visual Studio or any text editor.
Using the Code
The render method of the WebPart will look like the following:
protected override void Render(HtmlTextWriter writer)
{
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
{
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;
}
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)
{
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.