Localising MVC Views using Display Modes

When considering localisation/globalisation/internationalisation of an MVC website there are a lot of routes you can take. The easiest of these (and therefore the most tempting) is to use a Resources.resx file to handle string replacement, but there are scenarios where you need to do more than just replace some text.

You might need to rearrange the page to handle right-to-left languages, you might need to change a background image that is particularly offensive in some countries; either way, you’re looking at writing a specific view for a specific culture.

Custom Per-Culture Views

While googling for a solution I found quite a few examples (such as this blog post from Brian Reiter) that work something like this:

  1. Create a folder structure like the one below for your culture-specific views
  2. Subclass WebFormViewEngine or RazorViewEngine to transform the view path to the new folder structure

In the example above, any visitor with a culture of en-GB will see /Views/i18n/en-GB/Home/Index.cshtml; anyone else will see the default view.

In many ways this is a very elegant solution: it doesn’t require you to create localised versions of every view; it elegantly falls back to sensible defaults; and you don’t have to write a lot of custom code. Unfortunately it does have a couple of problems:

  • It requires that you subclass a specific -ViewEngine implementation
  • It doesn’t support localised versions of master pages
  • It doesn’t work with the MVC mobile features introduced in MVC4

The first is not necessarily a show-stopper, but the mobile support issue is a pretty big deal for my scenario and the lack of master page localisation could cause problems.

“Borrowing” from Mobile Features

Seeing as the mobile features were the main driver behind my need to change, it makes sense to use the same mechanism for path-transformation that they use: display modes.

Display Modes (Simple Version)

Display modes allow you to set up special suffixes (suffices?) such as “Mobile” that, under certain circumstances, ASP.NET will look for before looking for the standard .cshtml. For example, the following would set up an iPhone suffix that would be considered whenever the user agent of a request includes the string “iPhone”:

DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode("iPhone")
{
    ContextCondition = (context => context.Request.UserAgent
        .IndexOf("iPhone", StringComparison.OrdinalIgnoreCase) >= 0)
});

This means that when a user browses to your site using an iPhone, ASP.NET will try to find a /Views/Controller/Index.iPhone.cshtml before falling back to /Views/Controller/Index.cshtml.

Display Modes (Slightly-More-Detailed Version)

So how does the example above actually work?

The DefaultDisplayMode class in the example is part of MVC and is responsible for the transformation of a virtual path and for checking if it exists. By adding it to the DisplayModeProvider.Instance.Modes collection at index zero, we are telling ASP.NET that it should check this display mode before any others.

To determine which mode to use, each IDisplayMode instance that is added to the DisplayModeProvider.Instance.Modes collection will have the GetDisplayInfo method called in the order that they are added.

public DisplayInfo GetDisplayInfo(HttpContextBase httpContext, string virtualPath, Func<string, bool> virtualPathExists)
{
	//...
}

If the instance is able to return a valid view path then it does so by returning an instance of DisplayInfo; if not, it returns null and the next element in the Modes collection is queried.

So how can we use this to handle localisation?

Handling Localisation with Display Modes

The first step is to subclass DefaultDisplayMode so we have somewhere to implement our localisation path transformation. We could implement IDisplayMode directly, but we get some things for free by using the existing implementation:

public class LocalisedDisplayMode : DefaultDisplayMode
{
	private string _suffix;

	public LocalisedDisplayMode(string suffix)
		: base(suffix)
	{
		_suffix = suffix;
	}
}

All we are doing here is accepting a suffix in the constructor that we pass to the base class as well as keeping a reference for ourselves – we’ll need it later.

Next up we want to override the implementation of GetDisplayInfo so that we can transform the path to the localised equivalent.

public override DisplayInfo GetDisplayInfo(
	HttpContextBase httpContext,
	string virtualPath,
	Func<string, bool> virtualPathExists)
{
    //...	
}

The 3 parameters are:

  • The context of the current request
  • The current virtual path
  • An anonymous helper function to determine whether or not a given virtual path exists

Our implementation needs to:

  1. Get the appropriate culture for the user
  2. Check to see if a custom view exists for the specific culture (e.g. en-GB)
  3. If not, check to see if a custom view exists for the 2-character culture (e.g. en)
  4. If not, return null to indicate that this display mode cannot handle the request

Acquiring User Culture

We can get an appropriate culture for the user through a number of mechanisms – Accept-Language headers/Request.UserLanguages, storing a cookie, using CultureInfo.CurrentUICulture etc. – and how this is sourced doesn’t really matter for this example. For now, I’m going to leave it as an abstract method:

protected abstract CultureInfo GetCulture();

