Thursday, May 8, 2014

HTML5 AppCache with MVC bundles


HTML5 has many handy additions and I recently had the opportunity to use the Appcache Manifest. This feature enables your site to work offline which is particularly useful for mobile orientated sites as users may not always have a reliable 'net connection.

The following guide goes through the approach I took for getting it working via .Net's MVC framework.

Prep the HTML tag

On the page you want to have a manifest for, add the following:

<html lang="en" manifest="/AppCache/AppManifest">

This will tell the browser to look for the appcache manifest.

Setup the Model

We don't need many properties on our Model for the manifest generation. The following will do
public class AppCacheModel
    {
        public string AssemblyVersion { get; set; }
        public List< string> CacheCollection { get; set; }
    }
  • An assembly version which will help update the manifest file contents after every build
  • A CacheCollection list of urls to add to the cache

Add a view

You will need to set various headers to get the manifest file to work correctly. In particular, set the MIME content type to "text/cache-manifest" and set the cache headers so that the manifest never caches.
@model MyWebsite.Models. AppCacheModel
@{
    Layout = null;
    Response.ContentType = "text/cache-manifest";
    Response.Cache.SetCacheability( HttpCacheability.NoCache);
    Response.Cache.SetExpires( DateTime.MinValue);
}CACHE MANIFEST
# Server Assembly Version: @ Model.AssemblyVersion
NETWORK:
*
CACHE:
@foreach (string cacheItem in Model.CacheCollection)
{
    @ cacheItem
}

Instead of hard-coding the individual assets in the Cache section, we are going to get the controller to add them in for us...

Code up the Controller

The controller populates the model with:
  • the assembly version
  • the links to the various assets to be cached. 
The code handles MVC Bundles and also generates a list of asset urls from target folders. If the assets are in a CDN bucket somewhere, you can also provide the link to the CDN.

public class AppCacheController : Controller
    {
        public ActionResult AppManifest()
        {
            // Build the model
            var model = new AppCacheModel();

            model.AssemblyVersion = GetType().Assembly.GetName().Version.ToString();
            model.CacheCollection = new List< string>();
            model.CacheCollection.Add(WriteBundle( "~/bundles/jquery"));
            model.CacheCollection.Add(WriteBundle( "~/bundles/site"));
            model.CacheCollection.Add(GetPhysicalFilesToCache( "~/images", "*.jpg" , "http://cdn.eoinclayton.net/website" ));
            model.CacheCollection.Add(GetPhysicalFilesToCache( "~/images", "*.png" , "http://cdn.eoinclayton.net/website" ));
            model.CacheCollection.Add(GetPhysicalFilesToCache( "~/css", "style*.css", string .Empty));

            return View(model);
        }

        private string WriteBundle( string virtualPath)
        {
            var bundleString = new StringBuilder();
            bundleString.AppendLine( Scripts.Url(virtualPath).ToString());
            return bundleString.ToString();
        }

        private string GetPhysicalFilesToCache( string relativeFolderToAssets, string fileTypes, string cdnBucket)
        {
            var outputString = new StringBuilder();
            var folder = new DirectoryInfo(Server.MapPath(relativeFolderToAssets));
            foreach ( FileInfo file in folder.GetFiles(fileTypes))
            {
                string location = ! String.IsNullOrEmpty(cdnBucket) ? cdnBucket : relativeFolderToAssets;
                string outputFileName = (location + "/" + file).Replace("~" , string.Empty);
                outputString.AppendLine(outputFileName);
            }
            return outputString.ToString();
        }
    }


How to test

1. Open your app/site in Firefox
2. Once it is loaded, close down Firefox
3. Re-open and set Firefox to Offline mode (File -> Offline)
4. Hit your app/site. Even though you are offline, it should still work

You can follow a similar approach on your device, just use Airplane mode instead of Offline mode.

Things to watch out for

  1. Do NOT cache the manifest file! Just don't do it. If you do, your site/app may never update for your clients...!
  2. Refrain from using the "No-Store" response header on the manifest file. The offline site continued to work as expected in Chrome, but in Firefox the entire site stopped working offline...
  3. Use Chrome's Javascript Console (Tools -> Javascript Console) to see if the manifest is being loaded correctly. It will give a breakdown for each item being cached and will tell you, for example, if you try to cache a non-existing asset
  4. To deactivate the appcache manifest, just removing it from the HTML tag is not enough. My site stopped updating entirely when I did this. You will also need to remove the manifest url/view.
  5. You do have a additional bucket to store data known as the 'Local Storage' (http://diveintohtml5.info/storage.html)
  6. There are also optional Javascript events to listen to. This handy Stackoverflow post goes through what the different events do: http://stackoverflow.com/a/20045644