Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

SharePoint 2013 Razor View WebPart

5.00/5 (3 votes)
6 Mar 2014CPOL2 min read 26.1K   244  
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.

C#
<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:

C#
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.

C++
<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:

C++
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:

C++
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:

C#
<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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)