Export (0) Print
Expand All

Detecting devices and their features

The number of mobile web browsers in use today is surprisingly large. In addition, there is a lot of variation in the characteristics of these devices (such as screen size and pixel density) as well as in the browsers’ support for web standards. Historically, these variations have been so divergent that it was common for web apps to have two completely different sets of pages. That is, one set of markup for mobile devices and another for desktop browsers.

However, new types of devices such as tablets and televisions are being used to browse the web, and creating a separate set of pages for each of these types is simply not feasible. The distinctions between these devices are blurring. It is becoming increasingly difficult to make assumptions about device and browser capabilities based upon these historical classifications.

We’ve already mentioned that the safest choice when developing for the web is to keep things simple. However, users’ expectations are always increasing. One solution to building the simplest app is progressive enhancement. Progressive enhancement means that you first build your app to support the set of features that represents the lowest common denominator. In the case of the mobile web, this will likely mean simple HTML and CSS and very little JavaScript. First, ensure that the essential functionality of the app is available on highly constrained devices, then begin detecting specific features and enhancing the user experience when those features are present.

The concept of identifying mobile devices as the lowest common denominator has become known as mobile first.

The legacy experience of Mileage Stats targeted desktop browsers. Since our goal was to extend the app to provide a mobile experience, we decided early in the project to group devices into two classes: desktop and mobile. We further subdivided the mobile experience into the Works and Wow experiences mentioned in Mobilizing the Mileage Stats application.

Detecting features on the server

Identifying and grouping devices into classes

Every browser identifies itself by providing a user-agent string whenever it makes a request to a web server. This user-agent string not only identifies the browser’s vendor and version number, but frequently includes information such as the host operating system or some identifier for the device it’s running on.

It is generally a bad practice to provide unique responses for specific user-agent strings for a number of reasons.

User-agent strings may vary slightly even for the same browser on devices that are seemingly identical. The number of variations can quickly become overwhelming.

Likewise, there is no guaranteed way to predict the user-agent strings of future browsers. Historically this has caused problems. At one time, it was a common practice for many sites to deliver the latest markup only to browsers that identified certain vendors in their user-agent strings. Other vendors received down-level markup even if they supported the latest features. We still see vestiges of this practice in modern user-agent strings; for example, the inclusion of "Mozilla/5.0" in the user-agent string for Internet Explorer 9. For more information on user-agent strings, see Understanding User-Agent Strings.

Despite these problems, user-agent strings are still necessary in many scenarios.

There are third-party databases available that can provide detailed information about a browser based on its user-agent string. These solutions are not generally free for commercial use though, and they all require the use of a custom programming interface for making queries. Nevertheless, these databases allow developers to make choices about what assets to send to a browser based on capabilities. Furthermore, browsers can be sorted into classifications based upon their capabilities.

Built-in feature detection in ASP.NET

Out of the box, ASP.NET will examine an incoming request and provide you with information on the capabilities of the browser making the request. The capabilities are exposed on the HttpRequest object as the property Browser with a type of HttpBrowserCapabilities. Internally, ASP.NET uses an instance of HttpCapabilitiesDefaultProvider to populate this property.

However, you can create your own provider by inheriting from HttpCapabilitiesProvider. In Mileage Stats, we did just that and created MobileCapabilitiesProvider. Our custom provider inherits from the default provider. Then you can tell ASP.NET to use your custom provider instead of the default.

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    InitializeDependencyInjectionContainer();

    // Injects the custom BrowserCapabilitiesProvider 
    // into the ASP.NET pipeline.
    HttpCapabilitiesBase.BrowserCapabilitiesProvider = 
        container.Resolve<MobileCapabilitiesProvider>();

    // Some code omitted for clarity
}

In the snippet above, we resolve our custom provider from the container and tell ASP.NET to use it instead of the default, by assigning it to HttpCapabilitiesBase.BrowserCapabilitiesProvider.

At its core, the HttpBrowserCapabilities object is really just a dictionary, though it has many strongly typed properties available for convenience. These properties perform an internal lookup and then convert the raw value to the appropriate type. For most properties, the raw value is a string representing a Boolean value such as True or False.

