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[] { });
}
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[] { });
}
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)
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.