Selecting a View Path

Let’s assume that a request for ~/Views/Home/Index.cshtml is made from a user with a culture of es-ES, and our display mode has been set up with a suffix of “suffix”. We want to check the following paths (in order) to see if they exist:

  • ~/Views/i18n/es-ES/Home/Index.suffix.cshtml
  • ~/Views/i18n/es/Home/Index.suffix.cshtml

We’re not worried about looking in /Shared or in checking the non-localised paths – those will be covered by default display modes.

public override DisplayInfo GetDisplayInfo(
	HttpContextBase httpContext,
	string virtualPath,
	Func<string, bool> virtualPathExists)
{
	var culture = this.GetCulture();

	return
		TryGetDisplayInfo(virtualPath, culture.Name, virtualPathExists) ??
		TryGetDisplayInfo(virtualPath, culture.TwoLetterISOLanguageName, virtualPathExists);
}

private DisplayInfo TryGetDisplayInfo(string virtualPath, string cultureString, Func<string,bool> virtualPathExists)
{
	//use the base class to handle suffix transformation
	var transformedPath = this.TransformPath(virtualPath, _suffix);
		
	//use a regular expression to replace the view folder
	var cultureSpecificPath = Regex.Replace(
				transformedPath,
				"^~/Views/",
				string.Format("~/Views/i18n/{0}/", cultureString));

	//return the virtual path if it exists...
	if (virtualPathExists(cultureSpecificPath))
		return new DisplayInfo(cultureSpecificPath, this);

	//...or null if it doesn't
	return null;
}

Here we have implemented a helper method that will transform and check a path, then return a DisplayInfo instance if it exists. The null-coalescing operator in GetDisplayInfo will fallback to each less-specific view path in turn.

Plugging it in

The final step is to insert the new display mode into the configuration. We do this in the Global.asax file as below:

protected void Application_Start()
{
	DisplayModeProvider.Instance.Modes.Insert(0, new LocalisedDisplayMode(DisplayModeProvider.DefaultDisplayModeId));
	DisplayModeProvider.Instance.Modes.Insert(0, new LocalisedDisplayMode(DisplayModeProvider.MobileDisplayModeId)
		{ ContextCondition = context => context.GetOverriddenBrowser().IsMobileDevice });

	//...
}

We are inserting 2 instances so we can mimic the behaviour of the 2 default display modes (Default and Mobile), and we are inserting them at the top of the list so that they take precedence over the defaults.

Now we can create localised versions of master pages, views, partial views, mobile views…pretty much everything!

As an added bonus, this implementation is reusable between both Razor and Web Forms, and is much easier to unit test than the subclassed-view-engine approach.

Advertisements

ProxyApi: Now With Intellisense!

After announcing ProxyApi in my last post I had a few people suggest that it would be more useful if it included some kind of intellisense.

So…now it does! Install the new ProxyApi.Intellisense NuGet package and you will automatically have intellisense for the generated JavaScript API objects.

I’ve made this into a separate package for 2 reasons:

  1. The original ProxyApi package still works perfectly on it’s own; and
  2. The intellisense implementation is a little but more intrusive than I would have liked

It works by adding a T4 template to the Scripts directory of your project that uses the ProxyApi classes to generate a temporary version of the script at design-time. That scripts is then added to _references.js so it gets referenced for any JavaScript file in the solution.

This would be fine, but unfortunately Visual Studio doesn’t have any mechanism for regenerating the T4 template automatically, meaning that changes to the API or MVC controllers wouldn’t be reflected until you either manually rebuilt the templates or restarted VS. For the time being I have worked around this used a simple Powershell script to re-evaluate all T4 templates after each build, but hopefully I can find a more elegant solution later.

Because this does add a slight performance penalty, and because not everyone would need intellisense support, I’ve left this as an extra package. If you prefer the vanilla ProxyApi then you can grab it here.

The next step will be generating TypeScript files using a similar mechanism, which would allow the intellisense to extend to the parameter types as well.

Watch this space…

ProxyApi: Automatic JavaScript Proxies for WebAPI and MVC

Taking Inspiration from SignalR

One of my favourite features of SignalR is the the automatic generation of JavaScript proxies for hub methods. By adding a hub class in C#…

//server
public class ExampleHub : Hub
{
	public void SendMessage(string message)
	{
		//do something with message
	}
}

…you can get the JavaScript wrapper for that method just by adding a reference to /signalr/hubs:

//client
$(function () {
    var hub = $.connection.exampleHub;

    $.connection.hub.start(function() {
        hub.sendMessage("a message from the client");
    });
});

This is such a useful feature that it has even been suggested as an alternative to Web API controllers.