We can set values in the dictionary directly using the indexer and the strongly typed properties will return the corresponding values.

When creating a custom provider, the only method that needs to be overridden when inheriting from HttpCapabilitiesProvider is GetBrowserCapabilities.

HttpBrowserCapabilities GetBrowserCapabilities(HttpRequest request);

In our implementation, we’ll first get the values from the default provider and then supplement that with data from multiple sources.

Extending ASP.NET with a third-party database

There are a number of third-party databases and services that can be used to determine a browser’s capabilities from a user-agent string. 51Degrees, DeviceAtlas, and WURFL are a few examples. At the time of this writing, both 51Degrees and WURLF provide NuGet packages.

We chose not to implement a specific third-party product in Mileage Stats. Instead, we created a placeholder function named DetermineCapsBy3rdPartyDatabase that simulates providing results from a database.

In the snippet below, we first get the base capabilities by invoking the method on the super class. We then call GetAdditionalCapabilities and merge the results into a single dictionary. Finally, we return an instance of HttpBrowserCapabilities.

public override HttpBrowserCapabilities GetBrowserCapabilities(HttpRequest request)
{
    var httpContext = request.RequestContext.HttpContext;
    var browser = base.GetBrowserCapabilities(request);

    SetDefaults(browser);

    browser.Capabilities.Merge(GetAdditionalCapabilities(httpContext));

    return browser;
}

public virtual IDictionary GetAdditionalCapabilities(HttpContextBase context)
{
    var capabilities = new Dictionary<string, string>();

    if (BrowserOverrideStores.Current.GetOverriddenUserAgent(context) != null) return capabilities;

    capabilities.Merge(DetermineCapsBy3rdPartyDatabase(context));
    capabilities.Merge(DetermineCapsByProfilingClient(context.Request, _encoder));

    return capabilities;
} 

Detecting features on the client

Detecting browser capabilities with JavaScript

New devices and new browsers appear on the market every day. It becomes difficult to keep the existing server-side databases up to date and, as a consequence, it is possible to encounter a device or browser not yet available in any of them. This is one reason why you might not want to rely entirely on the server-side detection of features.

Another technique is to detect the browser’s capabilities using JavaScript code. Modernizr is a popular library that facilitates this approach.

In addition, the JavaScript code can also store the detected capabilities in a cookie and pass those back to the server in subsequent requests. This of course will only work for devices with JavaScript and cookie support.

The server can then use that information to extend or complement the information found in the database to classify the device in a specific class. Mileage Stats employs this approach to confirm features such as screen size.

When the browser requests the first page, the server also includes a reference to a JavaScript file for detecting capabilities. The JavaScript file is generated based upon a manifest located in the MileageStats.Web project at \ClientProfile\Generic.xml. This manifest is an XML file that maps a device capability to a fragment of JavaScript code. The following XML fragment shows how two capabilities are mapped in the manifest file to JavaScript code.

<profile title="Generic" id="generic" version="1.1">
  <feature id="width" default="800">
    <name>Screen Width</name>      
    <description>From window.innerWidth if available, otherwise from screen.width.</description>      
    <test type="text/javascript">
      <![CDATA[ return (window.innerWidth>0)?         window.innerWidth:screen.width; ]]>
    </test>
  </feature> 
</profile> 

The ProfileScript action on the MobileProfileController reads the manifest and generates the fully realized JavaScript. In addition, the manifest contains a version number which is associated with the cookie. If the manifest is updated and the version changes, the server will take care of renewing the cookie as well to reflect those changes.

The generated JavaScript sets a cookie containing the results of the tests defined in the manifest. On subsequent requests, the MobileCapabilitiesProvider collects these results using ProfileCookieEncoder. The results are then merged with the other device capabilities that have already been collected.

public static readonly Func<HttpRequestBase, IProfileCookieEncoder, IDictionary<string, string>> DetermineCapsByProfilingClient =
    (request, encoder) =>
    {
        // The profile cookie is parsed for getting the device
        // capabilities inferred on the client side.
        var profileCookie = request.Cookies["profile"];

        return (profileCookie != null)
                ? encoder.GetDeviceCapabilities(profileCookie)
                : new Dictionary<string, string>();
    };

