Precompiled Razor for Shared Views and Mono
SharePoint isn’t the only platform I work with where I can’t quite use ASP.NET MVC 3 out of the box. I also have quite a bit of code on Mono. Mono will probably support MVC 3 at some point, but right now it only almost works - there is only one line of code that doesn’t work, but that line gets called at the start of every request.
Since the problem is in finding and parsing the cshtml files, it should be possible to get around the issue with precompiled views and a custom VirtualPathProvider or ViewEngine. As well as getting around issues with Mono, having views precompiled makes it a lot easier to include views in a shared library. I have shared aspx views working with embedded resources and a custom VirtualPathProvider, but there usually end up being some differences between the handling of embedded and regular views.
As a starting point I used Chris van de Steegs code (Compile your asp.net mvc Razor views into a seperate dll), though what I ended up with was quite different. The current implementation of BuildManager is one of the places where Mono and .NET are not the same (http://go-mono.com/forums/#nabble-td3219000), and there’s a lot of stuff in that path that is more complex than is necessary in this case.
My solution uses a custom ViewEngine, View and ViewPage base class. My first attempt used a virtual path provider to find compiled views using the standard WebViewPage base class, but even if you have the ViewEngine find the compiled view, WebViewPage calls BuildManager anyway to find layouts and check various things that don’t need checking when the view is precompiled.
The ViewEngine is fairly simple - a dictionary maps virtual paths to compiled types and CreateView creates an instance of the appropriate type. The dictionary is populated by finding all types in an assembly that implement ICompiledViewPage and are in a Views namespace.
The IView implementation also doesn’t do much - RenderView populates some context objects and calls ExecutePage.
The custom view base class, CompiledViewPage does the actual page rendering. It is a subclass of WebViewPage, but I ended up not being able to use that much from the base class due to some inconvenient internal/private fields. It is set up to work as a base class for ViewStart pages, layouts, and regular views. The basic flow for rendering a page is:
-
If there is a ViewStart page, call its execute method and copy its context settings
-
Call execute on the main page. This will produce a string for the page body and a dictionary mapping section names to actions that will write the content of the section.
-
Call execute on the layout, which will include calls to RenderSection that run the actions set up by the main page execute method.
There are a few things in the code that can be cleaned up, particularly around the handling of writers and helpers. So far it’s working rather than really good code, but it works for everything I’ve tried it on (both .NET and Mono) and I’ll probably improve on it as it gets more real world use.
The other part of the system is generating the code files that will be compiled. I do this with a console app, GenerateViewsWithMvc, that is set up as an external tool in Visual Studio to generate code for all cshtml files in a project. It’s very similar to the one I use for Razor views in SharePoint - the main difference is that it uses WebPageRazorHost rather than RazorEngineHost so that it can use sections. It also uses a dynamic type parameter rather than an untyped base class for untyped views because I can’t have the base type extend both the typed and untyped versions of WebPageView without duplicating all the code.
The external tool settings are:
-
Command: GenerateViewCodeWithMvc.exe
-
Arguments (base class for views): CompiledViews.CompiledViewPage
-
Initial Directory: $(ProjectDir)
Using the compiled views in a web app requires a few lines in Application_Start:
ViewEngines.Engines.Clear();
var engine = new CompiledRazorViewEngine();
CompiledRazorViewEngine.RegisterViewAssembly(typeof(CompiledRazorViewEngine).Assembly);
CompiledRazorViewEngine.RegisterViewAssembly(typeof(CompiledViews.Mvc3Test.Views._ViewStart).Assembly);
ViewEngines.Engines.Add(engine);
The first RegisterViewAssembly call adds views from a shared library and the second adds the views in the current assembly. If the same view exists in both assemblies, the last one added will be used. This code removes all other view engines - it should be possible to use it with other view engines, though there will obviously be some conflicts if the other view engine also looks for cshtml files in the Views folder.