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 image has an empty alt attribute; its file name is customview.png

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.

This image has an empty alt attribute; its file name is langsnipesp1.png

This image has an empty alt attribute; its file name is langsnipen.png

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.

[code lang=”csharp”]
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();
}
}
[/code]

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

[code lang=”csharp”]
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()));
}
}
[/code]

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

[code lang=”csharp”]
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");
}
}
[/code]

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

[code lang=”csharp”]
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);
}
}
[/code]

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

[code lang=”csharp”]
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());
[/code]

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

I’m Peter

I’ve spent my career building software and leading engineering teams. I started as a developer and architect, grew into engineering leadership, and today I serve as a Chief Technology Officer.

Here, I share practical insights on technology, leadership, and building high-performing teams.

Connect with me on LinkedIn.

Discover more from Peter Mourfield

Subscribe now to keep reading and get access to the full archive.

Continue reading