September 2012

Volume 27 Number 09

Cutting Edge - Mobile Site Development, Part 4: Managing Device Profiles

By Dino Esposito | September 2012

Dino EspositoIn this article I’ll discuss a way to classify mobile devices and build a Web site that serves different markup to different devices based on the capabilities of the device.

If you don’t need to adapt the rendered markup to the capabilities of a requesting browser, building a mobile site can be a seamless experience. More often than not, though, you need to fine-tune the content you serve and adapt it to the effective capabilities of the browser. Does that sound like the reiteration of an old story? Years ago, developers faced a similar problem for desktop browsers. It was common to write Web pages that checked the type (and sometimes the version) of the browser before deciding about the markup to return. Recently, as the focus of Web programming has shifted more toward the client side, libraries such as jQuery and Modernizr have provided a significant contribution in keeping developers away from many of the browsers’ differences.

It still can be difficult and expensive to build a desktop Web site that looks and works the same regardless of the browser. However, the number of desktop browsers is relatively small, and the gap between the most recent versions of all browsers is not huge. When it comes to mobile browsers, though, nearly any model of device has its own slightly different and customized browser. In addition, users may have installed a cross-device browser such as Fennec or Opera Mini. The large number of possible mobile browsers makes targeting each of them separately—as developers did with desktop browsers—a highly impractical approach. A smarter approach is to partition mobile browsers into a few classes and serve each class an ad hoc version of any given page. This approach is often referred to as multiserving.

The sample Web site for this article is built using ASP.NET MVC 3. It should be noted, though, that ASP.NET MVC 4 brings some new facilities that could make the implementation of multiserving simpler. I’ll cover ASP.NET MVC 4 in relation to mobile sites in a future column.

From Fragmentation to Device Profiles

Mobile fragmentation is significant, with thousands of unique devices and hundreds of capabilities to fully describe them. You ideally need pages that can intelligently adjust to the characteristics of the requesting browsers. To achieve this, you have essentially two possible routes. One is authoring different versions of the same page—one for each class of device it’s intended to support. The other consists of having one common page template and filling it up with device-specific content on each request.

In the end, however, both approaches start from a common ground: Split your expected audience into a few categories. Then, each page of the site will provide ad hoc markup for each of the categories. 

To tame device fragmentation, you should decide early on how many versions of the mobile site you intend to have. This is a key decision because it impacts the perception of the site and, ultimately, its success. It’s not, therefore, a decision to take lightly, and business considerations apply—it’s not simply an implementation detail or technology decision. Depending on the business case, you might decide to offer the site only to smartphones and perhaps optimize the site for, say, Windows Phone devices, in much the same way some desktop sites were showcasing the label “best viewed with XXX” a decade or so ago. More likely, though, you’ll want to have at least two versions of the site—for smart and legacy devices—and maybe consider yet another version that specifically targets tablet devices or even smart TVs.

Smartphones, legacy devices and tablets are all examples of device profiles into which you split your expected audience. You don’t have to write your mobile site to address thousands of devices by name; instead, you identify a few device profiles and define which capabilities are required to join each profile. It goes without saying that there might not be fixed and universal rules that define when a device is a “smartphone” and when it’s not. There’s no ratified standard for this, and you are responsible for defining the capabilities required for a device to be classified as a smartphone in the context of your site. Also consider that the definition of a smartphone is variable by design. A Windows CE device was certainly perceived as a very smart device only five or six years ago. Today, it would be hard to include it in the smartphone category.

Project Liike—an effort of the patterns & practices group at Microsoft aimed at building a mobile reference site—splits the mobile audience into three classes, familiarly called WWW, short for Wow, Works and Whoops.

The Wow class refers to today’s rich and smart devices. The Works class refers to not-so-rich and capable devices. Finally, the Whoops class refers to any other legacy device that barely has the ability to connect to the Internet and render some basic HTML content. For more information on Project Liike, visit liike.github.com.