But why should we use hubs as a replacement for MVC or Web API controllers? Why can’t we instead write similar functionality to work with controllers?

Introducting ProxyApi

ProxyApi is a small NuGet package that automatically generates JavaScript proxy objects for your MVC and Web API controller methods.

PM> Install-Package ProxyApi

(as an aside, this was my first attempt at creating a NuGet package and it was embarrassingly simple).

Once you’ve installed the NuGet package you just need to add a link to ~/api/proxies and the JavaScript API classes will be automatically created.

So what do these actually give you?

API Controllers

Let’s say you have started a new MVC4 project and you add a new “API controller with empty read/write actions”:

public class DataController : ApiController
{
    // GET api/data
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }

    // GET api/data/5
    public string Get(int id)
    {
        return "value";
    }

    // POST api/data
    public void Post([FromBody]string value)
    {
    }

    // PUT api/data/5
    public void Put(int id, [FromBody]string value)
    {
    }

    // DELETE api/data/5
    public void Delete(int id)
    {
    }
}

Ordinarily if you wanted to call these methods from JavaScript you would need to write something like the example below using jQuery:

$.post("/Data/123", function(data) {
    //do something with the result
});

This is actually pretty concise, but I have 2 problems with this approach:

  1. The code calling this method knows that it is making a POST call. What do I do when I want to switch my data source to local storage, or some other data accessor?
  2. The code knows about the URL.

Instead, what I would prefer is a JavaScript proxy object on which I can call a method – passing in appropriate parameters – without ever knowing where that method gets it’s data or how it does so. And this is what ProxyApi provides.

Add a new script tag to _Layout.cshtml to ~/api/proxies (after the jQuery reference) and you can start directly calling API methods without writing another line of code yourself:


$.proxies.data.get().done(function(allItems) {
  //allItems will contain ['value1', 'value2']
});

$.proxies.data.get(123).done(function(item) {
  //item will be 'value'
});

//will send 'value' to Post method on controller
$.proxies.data.post("value");

//will send id=1, value='value' to Put method on controller
$.proxies.data.put(123, "value");

//will send 123 to Delete method on controller
$.proxies.data.put(123, "value");

These proxy objects can now be passed to any code that needs to perform data access without ever exposing how that data is sourced. You can easily mock them for unit testing, replace them if needed, and call them without needing to know where the website is hosted.

Complex Types

This is all well and good for simple data types like the strings in the example above, but what about when you want to manipulate complex types?

public class Person
{
	public int Id { get; set; }
	public string FirstName { get; set; }
	public string LastName { get; set; }
}

public class DataController : ApiController
{
    [HttpPost]
    public void UpdatePerson(Person value)
    {
    }
}

In this case, just pass a JSON object to the generated method:

$.proxies.data.updatePerson({
    Id: 123,
    FirstName: 'Steve',
    LastName: 'Greatrex'
});

This will send the JSON object as POST data to the Post method on the controller.

And when you have both URL and body data, such as in the auto-generated Put method? Just decorate the parameters with [FromBody] or [FromUri] and the rest will be taken care of:

public void Put([FromUri]int id, [FromBody]Person value)
{
}

Note: it generally isn’t necessary to use [FromUri] as ProxyApi will assume that anything is a URL parameter unless told otherwise. The only exception to this is for POST methods that take a single parameter, which will be assumed to be POSTed.

Non-Conventional Method Names

All the examples so far have been using conventionally-named methods, but there is no requirement for this: any method name will work:

public class DataController : ApiController
{
	public void DoSomething(int id)
	{
		// --> $.proxies.data.dosomething(123) (GET)
	}

	[HttpPost]
	public void DoSomethingElse(Person person)
	{
		// --> $.proxies.data.dosomethingelse({ ... }) (POST)
	}
}

Appropriate HTTP verbs will be used for any method based on the following rules (in priority order):

  • [Http*] attribute (e.g. [HttpPost], [HttpGet] etc.)
  • [AcceptVerbs(...)] attribute
  • Method naming conventions, e.g. DeletePerson() == DELETE
  • GET for everything else

You can also specify custom names for both the proxy objects and methods using the [ProxyName] attribute.

[ProxyName("custom")]
public class DataController : ApiController
{
	[ProxyName("method")]
	public void DoSomething(int id)
	{
		// --> $.proxies.custom.method()
	}
}

Excluding and Including Elements

By default, ProxyApi will automatically include every method in every type in the current AppDomain that inherits from either System.Web.Mvc.Controller or System.Web.Http.ApiController. You can change this behaviour to exclude everything by default by changing the ProxyGeneratorConfiguration.Default.InclusionRule property:

