Extending the ASP.NET MVC ViewEngine to support localization

I’ve been using with Scott Hanselman’s CustomMobileViewEngine from his post A Better ASP.NET MVC Mobile Device Capabilities ViewEngine along with jQuery Mobile (for mobile templates) and 51degrees.mobi (for accurate mobile browser detection) to build ASP.NET MVC sites that output nice mobile-friendly templates. The techniques that Scott talks about in his post have been working really well.

So recently when I had to solve a similar problem when trying to render out localized templates I took a deeper look into Scott’s approach to see if I could ‘tweak’ it to do what I wanted, which, in Visual Studio looks like:

This allows my site to serve the same URLs for multiple languages. For example the url /Home/Index uses the same controllers, models, etc, but will call different views for English and Spanish users based on the current uiCulture.

Here’s how I did it. I took Scott’s classes and refactored them just a bit so that as I extended this functionality there weren’t bits of code getting duplicated (See DRY – Don’t repeat yourself).

First, I took the existing CustomMobileViewEngine class and renamed it CustomViewEngine as this engine will no longer be Mobile only. Other than that no changes were necessary.

public class CustomViewEngine : IViewEngine
{
public IViewEngine BaseViewEngine { get; private set; }
public Func IsTheRightDevice { get; private set; }
public string PathToSearch { get; private set; }

public CustomViewEngine(Func<ControllerContext, bool> isTheRightDevice, string pathToSearch, IViewEngine baseViewEngine)
{
    BaseViewEngine = baseViewEngine;
    IsTheRightDevice = isTheRightDevice;
    PathToSearch = pathToSearch;
}

public ViewEngineResult FindPartialView(ControllerContext context, string viewName, bool useCache)
{
    if (IsTheRightDevice(context))
    {
        return BaseViewEngine.FindPartialView(context, PathToSearch + "/" + viewName, useCache);
    }
    return new ViewEngineResult(new string[] { }); //we found nothing and we pretend we looked nowhere
}

public ViewEngineResult FindView(ControllerContext context, string viewName, string masterName, bool useCache)
{
    if (IsTheRightDevice(context))
    {
        return BaseViewEngine.FindView(context, PathToSearch + "/" + viewName, masterName, useCache);
    }
    return new ViewEngineResult(new string[] { }); //we found nothing and we pretend we looked nowhere
}

public void ReleaseView(ControllerContext controllerContext, IView view)
{
    throw new NotImplementedException();
}
}

Next, I took the most generic AddMobile extension method, renamed it AddCustomView and put it in it’s own ViewHelper class.

public static class ViewHelper
{
public static void AddCustomView(this ViewEngineCollection ves, Func isTheRightDevice, string pathToSearch)
where T : IViewEngine, new()
{
ves.Add(new CustomViewEngine(isTheRightDevice, pathToSearch, new T()));
}
}

Additionally, I refactored the existing MobileHelpers class to call the newly refactored AddCustomView to prevent further duplication of code.

public static class MobileHelpers
{
public static bool UserAgentContains(this ControllerContext c, string agentToFind)
{
return (c.HttpContext.Request.UserAgent.IndexOf(agentToFind, StringComparison.OrdinalIgnoreCase) >= 0);
}

public static bool IsMobileDevice(this ControllerContext c)
{
    return c.HttpContext.Request.Browser.IsMobileDevice;
}

public static void AddMobile<T>(this ViewEngineCollection ves, string userAgentSubstring, string pathToSearch)
    where T : IViewEngine, new()
{
    ves.AddCustomView<T>(c => c.UserAgentContains(userAgentSubstring), pathToSearch);
}

public static void AddIPhone<T>(this ViewEngineCollection ves) //specific example helper
    where T : IViewEngine, new()
{
    ves.AddCustomView<T>(c => c.UserAgentContains("iPhone"), "Mobile/iPhone");
}

public static void AddGenericMobile<T>(this ViewEngineCollection ves)
    where T : IViewEngine, new()
{
    ves.AddCustomView<T>(c => c.IsMobileDevice(), "Mobile");
}
}

Finally, I created some AddLanguage extension methods in their own LocalizationHelpers class along with the UICulture detection routing.

public static class LocalizationHelpers
{
public static bool UICultureEquals(this ControllerContext c, string stringToFind)
{
var culture = CultureInfo.CurrentUICulture;
var cultureName = culture != null ? culture.Name : string.Empty;
    return (cultureName.IndexOf(stringToFind, StringComparison.OrdinalIgnoreCase) >= 0);
}

public static void AddLanguage<T>(this ViewEngineCollection ves, string cultureName, string pathToSearch)
    where T : IViewEngine, new()
{
    ves.AddCustomView<T>(c => c.UICultureEquals(cultureName), pathToSearch);
}

public static void AddLanguage<T>(this ViewEngineCollection ves, string cultureName)
    where T : IViewEngine, new()
{
    ves.AddCustomView<T>(c => c.UICultureEquals(cultureName), cultureName);
}

}

Using the Localized views in your project is as simple as registering the view in the Application_Start() method.

ViewEngines.Engines.Clear();
ViewEngines.Engines.AddLanguage("es-ES");
ViewEngines.Engines.AddGenericMobile();
ViewEngines.Engines.AddCustomView(c => c.IsMobileDevice() && c.UICultureEquals("es-ES"), "Mobile/es-ES");
ViewEngines.Engines.Add(new WebFormViewEngine());

Lastly, my sample project has these classes in they’re own class library because it’s my hope to be able to provide this functionality as a NuGet package soon.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s