What's new in MVC 6: View Location Expanders
ASP.NET 5, along with MVC 6, comes with a bunch of new stuff. One of the more interesting ones is without a doubt the View Location Expanders. They basically make it easy to completely customize the location in which the View Engine looks for a view.
Where is this useful? Well, one such example is for all those people (like me) who really hate the fact that Shared folder is alphabetically somewhere in the middle of all the views in the directory structure. If you want to rename it, to for example, _Shared, it used to require a significant investment in modifying the existing view engine. So, when the team rewrote the engine for MVC 6, this was one of the issues/suggestions that was raised.
Where else is this useful?
There are two scenarios that came to my mind when I saw this.
- Let’s say you’re writing a multi-tenant application – not so uncommon, these days, and with the cloud. You want to be able to allow tenants to customize the view (to some extent). Thus far, you had to implement some fairly complex theming engine (or roll your own), to do this. This is a bit more complex, but it’s doable.
- Your application can be multi-pricing-tier. You have certain features that you do not support on some tiers. You can now render a different view depending on the pricing tier of the current customer/tenant.
- As you build your application you may want to test different approaches to certain things. This is usually done using A/B testing. In A/B testing you would show a fixed percentage of people the A variant, and the others a B version. You may even have a third option, etc., depending on how complex you want to make this. The View Location Expander would allow you to do that.
- Localization/Customization – our team is currently building software that runs in different locales. The application serves several different markets that have (1) different languages (resources), but more importantly, (2) different legislative requirements. The first one is really easy to solve (or fairly easy), the second one though, is much more complicated. It sometimes requires capturing different fields for the same action. The location expanders give us an option to do this much more efficiently (e.g. we can have default views, like cshtml, and then separate views for different localizations, like Invoice.hr.cshtml).
How does it work?
The foundation of everything (aside from the new Dependency Injection framework in ASP.NET 5, which we’ll cover in a later post), are proper interfaces. In this case, we need to derive a class from IViewLocationExpander .
This interface has the following methods:
IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations);
void PopulateValues(ViewLocationExpanderContext context);
Understanding how either one of them works, is a bit of a challenge due to lack of documentation at the moment, but, since the entire framework is open source, you can look into the source itself, or run through with the debugger - which, IMHO, is a lost art these days. I am still surprised to see how many people are unable to use any of the debugger features, let alone the more advanced ones.
PopulateValues
This one is a bit of a strange one, but it makes sense in the long-run. From my understanding, the main reason for it is caching. This method basically gets called on every request, and gives you the chance to add context to the request. If the request (with context) is the same, the ExpandViewLocations method is not even called (anymore, or rather, is cached). For example, if we’re serving different views based on the current principal, we could add the user ID to the context:
context.Values.Add("UserId", context.ActionContext.HttpContext.User.Identity.Name);
Now, the framework will call the main method. It will pass in the context (the one where we populate in the PopulateValues), and an IEnumerable of strings, called viewLocations. The view locations originally contains the following:
We can now deduce what the framework does with the strings. It calls them with two parameters, the name of the Action (0) and the name of the Controller (1).
So, let’s build an easy example. Let’s say we want to show a completely different view if there’s something in the query string. There are many ways you can organize your views, but one example is to have a folder (e.g. for localization). In our case, that’s how I want to try it. So I decided that our special view will go into a Special folder.
To support the MVC now finding our view, we need to tell it to look into that folder. So, we do this:
if (descriptor.ControllerName == "Home" && context.ActionContext.ActionDescriptor.Name == "Contact"
&& context.ActionContext.HttpContext.Request.Query.ContainsKey("apple"))
{
return viewLocations.Select(x => x.Replace("{0}", "Special/{0}"));
}
This is for our simple example. We basically want to change the location of one single view, i.e. the Contact page. If there’s the query string “apple” present, we’ll show the special page.
This is the custom view location expander in its entirety:
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Razor;
using System.Collections.Generic;
using System.Linq;
namespace Demo06
{
public class CupertinoViewLocationExpander : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
// we don't want to change layout pages & partials ...
if (context.IsPartial) return viewLocations;
var descriptor = (context.ActionContext.ActionDescriptor as ControllerActionDescriptor);
if (descriptor == null) { return viewLocations; }
if (descriptor.ControllerName == "Home" && context.ActionContext.ActionDescriptor.Name == "Contact"
&& context.ActionContext.HttpContext.Request.Query.ContainsKey("apple"))
{
return viewLocations.Select(x => x.Replace("{0}", "Special/{0}"));
}
return viewLocations;
}
public void PopulateValues(ViewLocationExpanderContext context)
{
// we add this to enable caching!
var contains = context.ActionContext.HttpContext.Request.Query.ContainsKey("apple");
context.Values.Add("CupertinoKey", contains.ToString());
}
}
}
Note: it’s very rudimentary. The main part then, is the viewLocations.Select(x => x.Replace("{0}", "Special/{0}")) . It’s a real simple way, and it can be done a lot better. But it’s a good proof of concept.
So, if we open the page now, we'll see two different views, depending on our query string:
-
Without "Apple" in the query string
-
With "apple" in the query string