ProxyGeneratorConfiguration.Default.InclusionRule = InclusionRule.ExcludeAll;

You can also explicitly include or exclude any element by decorating it with one of the [ProxyInclude] or [ProxyExclude] attributes:


[ProxyExclude] //excludes entire controller
public class ExcludedController : ApiController
{}

[ProxyInclude] //includes entire controller and all methods...
public class IncludedController : ApiController
{
    [ProxyExclude] //...except for explicitly excluded methods
    public void ExcludedMethod() {}
}

public class DefaultController : ApiController
{
    [ProxyExclude] //will always be excluded
    public void ExcludedMethod() {}

    [ProxyInclude] //will always be included
    public void IncludedMethod() {}

    //will be included or excluded based on the globally configured default
    public void DefaultMethod() {}
}

Returning Data & Handling Errors

The generated proxy methods all return an instance of the jQuery $.Deferred object, so you can use the done, fail and complete methods to handle the results from the controller acctions:

$.proxies.person.getAllPeople()
    .done(function(people) {
        //people will contain return value of PersonController.GetAllPeople(), if it succeeds
    })
    .fail(function(err) {
        //this will be called if the controller throws an exception
        //err contains exception details
    })
    .complete(function() {
        //this will be called after success or failure
    });

You can get more information on how to use the jQuery Deferred object from the documentation

MVC Controllers

The examples above are all based around Web API, but everything will work with MVC controllers as well:

$.proxies.home.index().done(function(content) {
    //content will contain HTML from /Home/Index
});

Source Code

I’ve put the source code (including unit tests) on GitHub so feel free to take a look around and make any changes you think are useful.

This is an early version and will probably change quite quickly, so keep an eye out for new developments. If you have any feature suggestions then leave them in the comments (or fork and write them yourself)!

Ingenious Solution or Horrible Hack?

Today I found myself writing something that feels…hacky. Very hacky, actually. But it does work, and that’s more than can be said for any of the “clean” solutions I could come up with.

Routing Problems

This all came about because I am working on a project that relies on a couple of specific routes being present in the MVC routing tables. This project is intended to be added to other MVC websites as a bolt-on dependency so it can’t rely on any other routing, and I wanted to be able to access arbitrary controllers using {controller}/{action}.

As this is a bolt-on, I don’t want to affect any of the existing routes so I thought I would add a prefix to my routes so that existing routes would work as normal.

RouteTable.Routes.MapRoute(
	name: "DefaultProxy",
	url: "proxy/{controller}/{action}"
);

This way, you could use either http://server/Home/Index or http://server/proxy/Home/Index and get the same result…provided that the proxy/... route is registered before any of the existing route definitions.

Problem solved, right?

More Specific Routing Problems

At first the approach above seemed to be working, but then I noticed the URLs generated by Url.Action and Html.ActionLink

<a href="http://server/proxy/Home/Index">Home</a>

Even worse, the proxy segment (obviously) shows up in the browser toolbar:

address bar

That doesn’t really qualify as “not affecting existing routes”…

Sadly, MVC is actually behaving exactly as it should: using the first and most specific route it can find to generate the URL. The fact that I don’t want it to use that route is irrelevant.

Surely There’s a Better Way?

After making this discovery and bashing my head against various convoluted solutions I finally came up with something that works. But it feels so very wrong…

RouteTable.Routes.MapRoute(
	name: "DefaultProxy",
	url: "{proxy}/{controller}/{action}",
	defaults: new {},
	constraints: new { proxy = "^proxy$" }
);

So what is this oddity? First, I’m specifying a new URL segment – “proxy” – that prefaces the familiar {controller}/{action}. Then I’m setting up a constraint on that segment that will only allow the value “proxy” to appear using a regular expression.

This has the same overall effect that my original solution had, with the added bonus that (presumably because of the unrecognised URL segment) the URL-generation mechanisms ignore the new route.

This can’t be the best way to achieve this – there has to be a better solution that I just haven’t found yet.

Please – someone enlighten me in the comments!

Progressive, In-Place Menu Navigation with jQuery & Knockout

Progressive navigation is a pretty common theme throughout the web – we see hierarchical tree menus on half the sites we visit – but a menu structure doesn’t always fit.

Sometimes it is preferable to be able to guide a user through a series of progressively more specific options one step at a time – almost like a small-scale wizard.

An example of a progressive menu

This way the user still gets the ability to dig through menus, but they only see one step at a time and we don’t use up a bunch of space on our UI.

I have recently written something along these lines for a project so I thought I’d add it to my utils library. You can see an example of the final version running here.

