UPDATE from 2011: These View Engines have a subtle release mode caching bug. Peter Mourfield and I have released a better MobileViewEngine for ASP.NET MVC 3 that is closer to what MVC 4 will look like. The updated blog post with the new MobileViewEngine is here.
In March of 2009 I spoke at Mix 09, Microsoft's Web Conference and presented a number of ASP.NET MVC features. I extended the NerdDinner Sample with a naive implementation of what I called a MobileCapableWebFormViewEngine. Here's the basic implementation. Don't use this, it's broken.
public class MobileCapableWebFormViewEngine : WebFormViewEngine
{
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
ViewEngineResult result = null;
var request = controllerContext.HttpContext.Request;
// Avoid unnecessary checks if this device isn't suspected to be a mobile device
if (request.Browser.IsMobileDevice)
{
result = base.FindView(controllerContext, "Mobile/" + viewName, masterName, useCache);
}
//Fall back to desktop view if no other view has been selected
if (result == null || result.View == null)
{
result = base.FindView(controllerContext, viewName, masterName, useCache);
}
return result;
}
}
This sample was never meant to be official anything, just show possibilities, but a lot of folks built on top the general idea. However, as reader Michael Baden Roufa (along with Will Creedle and Paulie Srinuan) pointed out to me in email, the logic is wrong. The FindView (and its sibling FindPartialView) method gets called by the ASP.NET MVC framework with useCache=true first, then useCache=false if nothing is found. This little sample view engine, as it is, is dependant on what kind of browser requests a page first because it's using one cache for two kinds of requests. I've noticed this on the NerdDinner site...depending on which device hits the site first, it can get stuck (cached) in a situation where non-mobile pages are being served to iPhones and mobile browsers.
I'll write a much longer post in a few days about how one goes from Broken, to Complex, Refactors, then finally gets to a Simpler Design. Here's how you'd use this code today. This is an example usage within Global.asax. You'd change it depending on the devices you'd care about.
void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ViewEngines.Engines.Clear();
ViewEngines.Engines.AddIPhone<WebFormViewEngine>();
ViewEngines.Engines.AddMobile<WebFormViewEngine>("blackberry", "Mobile/BlackBerry");
ViewEngines.Engines.AddMobile<WebFormViewEngine>(c => c.UserAgentContains("SomethingCustom"), "Mobile/SomethingCustom");
ViewEngines.Engines.AddGenericMobile<WebFormViewEngine>();
ViewEngines.Engines.Add(new WebFormViewEngine());
}
You're probably familiar with the concept of ViewEngines and you have heard, at least around town, that you can add 3rd party View Engines, and even mix them and match them. You can have WebForms Views, Spark Views, and Razor Views all in the same project. Rather than deriving from WebFormsViewEngine (or now in ASP.NET MVC 3, RazorViewEngine) and making a super-smart ViewEngine that has too many responsibilities (remember the Single Responsibility Principle), instead I talked to a bunch of folks and realized that I didn't want a smart ViewEngine with knowledge of a lot of devices. ASP.NET MVC already has the notion of an ordered list of ViewEngines.
Since I was on campus this week visiting, I talked to lots of folks like Levi Broderick, Dmitry Robsman, Damian Edwards, Brad Wilson, Marcin Dobosz and more. That's what I really miss while working from Oregon...the wandering around and brainstorming.
Anyway, adding some extension methods to ViewEngineCollection for standard options, while creating a CustomMobileViewEngine as a base to build on makes for a more flexible solution and cleaner code. The example above is the "everything example." You've got an iPhone View, a generic Mobile View, a BlackBerry View based on UserAgent, as well as a Custom View using a custom Lambda for some custom device, then finally the desktop view, which in this case is the WebFormViewEngine.
If you just wanted a Desktop Site and an iPhone site with Razor you'd do this. Note that the ordering is important. View folders are checked in the order they are listed here, each falling to the next until a View is found.
ViewEngines.Engines.Clear();
ViewEngines.Engines.AddIPhone<RazorViewEngine>();
ViewEngines.Engines.AddGenericMobile<RazorViewEngine>();
ViewEngines.Engines.Add(new RazorViewEngine());
Mixing WebForms Views and Razor Views next to Desktop Views and Mobile Views
You can also mix and match. A common scenario might be that you already have a nice WebFormViewEngine ASP.NET MVC site, but you'd like to add a mobile site and start using Razor at the same time for just those mobile views.
ViewEngines.Engines.Clear();
ViewEngines.Engines.AddGenericMobile<RazorViewEngine>();
ViewEngines.Engines.Add(new WebFormViewEngine());
So where are these Views to be found? Let's look at a more custom example. Given this example above, if I am in the HomeController's Index method and it calls View(), we'll look first for ~/Views/Home/Mobile/Index.cshtml, then ~/Views/Home/Index.aspx.
I could also add a custom UserAgent string to look for, with its own folder, with one line:
ViewEngines.Engines.Clear();
ViewEngines.Engines.AddMobile<WebFormViewEngine>("blackberry", "Mobile/BlackBerry");
ViewEngines.Engines.AddGenericMobile<RazorViewEngine>();
ViewEngines.Engines.Add(new RazorViewEngine());
Now if I called the HomeController's Index's method looking for the Index view, we'll look first for ~/Views/Home/Mobile/Blackberry/Index.aspx, then ~/Views/Home/Mobile/Index.cshtml, and finally ~/Views/Home/Index.aspx. However, we'll only be checking Mobile if it's a mobile device (per the Browser Capabilities object) and only checking the BlackBerry folder if the UserAgent contains "blackberry."
Sometimes, though, you'll want more complex custom "is this the right device" logic. Then you can pass in a lambda using whatever reasoning makes you happy, be it UserAgents, cookies, headers, whatever.
ViewEngines.Engines.Clear();
ViewEngines.Engines.AddIPhone<WebFormViewEngine>();
ViewEngines.Engines.AddMobile<WebFormViewEngine>("blackberry", "Mobile/BlackBerry");
ViewEngines.Engines.AddMobile<RazorViewEngine>(c => c.SomeExtensionMethod("SomethingCustom"), "Mobile/SomethingCustom");
ViewEngines.Engines.AddGenericMobile<RazorViewEngine>();
ViewEngines.Engines.Add(new WebFormViewEngine());
Now I've added a new line that digs around the ControllerContext (that's "c") for some information to base my decision on, and if that is true, it'll use the Mobile/SomethingCustom folder for the views. Remember that order matters.
Certainly this is just one way to do things, but it does maximize controller reuse. Not every site can be done like this. Different sites have different flows. You might end up with custom controllers for certain devices or maybe just custom actions. Or, you may need two totally different sites. It depends on your business needs, but it's nice to know the flexibility is here.
CustomMobileViewEngine
To do a hello world example, make a new MVC project (this will work for ASP.NET MVC 2 or ASP.NET MVC 3) and add this class to it (be aware of your namespace.) Again, I'll get this in NuGet soon.
public class CustomMobileViewEngine : IViewEngine
{
public IViewEngine BaseViewEngine { get; private set; }
public Func<ControllerContext, bool> IsTheRightDevice { get; private set; }
public string PathToSearch { get; private set; }
public CustomMobileViewEngine(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();
}
}
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, Func<ControllerContext, bool> isTheRightDevice, string pathToSearch)
where T : IViewEngine, new()
{
ves.Add(new CustomMobileViewEngine(isTheRightDevice, pathToSearch, new T()));
}
public static void AddMobile<T>(this ViewEngineCollection ves, string userAgentSubstring, string pathToSearch)
where T : IViewEngine, new()
{
ves.Add(new CustomMobileViewEngine(c => c.UserAgentContains(userAgentSubstring), pathToSearch, new T()));
}
public static void AddIPhone<T>(this ViewEngineCollection ves) //specific example helper
where T : IViewEngine, new()
{
ves.Add(new CustomMobileViewEngine(c => c.UserAgentContains("iPhone"), "Mobile/iPhone", new T()));
}
public static void AddGenericMobile<T>(this ViewEngineCollection ves)
where T : IViewEngine, new()
{
ves.Add(new CustomMobileViewEngine(c => c.IsMobileDevice(), "Mobile", new T()));
}
}
Go to your Global.asax.cs (or .vb) as I've showed before, and add maybe these lines to the end of your Application_Start().
ViewEngines.Engines.Clear();
ViewEngines.Engines.AddIPhone<RazorViewEngine>();
ViewEngines.Engines.AddGenericMobile<RazorViewEngine>();
ViewEngines.Engines.Add(new RazorViewEngine());
If you're using ASP.NET MVC 2, or if you simply prefer to, change RazorViewEngine to WebFormViewEngine. Or, mix and match to your taste.
In your Views folder, make ~/Views/Home/Mobile and ~/Views/Home/Mobile/iPhone and copy ~/Views/Home/Index.* to it. Then, open the new Mobile Index and Mobile/iPhone Index and change them. I'm put in "I'm a mobile view" and "I'm an iPhone view" just so I'd know for this hello world example. Like this:
Next, either get a User Agent Switcher for your browser, or download a fake iPhone Simulator called iBBDemo2 (it's totally fake, but it'll do). Hit the site, and boom, there you go.
Adding iPad is trivial:
ViewEngines.Engines.AddMobile<RazorViewEngine>("iPad", "Mobile/iPad");
As is Windows Phone 7 if you like:
ViewEngines.Engines.AddMobile<RazorViewEngine>("Windows Phone", "Mobile/WP7");
You get the idea.
More to Come
Since that post, the Live.com team in Ireland that released and supported the original Mobile Device Browser File (MDBF) has stopped producing it. The best source for mobile browser device data is WURFL (that was one of the places that MDBF pulled from.)
While I haven't spoken with the 51degrees.mobi folks (yet), they have an ASP.NET module that sucks information from WURFL and populates the Browser Capabilities object. Remember that everyone's requirements are different. You may not need to know everything about every device. Maybe you want to support iPhones and Desktop and that's it. Or, maybe you need to know the screen width and JPEG capabilities of some obscure old Nokia device. Know what you need before you buy into a detection decision or mobile database. We're working on more mobile recommendations and pest practices on the ASP.NET team and I'll continue share everything I know.
I'll be working with my new teammate Steven Sanderson on this template, maybe getting it into NuGet in the short term, and more baked into the framework in the long term. I'll update the NerdDinner sample - older versions for Mobile, and newer versions for MVC3 and mobile. I'll also try to look at jQuery Mobile and some of the nice JavaScript mobile libraries and improve the default templates. I'll also look at adding "opt out" options, so you can hit a site, get a mobile experience, but click a link that says "give me desktop anyway."
For now, it's a much better proof of concept for you to play with, Dear Reader.
Hosting By