Providing content for the different identified classes

Once all the device capabilities from different sources are combined, the server is in good shape to determine and categorize the device into a specific class. The three classes that Mileage Stats is concerned with correspond to the experiences we’ve outlined: Legacy, Works, and Wow.

The Legacy experience is the original set of assets developed for Project Silk. For the Works experience, the server will simply send basic HTML markup with no JavaScript. All the implementation will rely on HTTP full postbacks.

For the Wow experience, a more robust single page application (SPA) implementation with JavaScript will be provided. See Delivering the SPA enhancements for details.

A device can be associated with the Wow class if the following capabilities are present:

  • JSON
  • XmlHttpRequest
  • HashChangeEvent
public static bool IsWow(this HttpBrowserCapabilitiesBase httpBrowser)
{
    // We should also check for supporting DOM manipulation; however,
    // we currently don't have a source for that particular capability.
    // If you use a third-party database for feature detection, then
    // you should consider adding a test for this.
    return httpBrowser.IsMobileDevice &&
            httpBrowser.SupportsJSON() &&
            httpBrowser.SupportsXmlHttp && 
            httpBrowser.SupportsHashChangeEvent();
}

If any of these capabilities cannot be found on the device, it is automatically associated with the Works experience.

JJ149688.290611D5A5C82326F6CCCCD0C76F9355(en-us,PandP.10).png

Determining which experience to deliver

Organizing ASP.NET MVC views for desktop and mobile experiences

Mileage Stats needs to deliver a different set of assets for the different experiences, so we wanted to generate markup that would be optimized for each experience. We took advantage of a new feature in ASP.NET MVC 4 called Display Modes. MVC 4 will automatically look for a mobile-specific view if the IsMobileDevice property returns "true." The mobile-specific views are designated with the file suffix .Mobile.cshtml. For example, two different views can be provided for the dashboard page: Index.cshtml for the desktop experience and Index.Mobile.cshtml for the mobile experience.

JJ149688.note(en-us,PandP.10).gifNote:
If you are using ASP.NET MVC 3, you can easily simulate this feature using the NuGet package, Mobile View Engines.

Writing ASP.NET views for the mobile experience

In a few rare cases, such as the _Layout.Mobile.cshtml, the view will contain some conditional code for checking the device capabilities using the current HttpBrowserCapabilities instance available in the view context.

For example, at the beginning of the layout we check to see if the SPA should be enabled.

<!DOCTYPE HTML>
@{
    var shouldEnableSpa = (Request.Browser.IsWow() && User.Identity.IsAuthenticated);
    var initialMainCssClass = shouldEnableSpa ? "swapping" : string.Empty;
}

Then inside the body, we conditionally render the client-side templates used by the SPA.

    <body>
        @if (shouldEnableSpa)
        {
            Html.RenderPartial("_spaTemplates");
        }

JJ149688.note(en-us,PandP.10).gifNote:
In general, we recommend that you avoid having logic in your views. It can be difficult to discover and even more difficult to test. Instead, logic in views should be moved into view models or controller actions.

False positives and caveats

No matter what approach you take, be it server-side or client-side detection of features, you will very likely encounter false positives. That is, some tests for browser capabilities will report that a capability is present even when it is not. For example, during the development of Mileage Stats we discovered some devices that reported support for geolocation; however, these devices failed to return any data. Our tests indicated that the problem was with the device and was not a problem with Mileage Stats itself.

We also encountered a situation in which certain tablets would provide a user-agent string that was easy to misinterpret as a desktop browser, though this only occurred when certain settings where enabled on the tablet.

The unavoidable truth is that you cannot accurately detect features for all devices. There will always be exceptions.

Summary

In general, your app should not attempt to deliver a specific experience to a specific device. Instead, it should respond to the presence of relevant features in a browser. Even though feature detection is typically associated with the client, the technique can be very useful on the server as well. In ASP.NET, there exists an API to facilitate this detection technique and there are also several ways to extend what is available out of the box.


Last built: June 5, 2012

Show:
© 2014 Microsoft