BT

HTML5 offline web applications using ASP.NET MVC

Posted by Jef Claes on May 02, 2012 |

One of the major constraints of web applications has always been connectivity. We imagined leveraging the browser to bring fully competent web applications to the desktop, but failed due to the lack of decent browser support. Although there were some caching techniques available before, they were never really designed with the intention of making web applications run completely offline, making them fragile and complex to set up. HTML5 tries to make up for this missing browser capability by introducing the offline application cache; a more reliable way to make web applications truly available even offline.

Why should my web application run offline?

To be honest, a lot of desktop web applications would hardly yield any return on investment of being able to run completely offline. Desktops are almost always connected, I especially see mobile web applications reaping the benefits of this new feature.

Mobile phone coverage continues to be flaky or even non-existent in many areas. Being able to fluently close that disconnectivity gap would significantly improve the user-friendliness of mobile applications running in the browser.

In more specific scenarios, being able to take the whole application offline could mean the difference between having to build multiple native applications or having the luxury of building one cross-platform browser solution.

Imagine a sales person who wants an interactive catalogue on her tablet to show to her customers in the field. She could use almost any device she wanted, simply browse to the catalogue when connected, then take it into the field offline.

You don't necessarily have to be offline to take advantage of the application cache though. One could use the application cache as a super cache, storing resources offline, so they don't slow down application startup. Updated resources will be downloaded in the background, and swapped with the older ones when the update can be committed. This scenario makes a lot of sense for heavy desktop web applications.

The manifest file

You don't need to do a whole lot of programming to start using the application cache. Resources that need to be available offline are defined in a simple text file, named the manifest file.

Format

A simple manifest file could look like this.

CACHE MANIFEST
# Version 1.0
CACHE:
/home/index
/content/style.css
/scripts/main.js
NETWORK:
/service/status
FALLBACK:
/logo.png /logo_offline.png

A manifest file should always have the CACHE MANIFEST header on the first line.

Lines starting with a number sign (#) are comments. These are often used to explicitly modify the manifest file, informing the browser that the cache needs to be updated. This is very useful when you, for example, change an image, but don't change the name of the image. The browser would otherwise have no way of detecting that the image on the remote server has changed.

Next to that, the manifest file can contain three sections: CACHE, NETWORK and FALLBACK. In the CACHE section you specify the resources that need to be cached. Resources that always should be downloaded from the server, even when offline, should be defined in the NETWORK section. If you have a lot of resources which should always be loaded from the server when online, you can use the asterisk character (*) as a wildcard in the NETWORK section. In the FALLBACK section you can specify fallback resources to be used when the user is offline.

The parsing of the manifest isn't very strict. The sections can be used in any order. They can even be used multiple times in one manifest file.

You can use both relative and absolute paths to locate the resources in the manifest file. If you use relative paths, they should be relative to the location of the application manifest file.

Referencing the manifest

To link the manifest to the application, the manifest attribute should be added to the html tag. Each page that references the manifest will be cached automatically. However, it is advised to explicitly list pages you want to cache in the manifest as well. Also, if the page is not referenced in the manifest, and it is never browsed to online, it will not be available offline. The browser has no clue that that page even exists.

<html manifest="cache.manifest")/>

Checking cache status

Using the applicationCache API, we can check the status of the application cache. Use the status property of the window.applicationCache singleton to query the current cache status. The status property is a number in the 0 to 5 range, of which each number is mapped to a specific cache state.

Number

State

Description

0

uncached

The page is not in the application cache. This is also the state the page will be in the very first time the application cache is downloaded.

1

idle

When the application cache is up-to-date, the browser will set the status to idle.

2

checking

When the application is checking for an updated manifest, the browser will set the status checking.

3

downloading

When the application is downloading the new cache, the browser will set the status to downloading.

4

updateready

When the new cache is downloaded, and ready to be swapped, the browser sets the status to updateready.

5

obsolete

When the manifest file can't be found, the browser sets the status to obsolete.

You can quickly visualize the status changes using the setInterval function.

setInterval(function () {
      console.log(window.applicationCache.status)
}, 500);

Handling events

Next to checking the cache status, we can also handle specific events.

Event

Description

checking

This event is fired when the browser is checking if there's an updated manifest file. This is always the first event that gets fired.

downloading

This event is fired when the browser starts to download new resources.

cached

This event is fired when all the resources have been downloaded, and are committed to the cache.

error

This event is fired when something is wrong with the application cache mechanism. This could mean that the manifest file can't be found, or that any of the resources defined in the manifest file can't be found. It could also mean that the offline cache browser quota is exceeded. In general, this event is fired on every fatal error.

