65.9K
CodeProject is changing. Read more.
Home

Extending the ASP.NET MVC ViewEngine to Support Localization

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (1 vote)

May 12, 2011

CPOL

2 min read

viewsIcon

14211

Adding localization support to the ASP.NET MVC ViewEngine

I’ve been using 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<ControllerContext, bool> 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<T> extension method, renamed it AddCustomView<T>, and put it in its own ViewHelper class.

public static class ViewHelper
{
    public static void AddCustomView<T>(this ViewEngineCollection ves, 
           Func<ControllerContext, bool> 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<T> 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<T> 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<WebFormViewEngine>("es-ES");
ViewEngines.Engines.AddGenericMobile<WebFormViewEngine>();
ViewEngines.Engines.AddCustomView<WebFormViewEngine>(c => c.IsMobileDevice() 
    && c.UICultureEquals("es-ES"), "Mobile/es-ES");
ViewEngines.Engines.Add(new WebFormViewEngine());

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

I’ve included my sample project here for reference.