This past weekend while working on my talk for the Heartland Developer's Conference, I toyed with the idea of showing multiple ASP.NET MVC View Engines (VE) ‘co-existing’ within the same application. Why do that? Well, I wanted to show how using open source tools like MVC Turbine, MVC Contrib and Spark within your application, you can assemble some pretty cool stuff. And most important, it made for a really cool demo. :)
Please note that this concept is not anything new. Phil has blogged about a similar topic in the past. However, this approach extends what Phil covered in his post along with the execution of view by a VE regardless of a controller. In other words, the view is just processed through the list of VEs (default behavior of MVC). If a VE can’t process the view, it ‘punts’ to the next one.
To get it all to work for the demo, I did the following:
- Auto-registration of IViewEngine via MVC Turbine.
- In-lined the registered IViewEngine instances to “trickle down” the resolution/execution of views if one is not found
- Made the WebForms VE last since that’s the one that comes out of the box.
- Provided views via Inferred Actions from MVC Turbine.
- Added minor fixes to the NVelocity VE from MVC Contrib.
Feel free to check out the sample code from Google code. Also, if you have any questions regarding the process, please add a comment below.
Let’s go into the details… :)
Auto-registration of IViewEngine via MVC Turbine
One of the nice features of MVC Turbine is the auto-registration of components. You tell Turbine which types you want to auto register and it goes off and does it. However, in v1, the auto-registration of IViewEngine is not supported (this will ship out in v2 along with tons of enhancements) so you’ll have to manually register the auto-registration. This is done by overridering the AutoComponentSetup method and adding a auto-registration delegate:
1: protected override void AutoComponentSetup(IServiceLocator locator)
2: {
3: // Don't forget to call the base to auto-register the other pieces
4: base.AutoComponentSetup(locator);
5:
6: // Add the registration of the IViewEngine types
7: AutoRegistration<IViewEngine>((loc, type) => loc.Register<IViewEngine>(type));
8: }
Once this is one, you will need to tell MVC about the auto-registered IViewEngine instances. This can be done via the ProcessComponentRegistration method; however, in v2, I’ve added an specific view engine registration method so this approach will be deprecated in v2. The following code gets the IViewEngine instances and ‘in-lines’ them for processing:
1: protected override void ProcessComponentRegistration(IServiceLocator locator)
2: {
3: // Don't forget to call the base to process all the other component registrations
4: base.ProcessComponentRegistration(locator);
5:
6: // Get the IViewEngine instances from the container
7: var engines = locator.ResolveServices<IViewEngine>();
8: if (engines == null || engines.Count == 0) return;
9:
10: // Clear all view engines since we want start from scratch
11: ViewEngines.Engines.Clear();
12:
13: foreach (var engine in engines)
14: {
15: ViewEngines.Engines.Add(engine);
16: }
17:
18: // Make the WebForm VE the last engine in the 'process' list
19: ViewEngines.Engines.Add(new WebFormViewEngine());
20: }
From here, you can now run your application and any assembly that’s referenced (or is in the bin folder) that contains a type that implements IViewEngine will be automatically registered to the container and wired up to MVC.
Now, let’s see how we can take advantage of Inferred Actions that ship with Turbine to simplify action execution.
Inferred Actions from MVC Turbine
Since this post is all about the view, let’s have inferred actions do the dirty work of handling action execution for us! If you look at all the controllers (with the exception of the NVelocityController) defined within the sample, you’ll see that they infer all of their actions:
1: public class SparkController : Controller
2: {
3: }
4:
5: public class MixedController : Controller
6: {
7: }
8:
9: public class HomeController : Controller
10: {
11: }
12:
13: public class NVelocityController : Controller
14: {
15: public ActionResult Index()
16: {
17: // Had to specify the Master page for the view
18: // since there were some issues with the NVelocity VE.
19: return View("Index", "Site");
20: }
21: }
With inferred actions, we move all the ‘action’ to the view since the controller just become a place holder. As you can see, the Views folder contains all the ‘magic’:
As you can see from the image, the Shared folder contained most of the ‘diversity’ in files. The files are broken this way:
- Application.spark -- ‘master page’ for the Spark views, can provide common pieces for all views.
- site.vm -- ‘master page’ for the NVelocity views.
- Site.Master – Good ol’ ASP.NET Master pages.
Each of the view subfolders contain the specific view for their ‘corresponding’ VEs. However, the Mixed folder contains both an WebForm view an well as a NVelocity view. This is due to the fact that the NVelocity VE doesn’t look inside the Shared view folder for views. So in order to make this work:
1: <%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage" %>
2:
3: <asp:Content ID="indexTitle" ContentPlaceHolderID="TitleContent" runat="server">
4: Mixed Controller Page
5: </asp:Content>
6: <asp:Content ID="indexContent" ContentPlaceHolderID="MainContent" runat="server">
7: <h2>
8: I'm a mixed controller and I'm using the WebForms ViewEngine!</h2>
9: <% 1: Html.RenderPartial("sparkpartial");
%>
10: <% 1: Html.RenderPartial("nvelocity");
%>
11: </asp:Content>
The NVelocity view had to placed within the Mixed folder. The correct way to fix this was to add Shared folder support for the VE, but I was too lazy and wanted to get the blog post out. :)
Now let’s examine the changes made to the NVelocity VE to make it play nice with the other engines.
Tweaking the NVelocity View Engine
The NVelocity VE ships with the MVC Contrib project and does a great job on providing NVelocity support to ASP.NET MVC. However, when you try to use with other VEs in the same project, you’ll need to change a few things.
View Lookup
Out of the box, the NVelocity VE will throw an IOE if a view is not found. This puts a stop on the ‘punting’ needed for the multiple view engines to work. So within the ResolveViewTemplate method, you’ll need to return null when a view is not found:
1: private Template ResolveViewTemplate(string controllerFolder, string viewName)
2: {
3: string targetView = Path.Combine(controllerFolder, viewName);
4:
5: if (!Path.HasExtension(targetView))
6: {
7: targetView += ".vm";
8: }
9:
10: if (!_engine.TemplateExists(targetView))
11: {
12: return null;
13: // Removed to allow the punting of an un-resolved view
14: //throw new InvalidOperationException("Could not find view " + viewName +
15: // ". I searched for '" + targetView + "' file. Maybe the file doesn't exist?");
16: }
17:
18: return _engine.GetTemplate(targetView);
19: }
Now, you’ll need to change the FindView method to handle the new return value from ResolveViewTemplate:
1: public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
2: {
3: var controllerName = (string)controllerContext.RouteData.Values["controller"];
4: string controllerFolder = controllerName;
5:
6: Template viewTemplate = ResolveViewTemplate(controllerFolder, viewName);
7: Template masterTemplate = ResolveMasterTemplate(masterName);
8:
9: // If the view was not found, you'll need to tell ASP.NET MVC to go look elsewhere.
10: if (viewTemplate == null) return new ViewEngineResult(new[] { controllerFolder });
11:
12: var view = new NVelocityView(viewTemplate, masterTemplate);
13: return new ViewEngineResult(view, this);
14: }
From here, if a view is not resolved, the NVelocity VE will just pass the request back to MVC for handling.
Master Lookup
This change is exactly like the View Lookup issue, however, it deals with ‘Master’ views. Since this was the first ‘hiccup’ I fixed with the VE, I implemented a fix that will search under the Shared folder as well as the default Masters folder. I did this to keep things common between all VEs and make the demo easier to follow. The change is below:
1: private Template ResolveMasterTemplate(string masterName)
2: {
3: Template masterTemplate = null;
4:
5: if (!string.IsNullOrEmpty(masterName))
6: {
7: string targetMaster = GetMasterPath(masterName);
8: masterTemplate = _engine.GetTemplate(targetMaster);
9: }
10:
11: return masterTemplate;
12: }
13:
14: private string GetMasterPath(string masterName)
15: {
16: if (!Path.HasExtension(masterName))
17: {
18: masterName += ".vm";
19: }
20:
21: // Iterate through the list of folders to search for masters
22: var pathList = new[] { _masterFolder, "shared" };
23:
24: foreach (var masterPath in pathList)
25: {
26: var path = Path.Combine(masterPath, masterName);
27: if (!_engine.TemplateExists(path)) continue;
28:
29: return path;
30: }
31:
32: return string.Empty;
33: }
By adding the GetMasterPath method and pushing the master resolution there, it made the logic a lot simpler to read and hopefully extend!
Wrap Up
I know, it can be scary when I let my mind wonder with “what if…” thoughts. :) I hope this post shows the flexibility you can gain with MVC Turbine and a little elbow grease. :
Again, feel free to look through the code and use as you feel like. If you have any questions, please add a comment below!
Happy coding!