noupdate

This event is fired after the first download of the manifest file.

progress

This event is fired for each resource that gets downloaded for the application cache.

updateready

This event is fired when new resources are downloaded and the new cache is ready to be swapped with the old one.

obsolete

This event is fired when the manifest can't be found.

Swapping cache

When a new cache is downloaded, it isn't replaced with the old one automatically. The application won't use the new cache until we tell it to. We should handle the updateready event, and use the swapCache function to replace the old cache with the new one. These changes will only be visible after a page refresh.

window.applicationCache.onupdateready = function(){
      window.applicationCache.swapCache();
});

How do I let a user know my application can run offline?

Not one browser I know of gives the user an indication when an application is available offline. We can do this ourselves though; by making use of the application cache specific events to inform the user when an application is ready to be used offline. We can even inform the user of each step in the application cache process lifecycle.

Handling these events is pretty straightforward. One particularly useful event is the progress event. This event fires each time a resource is downloaded and contains three useful attributes we can use to display the download progress: lengthComputable, loaded and total. Use the lengthComputable attribute to detect whether the loaded and total attributes are implemented. In the example below I used these two attributes to calculate the percentage of downloaded resources.

window.applicationCache.onchecking = function (e) {
    updateCacheStatus('Checking for a new version of the application.');
};

window.applicationCache.ondownloading = function (e) {
    updateCacheStatus('Downloading a new offline version of the application');
};

window.applicationCache.oncached = function (e) {
    updateCacheStatus('The application is available offline.');
};

window.applicationCache.onerror = function (e) {
    updateCacheStatus('Something went wrong while updating the offline version of the application. It will not be available offline.');
};

window.applicationCache.onupdateready = function (e) {
    window.applicationCache.swapCache();
    updateCacheStatus('The application was updated. Refresh for the changes to take place.');
};

window.applicationCache.onnoupdate = function (e) {
    updateCacheStatus('The application is also available offline.');
};

window.applicationCache.onobsolete = function (e) {
    updateCacheStatus('The application cannot be updated, no manifest file was found.');
};

window.applicationCache.onprogress = function (e) {
    var message = 'Downloading offline resources.. ';
    if (e.lengthComputable) {
        updateCacheStatus(message + Math.round(e.loaded / e.total * 100) + '%');
    } else {
        updateCacheStatus(message);
    };
};

How do I detect whether the browser is online or offline?

There are several reasons why you would want to know whether the browser is online or offline. You might want to inform the user that she is working offline, or maybe even disable certain features when there is no network connection. You also might think about supporting offline user input by falling back to local storage to queue messages, and synchronize them with the server when the browser goes back online. You can roll your own infrastructure for this, or you can have a look at open source or third party projects.

Checking online status

In theory this is simple enough; use the onLine property of the navigator singleton to check whether the browser is online or offline.

console.log(navigator.onLine)

In practice it's far from being this simple. Not all browsers have the same interpretation of what it means to be online or offline. For example, older versions of Firefox only update the onLine property when the user explicitly switches from or to the browser's offline mode, ignoring the actual network connectivity. Next to having inconsistent implementations, the problem of detecting network connectivity is not a trivial one. What do you expect the status to be when your machine is connected, but your router is down?

A popular hack is checking the HTTP statuscode of every AJAX request, and using an unsuccessful statuscode as a trigger to fall back to an offline mechanism.

Handling events

If you want to do something when the browser changes its online status, you can handle the offline and online event. But be warned, these events have the same constraints as manually checking the onLine property.

window.addEventListener('offline', function(e) {
  console.log('offline');
}, false);
window.addEventListener('online', function(e) {
  alert('online');
}, false);

Browser support

All mainstream modern browsers, except Internet Explorer, support offline web applications today. By the time Internet Explorer 10 gets released, it will have implemented the specifications as well. A complete overview of support per browser version can be found on caniuse.com.

Browser implementations seem to be fairly consistent. The storage quota and management of this quota, which are not defined in the specifications, seem to show the biggest discrepancies. Take this into consideration when testing your web applications. Mobile browsers seem to be very stingy when it comes to cache size.

Using ASP.NET MVC to generate and serve the manifest

Generating the manifest

There are several ways we can use ASP.NET MVC to build and serve the manifest file. The simplest way is letting ASP.NET MVC serve a static text file. However, if we want to use built-in ASP.NET MVC features for resolving routes, or if we want to manipulate the manifest in other ways using code, we are better off using a custom action result.

This custom action result, which I named ManifestResult, inherits from the existing FileResult class. The manifest should be served using the 'text/cache-manifest' MIME type. This can be passed on to the base constructor.

