Create Your Own Custom ViewWebPage for ASP.NET MVC
I prefer to build single page web applications and I also make web performance optimization a 1st class feature in my architecture. This means I need to have the entire application's markup available when the user first loads the site, but at the same time I don't want 'all' the markup delivered to the client each and every time. I can do this because I store each view's markup in the browser's localStorage. I explained the basic mechanism a couple of years ago.
So how do I manage serving fresh markup when I make changes to the site? I use a cookie and store the last time the user made a request to the server for markup. The time is stored as a DateTime tick value (DateTime.Now.ToUniversalTime().Ticks, which is a long. When the user requests markup this value is passed to the server in the cookie. On the server, in my Razor View, I use the last accessed value to compare to the file's last updated time for each view. The code looks like this (path is the view's physical file path, lastRead is the value passed from the cookie):
var fileTime = File.GetLastWriteTimeUtc(path).Ticks;
if (lastRead < fileTime)
{
@RenderPage(path, data);
}
The path variable is the path to the actual razor view's file. The File.GetLastWriteTimeUtc method returns the file's last updated datetime value. I can then compare that value in ticks with the value supplied by the cookie, if the file has changed since the last time the user requested content from the server the newer file is rendered or included in the response. If not nothing is sent to the user. This means I send far less markup to the user on subsequent visits, yet they never miss out on changes to the application.
This is a simple explanation of my technique to manage markup for my Single Page Application framework, but today I wanted to review how I optimized the mechanism using a custom ViewWebPage. When I first crafted this mechanism I did it inside the Razor view, meaning as part of the markup file on the server. When the application is just a few views its not so bad. As the application's size increases you quickly realize this is not a very easy code pattern to maintain.
I decided I needed to refactor my code into the view class itself. I had never customized the Razor View engine or anything in MVC at this level before. At first I thought I could simply write an HtmlHelper extension method. The problem is that would not render the view, I could create a string and if course return it, but that would not work if you wanted to use a Model. I needed to execute RenderPage, a member of the ViewWebPage class.
At this point I needed to inherit from ViewWebPage and add an overload method for RenderPage that would accept the last read time and compare it to the file's last updated time and of course render it if there had been a change. If not then I wanted to render nothing or an empty string. I chose an empty string because I felt it would take far too much to figure out how to render an empty view otherwise.
Looking around the web I found very little help on how to accomplish my goal. I found two blog posts that led me down the right path. First was a Phil Haack post from February 2011 on changing the base type of a Razor View. This showed how to create a custom ViewWebPage that inherits from ViewWebPage and adds an additional HTMLHelper. He shows how to create a custom view and then derive from it to include the view's model. I could not get the second part, where he derived a custom ViewWebPage<T> to work as he showed.
I found an additional post by Scott Allen where he talks about inheriting from ViewWebPage. In his post he only inherits the version where the model is passed, ViewWebPage<T>. I decided to run with Scott's example and found it to work just fine.
The code I came up with looks like this:
namespace SPAHelper
{
public abstract class SPAWebViewPage<T> : WebViewPage<T>
{
public IHtmlString RenderPage(
long lastRead,
string path,
params Object[] data
)
{
var fileTime = File.GetLastWriteTimeUtc(path).Ticks;
if (lastRead < fileTime)
{
return base.RenderPage(path, data);
}
return new SPAHelperResult();
}
public new ViewDataDictionary<T> ViewData
{
get;
private set;
}
}
}
Focusing on the custom RenderPage method I add a lastRead parameter where I can pass in the tick value supplied by the cookie. I waffled between this and simply overwriting the existing RenderPage to read the cookies value internally. I decided to overload the method to preserve the existing method because I may not actually need it as is and I liked having a dedicated version because I could then retrieve the last read value once and reuse it for each view, which should make my page render slightly faster.
The RenderPage method returns an HelperResult, a custom class that implements the IHtmlString interface. I used ILSpy to see how the HelperResult class works and decided it was way to complex to follow its example for what I needed. First if the page needs to be rendered my RenderPage method actually calls the base RenderPage method and returns the result. If it does not need to be rendered I return a custom IHtmlString class I created called SPAHelperResult.
The IHtmlString interface has a single method, ToHtmlString. I just need to return an empty string so that is all this class does. Now when the view does not need to be provided to the end user nothing is added to the MVC rendering pipeline, which means the actual response is much smaller. For this blog that means a difference between about 39kb for an unprimed request and just under 8kb for a primed request with no changes on the server.
public class SPAHelperResult : IHtmlString
{
public string ToHtmlString()
{
return "";
}
}
We are not done yet, the custom ViewWebPage needs to be integrated into the site. This is done by changing the web.config file located in the site's View folder. In this file you will find a <pages/> section In the pages node there is the pageBaseType attribute. This attribute's value needs to be changed to use the custom SPAWebViewPage. Once this is done the Razor View Engine will now use the custom ViewWebPage.
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="SPAHelper.SPAWebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
</namespaces>
</pages>
</system.web.webPages.razor>
To actually use the custom RenderPage method you get the cookie's last read value and pass it along with the view's file name.
long lastLoad = Html.SetUpdateCookie("dt", 1, 360);
@RenderPage(lastLoad, "home-view.cshtml")
@RenderPage(lastLoad, "events-view.cshtml")
@RenderPage(lastLoad, "about-view.cshtml")
I did write an HTMLHelper extension method (SetUpdateCookie) to access the last read value. I will review that in a later post.
Creating a custom ViewWebPage is not that obvious, but it is not that difficult. I hope this helps you create a more customized version of ASP.NET Razor Views. You can download the source code for the SPAHelper library from github.