Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Extending the ASP.NET MVC ViewEngine to Support Localization

0.00/5 (No votes)
12 May 2011 1  
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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here