Defining the Structure

To define the hierarchy of links, I decided to use a data- attribute to denote groups of links:

<a href="#" data-nav-group="group1">Link in group 1</a>
<a href="#" data-nav-group="group1">Another link in group 1</a>

<a href="#" data-nav-group="group2">Link in group 2</a>
<a href="#" data-nav-group="group2">Another link in group 2</a>

To define the navigation between groups I used another data attribute to specify the target for each link. If no target is specified then the link will be ignored (though any other click handler or href attribute would be treated normally):

<a href="#" 
   data-nav-group="group1"
   data-nav-target="group2">Link to group 2</a>
<a href="#"
   data-nav-group="group1">Link to Nowhere</a>

This non-hierarchical method means that links can go from any group to any other group as required.

Plugin Implementation

The plugin needs to do 3 things:

  1. Handle click events to select a specific group
  2. Hide elements that aren’t in the selected group
  3. Fade in elements that are in the selected group

Let’s look at these one at a time.

Handling Click Events

Whenever a user clicks on an element with a data-nav-target attribute, we want to select the group specified by that target: nice and simple with the jQuery ‘on’ method:

$.fn.progressiveNav = function () {
	var $this = $(this),
		showGroup = function (groupName) {
			//to be implemented
		};
		
	$this
		.off("click.nav")
		.on("click.nav", "[data-nav-target]", function () {
			showGroup($(this).attr("data-nav-target"));
		});
};

Here we firstly use off with a namespaced click event to remove any existing nav handlers, and then use on to attach to any element that has the required attribute.
When the click event is invoked, we call a showGroup method (which we’ll implement in a minute) with the value of the attribute from the clicked element.

Showing a Group

To show a group, we want to hide anything that doesn’t match the current group, and fade-in anything that does match. We can achieve this easily enough with a combination of attribute selectors and the hide and fadeIn methods:

var showGroup = function (groupName) {
	$this.find("[data-nav-group!=" + groupName + "]").hide();
	$this.find("[data-nav-group=" + groupName + "]").fadeIn();
};

This will immediately hide anything outside of the specified group, and will fade in the new group without causing the UI to jump about.

Showing the Initial Group

When we first invoke the progressiveNav function, we want to display either a parameter-specified group, or the first group found.

Again, nothing too complicated:

$.fn.progressiveNav = function (startGroup) {

	//...hook up click event as before...

	//grab the first group if none was specified
	if (!startGroup) {
		startGroup = $this.find("[data-nav-group]")
			.first()
			.attr("data-nav-group");
	}

	//show the first group - either passed in or the first one found
	showGroup(startGroup);
};

If the user has specified a group name then we just pass that to our showGroup method. If not, we use the same attribute selectors as earlier to grab the first group attribute we find.

You can see a working example of everything so far in this jsFiddle.

Extending for Knockout

If we want to be able to set and retrieve the current group using Knockout then we need to do a little bit more work. Specifically, we’ll need to create a custom binding so that we can specify our navigation container as below:

<!-- HTML -->
<div data-bind="progressiveNav: level">
    <!-- grouped links as before -->
</div>
//JavaScript
var viewModel = {
	level: ko.observable()
};

ko.applyBindings(viewModel);

Setting the Group from Knockout

To allow changes in the view model to be reflected in the navigation, we can use the update method of the custom binding to call the progressiveNav plugin method we created earlier:

ko.bindingHandlers.progressiveNav = {
	update: function (element, valueAccessor) {
		$(element).progressiveNav(ko.utils.unwrapObservable(valueAccessor()));
	}
};

Updating the view model property from click event handlers, on the other hand, is slightly harder…

Updating View Model from Click Handler

The issue here is that, at this point, there is no way for the custom binding to know that someone has clicked on one of the navigation links, so there is no way for it to update the view model.

We can remedy this by adding a custom jQuery event to the showGroup method from earlier that will notify subscribers whenever the selected group is changed:

var showGroup = function (groupName) {
	//show & hide groups as before

	//raise an event on the element to notify that it was updated
	$this.trigger("navchanged", groupName);
};

We can now subscribe to that event in the init function on the custom binding to update the source observable:

ko.bindingHandlers.progressiveNav = {
	init: function (element, valueAccessor) {
		var setter = valueAccessor();

		$(element).on("navchanged", function (e, groupName) {
			setter(groupName);
		});
	}
	update: function (element, valueAccessor) { /* as before */ }
};

Now we can update the view model property from the view, and update the view from the view model. You can see all this working
here.

The source code for the combined jQuery & Knockout versions can be found
on GitHub.

Enjoy!