In this article I’ll use the following device profiles: smartphone, tablet and legacy mobile. Figure 1 shows the (minimal) set of rules I used to accept devices in the various profiles. Note that the rules should be expanded to include more specific capabilities that depend on what your pages really need to do. For example, if you plan to use Asynchronous JavaScript and XML and HTML Document Object Model manipulation, you might want to ensure that devices have those capabilities. If you’re serving videos, you might want to ensure that devices support some given codecs. For devices that might not be able to match all of your expectations, you should provide a fallback page, and this is precisely the role of the legacy (that is, catch-all) profile.

Figure 1 Sample Device Profiles

Device Profile Capabilities
Smartphone Mobile device, touch device, screen width greater than 240 pixels, based on a known OS (Android 2.1, iOS, BlackBerry 6.0 or Windows Phone).
Tablet Mobile device and tablet device.
Mobile Mobile device not falling into other profiles.

Implementing a Simple Device Profiler

In the sample site, I formalize the content of Figure 1 into an interface named IDeviceProfiler:

public interface IDeviceProfiler
{
  Boolean IsDesktop(String userAgent);
  String MobileSuffix { get; }
  Boolean IsMobile(String userAgent);
  String SmartphoneSuffix { get; }
  Boolean IsSmartphone(String userAgent);
  String TabletSuffix { get; }
  Boolean IsTablet(String userAgent);
}

The suffix refers to a unique name used to differentiate views. For example, the page index.cshtml will be expanded to index.smartphone.cshtml, index.tablet.cshtml and index.mobile.cshtml for the various profiles. Figure 2 shows a basic implementation for a device profiler object.

Figure 2 A Minimal Device Profiler Implementation