public class ManifestResult : FileResult
{
    public ManifestResult(string version)
        : base("text/cache-manifest") { }
}

The ManifestResult class has four properties; one for every section, and one for the version number. The CACHE and NETWORK section properties are just an IEnumerable of strings, while the FALLBACK section needs to be a dictionary to map resources to FALLBACK resources.

public class ManifestResult : FileResult
{
    public ManifestResult(string version)
        : base("text/cache-manifest")
    {
            Version = version;
        CacheResources = new List<string>();
        NetworkResources = new List<string>();
        FallbackResources = new Dictionary<string, string>();
    }
    public string Version { get; set; }
    public IEnumerable<string> CacheResources { get; set; }
    public IEnumerable<string> NetworkResources { get; set; }
    public Dictionary<string, string> FallbackResources { get; set; }
}

To write the formatted manifest to the response stream, the WriteFile method needs to be overridden.

protected override void WriteFile(HttpResponseBase response)
{
WriteManifestHeader(response);
    WriteCacheResources(response);
    WriteNetwork(response);
    WriteFallback(response);
}
private void WriteManifestHeader(HttpResponseBase response)
{
    response.Output.WriteLine("CACHE MANIFEST");
    response.Output.WriteLine("#V" + Version ?? string.Empty);
}
private void WriteCacheResources(HttpResponseBase response)
{
    response.Output.WriteLine("CACHE:");
    foreach (var cacheResource in CacheResources)
        response.Output.WriteLine(cacheResource);
}
private void WriteNetwork(HttpResponseBase response)
{
    response.Output.WriteLine();
    response.Output.WriteLine("NETWORK:");
    foreach (var networkResource in NetworkResources)
        response.Output.WriteLine(networkResource);
}
private void WriteFallback(HttpResponseBase response)
{
    response.Output.WriteLine();
    response.Output.WriteLine("FALLBACK:");
    foreach (var fallbackResource in FallbackResources)
        response.Output.WriteLine(fallbackResource.Key + " " + fallbackResource.Value);
}

Serving the manifest

To serve the manifest, we should add an action to an existing or new controller, which initializes and returns the manifest action result. In that action we can make use of MVC's UrlHelper to resolve the correct routes.

public ActionResult Manifest()
{
     var manifestResult = new ManifestResult("1.0")
    {
         CacheResources = new List<string>() 
             {
                   Url.Action("Index", "Home"),
                   "/content/style.css",
                   "/scripts/main.js"
             },
             NetworkResources = new string[] { Url.Action("Status", "Service")},
             FallbackResources = { { "/logo.png", "/logo_offline.png" } } 
    };
    return manifestResult;
}

Setting up a route for the manifest

We should set up a specific route for the manifest. While most browsers aren't very strict, I had the most reliable cross-browser behavior putting the manifest in the root and naming the file cache.manifest. On application start, I add this new 'cache.manifest' route to the route table.

routes.MapRoute("cache.manifest", "cache.manifest", new { controller = "Resources", action = "Manifest" });

Conclusion

Offline web applications are a great addition to the ever evolving HTML specifications. Depending on the usecase, you want to use this feature to just cache assets or to completely take web applications offline. At the heart of this feature lies the manifest file. The format and requirements of the manifest are far from complex, making it straightforward to generate and serve using ASP.NET MVC or any other server stack. Once the manifest file is taken care of, an obsolete cache can be swapped easily using the applicationCache API. Also make use of this API to query the cache status or to handle application cache specific events. To know whether the browser is online or offline, you can use the onLine property of the navigator object, or handle the specific online and offline events.

About the Author

Jef Claes has a passion for building things. At his day job, he is a software engineer working for Euricom, building software for the enterprise on the Microsoft stack. In his spare time, he loves hacking on random ideas or innovative solutions. Next to that, Jef regularly writes about things that interest him on his blog. He also doesn't shy away from speaking  and has talked about the web on a few local events. You can get in touch with him through Twitter.

 

 

Hello stranger!

You need to Register an InfoQ account or to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Tell us what you think

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

good by yang Aries

it's very good.

Bundling Problem by Ramon Cruz

Hi Sir. It's a very Good Article.

I implement your article in 1 of my project but unfortunately I was not able to make it work. I don't know if this is because of bundling.

I post question in stackoverflow
stackoverflow.com/questions/22371304/bundling-w...

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Email me replies to any of my messages in this thread

2 Discuss

Educational Content

General Feedback
Bugs
Advertising
Editorial
InfoQ.com and all content copyright © 2006-2013 C4Media Inc. InfoQ.com hosted at Contegix, the best ISP we've ever worked with.
Privacy policy
BT