Mobile Views in ASP.NET MVC3

Stack Overflow in the Windows Phone 7 Simulator

On Stack Exchange, we’ve just rolled out a brand spanking new mobile site.  This took about 6 weeks of my and our designer’s (Jin Yang) time, the majority of it spent building mobile Views.

Very little time was spent hammering mobile View switching support into MVC, because it’s really not that hard.

A nice thing about the Stack Exchange code base is that all of our Controllers share a common base class.  As a consequence, it’s easy to overload the various View(…) methods to do some mobile magic.  If your MVC site doesn’t follow this pattern it’s not hard to slap it onto an existing code base, it is a pre-requisite for this approach though.

Here’s the gist of the additions to the Controller base class:

protected new internal ViewResult View()
{
	if (!IsMobile()) return base.View();

	var viewName = ControllerContext.RouteData.GetRequiredString("action");
	CheckForMobileEquivalentView(ref viewName, ControllerContext);

	return base.View(viewName, (object)null);
}

protected new internal ViewResult View(object model)
{
	if (!IsMobile()) return base.View(model);

	var viewName = ControllerContext.RouteData.GetRequiredString("action");
	CheckForMobileEquivalentView(ref viewName, ControllerContext);

	return base.View(viewName, model);
}

protected new internal ViewResult View(string viewName)
{
	if (!IsMobile()) return base.View(viewName);

	CheckForMobileEquivalentView(ref viewName, ControllerContext);
	return base.View(viewName);
}

protected new internal ViewResult View(string viewName, object model)
{
	if (!IsMobile()) return base.View(viewName, model);

	CheckForMobileEquivalentView(ref viewName, ControllerContext);
	return base.View(viewName, model);
}

// Need this to prevent View(string, object) stealing calls to View(string, string)
protected new internal ViewResult View(string viewName, string masterName)
{
	return base.View(viewName, masterName);
}

CheckForMobileEquivalentView() looks up the final view to render, in my design the lack of a mobile alternative just falls back to serving the desktop versions; this approach may not be appropriate for all sites, but Stack Exchange sites already worked pretty well on a phone pre-mobile theme.

private static void CheckForMobileEquivalentView(ref string viewName, ControllerContext ctx)
{
	// Can't do anything fancy if we don't know the route we're screwing with
	var route = (ctx.RouteData.Route as Route);
	if (route == null) return;

	var mobileEquivalent = viewName + ".Mobile";

	var cacheKey = GetCacheKey(route, viewName);

	bool cached;
    // CachedMobileViewLookup is a static ConcurrentDictionary<string, bool>
	if (!CachedMobileViewLookup.TryGetValue(cacheKey, out cached))
	{
		var found = ViewEngines.Engines.FindView(ctx, mobileEquivalent, null);

		cached = found.View != null;

		CachedMobileViewLookup.AddOrUpdate(cacheKey, cached, delegate { return cached; });
	}

	if (cached)
	{
		viewName = mobileEquivalent;
	}

	return;
}

The caching isn’t interesting here (though important for performance), the important part is the convention of adding .Mobile to the end of a View’s name to mark it as “for mobile devices.”  Conventions rather than configurations, after all, being a huge selling point of the MVC framework.

And that’s basically it.  Anywhere in your Controllers where you call View(“MyView”, myModel) or similar will instead serve a mobile View if one is available (passing the same model for you to work with).

If you’re doing any whole cloth caching (which you probably are, and if not you probably should be) [ed: I seem to have made this phrase up, "whole cloth caching" is caching an entire response] you’ll need to account for the mobile/desktop divide.  All we do is slap “-mobile” onto the keys right before they hit the OuputCache.

One cool trick with this approach is that anywhere you render an action (as with @Html.Action() in a razor view) will also get the mobile treatment.  Take a look at a Stack Overflow user page to see this sort of behavior in action.  Each of those paged subsections (Questions, Answers, and so on) is rendered inline as an action and then ajax’d in via the same action.  In fact, since the paging code on the user page merely fetches some HTML and writes it into the page (via jQuery, naturally) we’re able to use exactly the same javascript on the desktop user page and the mobile one.

I’m not advocating the same javascript between desktop and mobile views in all cases, but when you can do it (as you sometimes can when the mobile view really is just the “shrunk down” version of the desktop) it’ll save you a lot of effort, especially in maintenance down the line.

Another neat tidbit (though MVC itself gets most of the credit here), is the complete decoupling of view engines from the issue.  If you want Razor on mobile, but are stuck with some crufty old ASPX files on the desktop (as we are in a few places) you’re not forced to convert the old stuff.  In theory, you could throw Spark (or any other view engine) into the mix as well; though I have not actually tried doing that.

As an aside, this basic idea seems to be slated for MVC per Phil Haack’s announcement of the MVC4 Roadmap.  I’ve taken it as a validation of the basic approach, if not necessarily the implementation.


5 Comments on “Mobile Views in ASP.NET MVC3”

  1. Jeff Dalley says:

    Great post, awesome approach! Thanks

  2. Jim Geurts says:

    What do you mean by “whole cloth caching”?

  3. Ben Vitale says:

    Kevin, what’s the delta like between your desktop views and mobile views? Significant HTML structural differences, CSS, both? (Maybe get Jin to bog about the experience from his standpoint?)

    Cheers
    Ben

Follow

Get every new post delivered to your Inbox.