public class DefaultDeviceProfiler : IDeviceProfiler
{
  public virtual String MobileSuffix {     get { return "mobile"; }   }
  public virtual Boolean IsMobile(String userAgent)
  {
    return HasAnyMobileKeywords(userAgent);
  }
  public virtual String SmartphoneSuffix {
    get { return "smartphone"; }
  }
  public virtual Boolean IsSmartphone(String userAgent)
  {
    return IsMobile(userAgent);
  }
  public virtual String TabletSuffix {
    get { return "tablet"; }
  }
  public virtual Boolean IsTablet(String userAgent)
  {
    return IsMobile(userAgent) &&
      userAgent.ContainsAny("tablet", "ipad");
  }
  public virtual Boolean IsDesktop(String userAgent)
  {
    return HasAnyDesktopKeywords(userAgent);
  }
  // Private Members
  private Boolean HasAnyMobileKeywords(String userAgent)
  {
    var ua = userAgent.ToLower();
    return (ua.Contains("midp") ||
      ua.Contains("mobile") ||
      ua.Contains("android") ||
      ua.Contains("samsung") ||      ...
  }
  private Boolean HasAnyDesktopKeywords(String userAgent)
  {
    var ua = userAgent.ToLower();
    return (ua.Contains("wow64") ||
      ua.Contains(".net clr") ||
      ua.Contains("macintosh") ||
      ...
  }
}

As you can guess from Figure 2, each device is identified through its user agent string. The user agent string is processed to see if it contains some keywords known to represent a mobile or desktop browser. For example, a user agent string that contains the substring “android” can be safely matched to a mobile browser. Similarly, the “wow64” substring usually refers to a desktop Windows browser. Let me say up front that while relying on user agent strings is probably the best approach to detect device capabilities on the server side, it’s not a guarantee of success. Personally, I recently bought an Android 4.0 tablet and discovered that the embedded browser just sends out verbatim the user agent of an iPad running iOS 3.2. Device fragmentation is hard because of these issues.

Selection of View, Layout and Model

Let’s say that the device profiler can reliably tell us which profile the requesting browser belongs to. In an ASP.NET MVC site, how would you select the right view and layout from within each controller method? In any controller method that returns HTML markup, you may indicate explicitly the name of the view and related layout. Both names can be determined in the controller method using the following code:

// Assume this code is from some Index method
var suffix = AnalyzeUserAgent(Request.UserAgent);
var view = String.Format("index.{0}", suffix);
var layout = String.Format("_layout.{0}", suffix);
return View(view, layout);

In a multiserving scenario, the differences between views for the same page may not be limited to the view template. In other words, picking up a specific pair of view and layout templates might not be enough—you might even need to pass a different view model object.

If you’re passing view model data through built-in collections such as ViewBag or ViewData, you can consider moving any code that deals with the analysis of the user agent string out of the controller. In the sample mobile site code download accompanying this article, the Index method for the home page looks like this:

public ActionResult Index()
{
  ViewBag.Title = "...";  ...
  return View();
}

As you can see, the view is generated without an explicit indication of the name and layout. When this happens, the view engine is ultimately responsible for finalizing the view to use and its layout. The view engine is therefore a possible place to embed any logic for managing device profiles. By creating and registering a custom view engine, you isolate any logic for analyzing device profiles in a single place, and the remainder of your mobile site can be developed as a plain collection of related pages. The following code shows how to register a custom view engine in global.asax:

 

// Get rid of any other view engines
ViewEngines.Engines.Clear();// Install an ad hoc mobile view engine
ViewEngines.Engines.Add(new MobileRazorViewEngine());

Figure 3 shows the source code of the custom (Razor-based) view engine.

Figure 3 A Mobile-Aware View Engine

public class MobileRazorViewEngine : RazorViewEngine
{
  protected override IView CreatePartialView(
    ControllerContext context, String path)
  {
    var view = path;
    if (!String.IsNullOrEmpty(path))
      view = GetMobileViewName(context.HttpContext.Request, path);
    return base.CreatePartialView(controllerContext, view);
  }
  protected override IView CreateView(
    ControllerContext context, String path, String master)
  {
    var view = path;
    var layout = master;
    var request = context.HttpContext.Request;
    if (!String.IsNullOrEmpty(path))
      view = GetMobileViewName(request, path);
    if (!String.IsNullOrEmpty(master))
      master = GetMobileViewName(request, master);
    return base.CreateView(context, view, master);
  }
  public static String GetMobileViewName(
    HttpRequestBase request, String path)
  {
    var profiler =  DependencyResolver.Current.GetService(
      typeof(IDeviceProfiler)) as IDeviceProfiler
      ?? new DefaultDeviceProfiler();
    var ua = request.UserAgent ?? String.Empty;
    var suffix = GetSuffix(ua, profiler);
    var extension = String.Format("{0}{1}",
      suffix, Path.GetExtension(path));
    return Path.ChangeExtension(path, extension);
  }
  private static String GetSuffix(String ua, IDeviceProfiler profiler)
  {
    if (profiler.IsDesktop(ua))
      return String.Empty;
    if (profiler.IsSmartphone(ua))
      return profiler.SmartphoneSuffix;
    if (profiler.IsTablet(ua))
      return profiler.TabletSuffix;
    return profiler.IsMobile(ua)
      ? profiler.MobileSuffix
      : String.Empty;
  }
}

Before rendering a view, the view engine uses the installed device profiler to query about the profile of the requesting user agent. Based on that, the view engine switches to the most appropriate view. If the layout name is provided explicitly in the View call from within the controller, the view engine can resolve it seamlessly. If the layout name is set in _viewStart.cshtml (as in most ASP.NET MVC code), the view engine won’t be able to resolve it because the master parameter in CreateView is always empty. Here’s a fix to apply in _viewStart.cshtml:

@using MultiServing.ProfileManager.Mvc
@{
  const String defaultLayout = "~/Views/Shared/_Layout.cshtml";
  Layout = MobileRazorViewEngine.GetMobileViewName(
    Context.Request, defaultLayout);
}

What if you use strongly typed views, and the various mobile views for the same page (smartphone, tablet and so on) each requires its own view model? In this case, you might want to build a worker component that analyzes the user agent and returns the view/layout name, and use this component from within each controller method. As I see things, if you need to parse the user agent right at the controller level to decide about the view model, then relying on a custom view engine is redundant because you already know which view to call.

Beyond this point, it’s all about using an appropriate device profiler and building multiple HTML page templates.

Configuring WURFL in ASP.NET MVC

As mentioned in previous installments of this series, the Wireless Universal Resource File (WURFL) is a popular Device Description Repository (DDR) used in the back ends of Google and Facebook mobile sites. WURFL offers a multiplatform API and can be easily plugged into any ASP.NET MVC project using NuGet (see Figure 4).

Adding WURFL to ASP.NET MVC via NuGet

Figure 4 Adding WURFL to ASP.NET MVC via NuGet

WURFL adds an XML database to your project that contains device information. The database should be loaded into memory at application startup and provides nearly instant access to device profiles. In global.asax, you add the code shown in Figure 5.

Figure 5 Using WURFL

public class MyApp : HttpApplication
{
  public static IWURFLManager WurflContainer;
  protected void Application_Start()
  {    ...
    RegisterWurfl();    
    DependencyResolver.SetResolver(new SimpleDependencyResolver());  }
  public static void RegisterWurfl()
  {
    var configurer = new ApplicationConfigurer();
    WurflContainer = WURFLManagerBuilder.Build(configurer);
  }
}

In Figure 6, you see an IDeviceProfiler component that uses WURFL to detect smartphones and tablets. You resolve the profiler via a custom dependency resolver. (See the accompanying source code for details about the resolver.)

Figure 6 A WURFL-Based Device Profiler

public class WurflDeviceProfiler : DefaultDeviceProfiler
{
  public override Boolean IsMobile(String ua)
  {
    var device = MyApp.WurflContainer.GetDeviceForRequest(ua);
    return device.IsWireless();
  }
  public override Boolean IsSmartphone(String ua)
  {
    var device = MyApp.WurflContainer.GetDeviceForRequest(ua);
    return device.IsWireless() && !device.IsTablet() &&
      device.IsTouch() &&
      device.Width() > 240 &&
      (device.HasOs("android", new Version(2, 1)) ||
      device.HasOs("iphone os", new Version(3, 2)) ||
      device.HasOs("windows phone os", new Version(7, 1)) ||
      device.HasOs("rim os", new Version(6, 0)));
  }
  public override Boolean IsTablet(String ua)
  {
    var device = MyApp.WurflContainer.GetDeviceForRequest(ua);
    return device.IsTablet();
  }
}

The method GetDeviceForRequest queries the WURFL database and returns an IDevice object that can be queried using a relatively fluent syntax. Note that methods such as IsTouch, IsTablet and HasOs are actually extension methods built over the native WURFL API that you find in the sample project. As an example, here’s the code for IsTablet:

public static Boolean IsTablet(this IDevice device)
{
  return device.GetCapability("is_tablet").ToBool();
}

I’ve discussed in this column a concrete example of an ASP.NET MVC mobile site built to provide a different experience on a variety of devices: smartphones, tablets and legacy mobile devices, as shown in Figure 7. I suggest you download the source code and run it in Internet Explorer (or other browsers), switching to different user agents. You can also test the site live at www.expoware.org/amse/ddr. Note that accessing the site from a desktop browser results in the message: “This site is not available on desktop browsers. Try using a mobile or tablet browser.”

Tablets, Smartphones and Plain Mobile Devices Accessing the Sample Site

Figure 7 Tablets, Smartphones and Plain Mobile Devices Accessing the Sample Site


Dino Esposito is the author of “Architecting Mobile Solutions for the Enterprise” (Microsoft Press, 2012) and “Programming ASP.NET MVC 3” (Microsoft Press, 2011), and coauthor of “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2008). Based in Italy, Esposito is a frequent speaker at industry events worldwide. Follow him on Twitter at twitter.com/despos.

Thanks to the following technical experts for reviewing this article: Erik Porter and Pranav Rastogi