Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML5

HTML App Cache in action, create local copies of webpages

4.94/5 (28 votes)
15 Jan 2015CPOL28 min read 44.6K   833  
An MVC application to make a copy of CodeProject articles in your Browser Cache using HTML5's AppCache feature

Introduction

This article is 9th in the series of 12 articles that will take us through learning HTML5 and CSS3.

In this article we will specifically be learning about HTML5’s App Cache features and how to use them.

To demonstrate the usage of App Cache I have created a simple MVC app that lists the top 10 CodeProject articles and caches them enabling us to peruse them even without a network connection.

The reason to choose this particular example is that I own a Wi-Fi only iPad and I can only wish I could Cache the articles I would like to read on the move. Until now I had to print the article to a PDF and read them. When I read about App Cache the first thing that came to my mind was “Let’s Cache Code Project” :)

This article would introduce the below concepts and use them in our project alongside.

What is Application Cache?

Application cache is a feature of HTML5 wherein users will be able to view some or all of web pages of a site even without an active network connection.

Web Apps are built to suit different set of users and scenarios. And with mobile technology becoming becomes one of the most used means of accessing information on the web, we face a new scenario of Network Availability. To access the information we need an active network connection without which access to the content online would be impossible. Application cache helps us deal with the scenario by allowing us to make some (or all if you wish) of the content available to the user offline without an active network connection.

The types of interaction a user does with a Web Application can broadly be classified into a Get/Fetch operation or a Set/Post operation.

Most of the Web Apps will have a combination of these operations and some are specifically Get/Fetch sites.

Get/Fetch Applications:

Fetch applications are those where the user is only looking for information based on some clicks and selections. There will not be any information sent to the Server for storage. Most of the times these pages are static and if not, they are interacting with the user using Javascript or CSS loaded on the client side.

Some example applications would include A Photo Gallery (without like and comments functionality), Event information sites, Codeproject articles etc.,

Set/Post Applications:

Set/Post applications are those where the user is submitting information on a Web Application that needs to be stored on the server or sent over to another server. These applications will have frequent interactions with the user and server to present information from server and capture the information from the user.

A simple and familiar example would be writing and submitting an article on Code project site or answering a question on the same. Here the information entered by the end user is stored and maintained on Code Project servers and is sent back to browsers whenever anyone wants to view it.

Why would you use App Cache? Where is it useful?

App Cache in HTML5 helps us making the Get/Fetch web application available for offline browsing. Using App Cache we could cache the necessary files locally on the user’s machine and when the network becomes unavailable the browser would automatically switch over to the stored copies and load them for display.

The Set/Post applications will need to store the information entered by the user locally and then send it to the server when network becomes available, this can be achieved easily with another new HTML5 feature called Web Storage which we will discuss in our next article.

Image 1

What does a browser need to present a typical Get/Fetch Web Application?

A Web Application meant to serve static information to the user mainly consist of the below items

  • HTML/CSHTML/ASPX etc., : These are the files which hold the layout information and sometimes dynamic validations on what the layout should look like
  • CSS: These are the styles which contain the information of how the Layout should be formatted and presented
  • Images: Any images that are on the web page served from the server (not external links)
  • Javascripts: The scripts necessary to provide a client side functionality, for example a script which supports a game by drawing on a canvas based on user mouse and keyboard inputs.

If we could store these locally then there is no need for the browser to contact the server to present this information unless there was an update to the information presented.

An application can have multiple advantages using App Cache. Below are some of them.

  1. Speed: If there are images (especially Large ones) on your site which don’t change very often then you could just Cache them so that your page loads much faster. Similarly caching resources that don’t change often can help improve the speed at which your application loads and becomes available for the user because now those images and resources comes from the local disk instead of the remote server.
  2. Offline Browsing: As discussed earlier you could cache the necessary files and make your web application available offline. This could be done to the whole site or specific pages depending on the requirements. The user can now access the information he needs without having to wait for an active network connection(internet)
  3. Fall Back plan: Caching helps retain access to important information even when our Web Servers go down due to technical glitches (or any other reason). Say , something went wrong and our webserver went down for a longer time than tolerable, then users will have no way to get the information they need on time. This could become highly inconvenient if the information is crucial. For instance one of your webpage is a confirmation page for a movie ticket and the user did not get a print out. If your web server becomes unavailable just before user needs to present or print ticket then its sure to make users unhappy :) . Or a web page serving driving directions. In the above cases if the user had access to the information before the server went down and App cache was enabled, then he will be able to access the info even when the server is not available.

What Browsers support HTML5 App Cache feature ?

Most modern browsers support App Caching.

Below is a table for a quick reference on what versions of browsers started supporting the App Cache.

Browser Earliest Supported Version
FireFox 3.5
Firefox for Android 26.0
Chrome Desktop 4.0
Safari Desktop 4.0
Opera 10.6
Internet Explorer 10.0 *Don’t believe me? Click here
Android 2.3
Chrome for Mobile 33.0
Opera Mini Not supported yet
Safari iOS 3.2

How much data can the App cache hold?

Each browser has a different quota assigned to this feature. Below is a table with the information available as of today.

Browser Limit
Safari Unlimited
Safari Mobile 10 MB
Chrome 5 MB
Android Browser Unlimited
Firefox Desktop Unlimited
Opera Default= 50 MB, but can be managed by user

Hopefully soon we might have uniformity regardless of browser type and version.

Is App Caching same as Browser/HTTP Caching?

No, Browser/HTTP caching and App caching are very different in terms of their intended use and how they work. Browser caching was introduced to improve your Web Apps performance by caching the resources on the client side with a expiry date which would reduce the roundtrips to server to fetch them each time. This was typically used to cache the CSS, Javascripts and images that changed less often. Also, even though the resources were cached the user couldn’t use the page offline.

To enable Browser Caching we had to add HTTP headers with expiry dates on each file. Depending on these expiry dates the browser would go back to server for the latest/fresh version of the resource. As mentioned earlier this was a technique aimed at improving the site performance and not providing offline capability. It’s also common to set the expiry date of these Browser Cached files relatively longer.

On the other hand, App Caching isn’t intended to cache the resources for performance improvement(at least not the primary intention). This is used to make a part of your Web App available offline.

We also need to be careful here because these two features together can cause unintended problems or yield unexpected results.

For instance a page configured to be available offline using App Cache also has a header to be cached for a week. Now if you change your manifest to force a new version to be cached, this page which had an expiry of a week will not be cached until next week. It’s always recommended to disable HTTP caching on pages using the App Caching.

Let’s Cache-In

Now that we know App Cache is the new super hero on the block, let’s see how to really implement/use App Cache in our applications.

To understand the concepts and uses of App Cache we are going to create a MVC application which would list the top articles of the day from Code project and display the article content when clicked on the links. Our objective would be to Cache these articles for offline viewing. This way we can see the App Cache in action. I am also throwing in the games we previously developed to make sure you have something to play with when you want to take a break from reading articles offline :)

Our Project consists of the below pages

  • Index – This page is the Index of all the top articles
  • Article – The page which displays the article content
  • Balloon_Bob – Page for the game Balloon_Bob
  • Bobby Carter – Page for the game Bobby_Carter

To support the pages we will also have some Javascripts, CSS and image files.

Let’s get our project ready before we get into the next part of this article. Below are some simple steps to get started, or you could just download the solution attached here and review the files as we go along.

Creating a Sample App:

Create a MVC empty project and add 4 views namely Index.cshtml, Article.cshtml, Balloon_Bob.cshtml and Bobby_Carter.cshtml.

Create a Home controller and create actions for all these 4 views as below. Our actions will just return the views.

C#
public ActionResult Index()
{
    return View();
}
public ActionResult Article()
{
    return View();
}
public ActionResult Balloon_Bob()
{
    return View();
}
public ActionResult Bobby_Carter()
{
    return View();
}

I am not adding the view code to keep the length of this article short but the same have been made available above for download.

We shall also add some images, javascripts and css to support these views which are also part of the download package.

In our Index view lets read the RSS of code project top articles exposed at http://www.codeproject.com/WebServices/ArticleRSS.aspx and list them in a table using SyndicationFeed object.

The table will contain the Title which will be a link to the Article view with some querystring and the name of the Author.

Below is the code in the view and how it would look on the page.

ASP.NET
<div style="margin: auto;">
    <article class="blog-post">
        <div>
            @using System.Xml;
            @using System.ServiceModel.Syndication;
            @{
                var feed = SyndicationFeed.Load(new XmlTextReader("http://www.codeproject.com/WebServices/ArticleRSS.aspx"));
            }
            <table class="results">
                <tr>
                    <th>Article
                    </th>
                    <th>Author</th>
                </tr>
                @foreach (var item in feed.Items)
                {
                    <tr>
                        <td><a href="/Home/Article?article=@item.Id&Author=@item.Authors[0].Email.ToString()&Title=@item.Title.Text.ToString()">@item.Title.Text.ToString()</a></td>
                        <td>@item.Authors[0].Email.ToString()</td>
                    </tr>
                }
            </table>
        </div>
    </article>
</div>

Screenshot :

Image 2

Let’s also update the Article view to take the querystring, get the article content and display the same on the page.

Here we are going to use HtmlWeb object to read the html content of the article page and use the HTML Agility pack to update the HTML source to render the same on our page which is at least NOT Ugly if not beautiful like on the native CodeProject page.

Below is the code for the same and also a GIF image to show how the article would appear upon clicking the link.

ASP.NET
<div style="margin: auto;">
    <article class="blog-post">
        <div>
            @using System.Xml;
            @using HtmlAgilityPack;
            @{
                HtmlWeb hw = new HtmlWeb();
                HtmlDocument doc = new HtmlDocument();
                try
                {
                <h2 style="color: #f90;">@Request["Title"]</h2>
                <h3>Author : @Request["Author"]</h3>
                    doc = hw.Load(@Request["article"]);
                    HtmlNode node = doc.DocumentNode.SelectSingleNode("//div[@id='contentdiv']");
                    var nodesToRemove = node
                    .SelectNodes("//img")
                    .ToList();
                    foreach (var node1 in nodesToRemove)
                    {
                        var varAtt = node1.Attributes["src"].Value;
                        node1.SetAttributeValue("src", "http://www.codeproject.com" + node1.Attributes["src"].Value);
                        node1.Remove();
                    }
                @Html.Raw(@node.InnerHtml.ToString());
                }
                catch (Exception ex)
                {
                <h2>Invalid Article Link</h2>
                }
            }
        </div>
    </article>
</div>

Screenshot :

Image 3

As expected the page would not be loaded when we are not able to connect to the network. To test this scenario, instead of turning your network connection off let’s use fiddler which will be much easier and better way to test this.

To use fiddler to test this you will need to just configure your autoresponder to clock access within our application by creating a simple rule. Below is a GIF showing step by step on how to configure your Fiddler.

Image 4

In the above steps we created a new rule to say if there is a request with a URL which contains localhost:3701 then send a response back by closing the TCP/IP connection. Using * drop will close the connection and if *reset was used then it would kill the connection using TCP/IP RST.

The basics of creating a Cache Manifest

To enable Application Caching for our web application we need to specify what files will need to be cached so that the page would be rendered without having to wait for the server to serve the resources. This will be the Browsers instruction manual for the site.

The cache manifest is a text file with *.APPCACHE extension that lists all the resources that needs to be cache to enable offline access when network is unavailable, the resources that should not be cached because these should always come from the server, some comments and a Fall Back page pointer which tells the browser what to when the user reaches for an uncached (this seems to be made up wordJ) page.

By now you might have realized that the easiest and best way to create this file would be to open up your notepad and start typing. Well, I couldn’t agree more.

But when creating this file we have to be a little careful as the browser reads these files as case sensitive and the file names and paths have to be exactly as they are on the server.

I found an interesting tool to create this file or at least validate our manifest after creation.

It’s called manifestR, it is a bookmarklet that can be dragged to your bookmarks bar and when you load up your page you can just click on the bookmarklet and it will create a manifest file for you. You can then just copy paste the text it generated into a file.

However you will need to be careful because this bookmarklet lists almost all the resources referenced in your page and you may not need all of them so a little fine tuning would be required. Below is what it lists.

  • Looks for all the image elements in the page irrespective of the fact that they reside on the same domain or not.
  • Links to all pages in the same domain
  • Stylesheets referenced
  • Scripts referenced

Once the above details are collected it will put together a quick manifest file in the right format with CACHE section. You will then need to list the FALLBACK and NETWORK section.

Below is what I got when I did this on the CodeProject Homepage.

Image 5

The Manifest File (Cache Manifest, Cache, Network, Fallback)

This manifest file is the master file that dictates when, what and what not to cache hence the manifest file itself should never be cached. Let’s look at what this file consist of.

Image 6

As explained in the Image itself this file consists of five sections as below

  • CACHE MANIFEST: This has to be the First Line of the file to tell the browser that this is a manifest file. This is mandatory
  • COMMENTS : Comments are prefixed with # character and are optional in the file. These are usually used to mention the version and dates
  • CACHE : These are the urls of the files that need to be cached by the browser.
  • FALLBACK : This is the name of the file that will be presented when the user tries to reach a uncached page. The page mentioned here will be cached.
  • NETWORK: An explicit whitelist that tells the browser what resources should not be cached and should always be requested from the server.

​CACHE:

This section is also referred as Explicit Section and is the default section in the file meaning any section without a heading would be treated as the CACHE section.

We list all the files with their path on server that are to be cached in order to support the intended offline functionality. This list can include external resources as well.

All the downloaded resources would be used in place of their online counterparts on the next load of this page in a browser. You may not list your current page to be cached explicitly because the browser caches the page implicitly which contains the manifest attribute in their html tag.

However, if you need other html or aspx files to be cached then you will have to list them explicitly.

Example:

CACHE MANIFEST

CACHE:

/css/siteCSS.css
/css/common.css
/js/index.js
/js/Bobby_Carter.js
/images/logo.png
/images/banner.jpg
http://s.codeproject.com/App_Themes/CodeProject/Img/logo250x135.gif

In our above example we have the CACHE Manifest as our first line to notify the browser that this is a manifest file and after that we have the CACHE section this could have been omitted as the section without a header will be considered CACHE by default.

We have 7 paths specified above to be downloaded and Cached and there is also an external image from code project site among them which will also be downloaded and cached.

The file names here have to be exactly same as they are on the server meaning the names should be case sensitive. Hence, it is advised to populate this file programmatically to avoid mistakes. Also every file that needs to be cached has to be listed explicitly and no shortcuts using wildcards like js/*.js is allowed.

FALLBACK:

The Fallback section always contains a pair of paths in each line. The first path will be the online version of the file and the second path is the fallback page which will be presented if the online page is not reachable.

Example:

CACHE MANIFEST

FALLBACK:
/Index.cshtml     /offline.cshtml
/Article.cshtml   /noArticle.cshtml

Here we are telling the browser that if the user tries to reach the page Index.cs and if it was not cached earlier then present him with the offline.cshtml page. And present the noArticle.cshtml page if the user tries to reach Article.cshtml which was not cached. The two paths have to be separated with a space.

Alternately we could configure a global fallback page for all other resources that are not mentioned here by starting with a backslash and a space before the global Fallback page path. Below is an example

Example:

CACHE MANIFEST

FALLBACK:

/Index.cshtml     /offline.cshtml 
/Article.cshtml   /noArticle.cshtml 
/   /globalOffline.cshtml

The Fallback directive is meant for all types of resources and not only pages. For instance you could assign a fallback image when the orginal image was not cached.

Another way of defining FallBack is to define fallback resource per directory. Any resource requested under the specified directory when uncached will result in the fallback resource.

Below is an example of serving a default image when any of the images under Image directory were not cached.

Example:

CACHE MANIFEST

FALLBACK:

/Index.cshtml     /offline.cshtml

/Article.cshtml   /noArticle.cshtml

/   /globalOffline.cshtml

/images/     /imageUnavailable.png

NETWORK:

As mentioned earlier the network section is simply a whitelist of all pages that should not be cached. We can give a specific list of files that should explicitly be requested from the server like below example.

CACHE MANIFEST

NETWORK:

/Events.cshtml

/statistics.svc

Or like the fallback you could restrict a whole directory of resources from being cached like below

CACHE MANIFEST

NETWORK:

/statisticImages/

Or a very common way is to just provide an asterisk which would mean that any resource that was not explicitly list to be cached should be requested from the server

CACHE MANIFEST

NETWORK:
*

Linking and Serving the Manifest File:

As we saw earlier a manifest file needs to be created and located on the server for the browser to request the same. So we could create a manifest file with .appcache in our root directory and link the same.

However, since we may want to control what files to be cached and what shouldn’t be, we can programmatically create the file content and serve the same when requested.

Serving the Manifest file:

Ours is a MVC application so let’s create an action to serve the Manifest file content when requested.

I will add a loop to include all the scripts and css in the site but in real world we should always make sure we load only the resources we would need for that page.

C#
public ActionResult Manifest()
{
    Response.ContentType = "text/cache-manifest";
    Response.ContentEncoding = System.Text.Encoding.UTF8;
    Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache);

    var manifest = "CACHE MANIFEST" + Environment.NewLine +
    "CACHE:" + Environment.NewLine+
"NETWORK:" + Environment.NewLine +
    "*" + Environment.NewLine 
;

    string startupPath = System.AppDomain.CurrentDomain.BaseDirectory;
    string[] JSfiles = System.IO.Directory.GetFiles(startupPath, "*.js",SearchOption.AllDirectories);
    string[] CSSfiles = System.IO.Directory.GetFiles(startupPath, "*.css",SearchOption.AllDirectories);

    for(int i=0;i<JSfiles.Length;i++)
    {

        manifest = manifest + "\\" + Url.Content(JSfiles[i].ToString().Replace(startupPath, "")) +
            Environment.NewLine;
                
    }
        for(int j=0;j<CSSfiles.Length;j++)
    {
        manifest = manifest + "\\" + Url.Content(CSSfiles[j].ToString().Replace(startupPath, "")) +
            Environment.NewLine;
    }

    return Content(manifest, "text/cache-manifest");
}

Looking into the code above you will see that we are setting content type to text/cache-manifest. That’s because a manifest file can have any extension (*.appcache is the recommended one) but it should always be served with the mime-type “text/cache-manifest”

If you are going to serve this from a file on the server then we may need to add a custom file type on the webserver.

For instance in IIS you can set the mime-type like shown in the below GIF image.

Image 7

Google App engine the app.yaml file will need the below lines added.

- url: /mystaticdir/(.*\.appcache)
  static_files: mystaticdir/\1
  mime_type: text/cache-manifest
  upload: mystaticdir/(.*\.appcache)

And in Apache the below needs to be added to the config file.

AddType text/cache-manifest .appcache

In our code we are managing this by setting the content type explicitly to the right mime-type

Response.ContentType = "text/cache-manifest";

After this we are also setting the encoding to UTF 8 as below.

Response.ContentEncoding = System.Text.Encoding.UTF8;

One other important point is to set the Cachebility of this file to be NoCache because as we discussed earlier the manifest file itself should never be cached because if there is no change in the manifest then your cached resources will never see the light of the internet :) We will see this in detail with an example.

Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache);

Linking the Manifest file:

Once this action is ready all we need to do is to link the action url in the html tag of our pages like below. Let’s add this to our Index Page only for now.

<html manifest="/Home/Manifest">

And if you were serving it from a file then you could give the path of the file in place of the action link. Below is an example

<html manifest="~/Content/Manifests/Manifest.appcache">

And that’s it, you have enabled your App Cache on the Index Page.

Let’s see what happens when we access our web page now. Remember that we haven’t enabled app cache for any of the other web pages including the Article. Below is a GIF showing the result

Image 8

As you can see in the above example, the home page loaded even without a connection whereas the Article page did not.

That’s because we only enable app cache on Index and not the Article page. Now you can go ahead and enable app cache on all our pages.

Flow of App Cache events

We now know that the App Cache helps with providing offline browsing capability. Let us now see what really goes on under the hood, how browsers make a decision and what the events are that gets triggered in the process.

Window.ApplicationCache is the object that gets created by the browser when a webApp with App Cache enabled is loaded. And as and when the browser works on caching or updating the site resource events are triggered to this object.

To keep this short and simple lets list out all the events that are triggered and go through them quickly.

Events:

Event Next Events in the flow
Checking
  • [noupdate] or
  • [downloading] or
  • [obsolete] or
  • [error]
Downloading
  • [progress] or
  • [error] or
  • [cached] or
  • [updateready]
Progress
  • [progress] or
  • [error] or
  • [cached] or
  • [updateready]
Cached Last Event
UpdateReady
NoUpdate
Obsolete
Error
Online NA
Offline NA

An important thing to remember is that the browser loads up the cached version of the page even before requesting the manifest and updating the Cache.

Checking is the first event that gets triggered when the browser notices that the App Cache is enabled on a page by looking at the manifest attribute in the html attribute of the page. This even is triggered regardless of the page was cached earlier or not.

Once the manifest file is acquired from the server the browser compares the content of the received file with its cached counterpart, if any change is detected then the Downloading event is triggered. The Downloading event is triggered even when the manifest is being received the first time.

Usually when we change some of the code and redeploy our page the manifest itself is not changed as it contains only the file names. Hence, whenever we need the browser to cache the latest version we should include a comment line in the manifest file that would change every time we have a new build.

In our example for testing purpose lets include a version number which is current Datetime(YYYYMMDDHHmm), this will force the browser to refresh the cache every minute.

C#
public ActionResult Manifest()
{
    string version = DateTime.Now.ToString("yyyyMMddHHmm");
    Response.ContentType = "text/cache-manifest";
    Response.ContentEncoding = System.Text.Encoding.UTF8;
    Response.Cache.SetCacheability(System.Web.HttpCacheability.NoCache);

    var manifest = "CACHE MANIFEST" + Environment.NewLine +
    "# Version: " + version + Environment.NewLine +
    "CACHE:" + Environment.NewLine;

    string startupPath = System.AppDomain.CurrentDomain.BaseDirectory;
    string[] JSfiles = System.IO.Directory.GetFiles(startupPath, "*.js",SearchOption.AllDirectories);
    string[] CSSfiles = System.IO.Directory.GetFiles(startupPath, "*.css",SearchOption.AllDirectories);

    for(int i=0;i<JSfiles.Length;i++)
    {

        manifest = manifest + "\\" + Url.Content(JSfiles[i].ToString().Replace(startupPath, "")) +
            Environment.NewLine;
                
    }

        for(int j=0;j<CSSfiles.Length;j++)
    {
        manifest = manifest + "\\" + Url.Content(CSSfiles[j].ToString().Replace(startupPath, "")) +
            Environment.NewLine;
    }

        manifest = manifest + "#File_Count=" + (JSfiles.Length+CSSfiles.Length) + Environment.NewLine +
       "NETWORK:" + Environment.NewLine +
       "*" + Environment.NewLine;

    return Content(manifest, "text/cache-manifest");
}

During the download, a Progress event is triggered on every download completion of a resource. Let’s say we are caching 20 files then the browser would trigger 20 progress events, each one after a successful download. The progress event’s argument has attributes which can help us track the progress of how many files have been loaded and how many are in total, the event.loaded and event.total attributes.

Now if there was an issue during the download of any of these files then an error event is triggered and none of the resources in the cache are updated.

Once the download of all the resources is complete then one of two events is triggered based on the state of the cache before the download started.

Cached event: This event gets triggered when the page was being cached for the first time and no cache for this page existed before the download started

UpdateReady event: This event gets triggered when the file was previously cached and a new version of the resources was just downloaded. It is in this event we will have to handle the swapping of old cache with the new cache. To do this the ApplicationCache object provides us with a method called appCache.swapCache(). *Note: Some browsers don’t need this method to be called explicitly and swapping happen everytime a download is completed successfully.

Also remember that once the cache is updated the page refresh is not automatic, so we will have to provide the user with a message to refresh the page manually to get the latest or we need to do it programmatically inside the UpdateReady event using scripting.

After the checking event if the browser finds out that the manifest received now is exactly same as the previous one then a NoUpdate event is fired. And if the manifest file wasn’t found on the server an Obsolete event is fired.

AppCache also triggers an event (Online Event and Offline Event) whenever the network changes its state from Online to Offline or vice versa. The event name will be the target status. For instance, when changing from Online to Offline, it would trigger an offline event.

Below is a diagram which shows the flow of these events.

Image 9

Apart from the events we can also access the STATUS attribute of the ApplicationCache object to know latest status.

Below are the Status Codes and meaning.

Status Code Description
0 - Uncached The applicationCache object’s cache host is not associated with a cache, this is the state of a uncached page just before checking event is triggered for the first time.
1 – Idle The current associated applicationcache is the latest
2 - Checking The manifest is currently being requested and compared
3 – Downloading Downloading of resources in progress
4 - UpdateReady A new version of the resource is available is ready and is awaiting a swap. Once the swap is completed the status will changes to Idle.
5 – Obsolete The manifest file was not available for download and the application cache is now marked as Obsolete.

Let’s track these events and status on our webpage and enable manual Cache update using a button.

Step1: Add a progress bar to display the progress of download event

ASP.NET
<div style="text-align: center;margin-left:5%;margin-right:5%">

            <span id="progText" style="width: 80%; color: orangered"></span>
            <progress id="progressBar" max="100" value="0"></progress>

        </div>

Step2: Lets add a button to enable manual Update

ASP.NET
<div style="text-align: center;">
     <span style="display: inline-block;">
         <input type="button" id="manualUpdate" value="Update Cache" style="font-weight:bold;border-radius: 25px;padding-top:2px;margin:5px;padding-bottom:2px;" />
     </span>

Step3: Add some spans for Network status and Application Cache Status. We will have two css classes called eventNotTriggered and eventTriggered which will have the background color set to Gray and Green respectively.

ASP.NET
<span class="eventNotTriggered" id="networkStatus"></span>

<span class="Status" id="Status"></span>

Step4: Add 5 spans representing different events, and whenever an event is triggered we will change their color to green, this will give a quick reference as what events were fired during that particular update.

ASP.NET
   <span class="eventNotTriggered" id="CHECKING">Checking</span>
    <span class="eventNotTriggered" id="DOWNLOADING">Downloading</span>
    <span class="eventNotTriggered" id="UPDATEREADY">UpdateReady</span>
    <span class="eventNotTriggered" id="noupdate">noupdate</span>
    <span class="eventNotTriggered" id="progress">Progress</span>
</div>

Step5: Apart from these lets also add an area where we can log events as they are triggered so we will have a running text of what happened when.

ASP.NET
<div style="float: right; width: 50%; height: 500px; overflow: hidden">
<div style="overflow: auto; height: 490px">
<ul id="applicationEvents" style="color: black;">
<!-- List items will be created dynalically using  Javascript. -->
</ul>
</div>
</div>

Step6: Add a Javascript file called AppcacheEvents.js to the project and create a method which runs every 10 milliseconds, polls for the AppCache Status and updates it on one of the spans created above.

JavaScript
$(document).ready(function () {

    var appStatus = $("#applicationStatus");
    var appEvents = $("#applicationEvents");
    var manualUpdate = $("#manualUpdate");
    var progBar = $("#progressBar");
    var progText = $("#progText");
    var Status = $("#Status");
    var appCache = window.applicationCache;
    var timer = setInterval(updateStatus, 10);

    function updateStatus() {
        var txt = "Status : Code( " + appCache.status + " )";
        switch (appCache.status) {
            case 0:
                txt = txt + " : Uncached";
                break;
            case 1:
                txt = txt + " : Idle";
                break;
            case 2:
                txt = txt + " : Checking";
                break;
            case 3:
                txt = txt + " : Downloading";
                break;
            case 4:
                txt = txt + " : UpdateReady";
                break;
            case 5:
                txt = txt + " : Obsolete";
                break;
            default:
                txt = txt + " : Unknown";
                break;

        }
        Status.text(txt);

    }
})

Step7: Add a method called LogEvent to add List items dynamically for the UL we created above.

JavaScript
function logEvent(event) {
        appEvents.prepend(
            "<li>" +
                ((new Date()).toLocaleTimeString()) + ":Curr_status:" + appCache.status + ": " + event + "     " +
            "</li>"
        );
    }

Step8: Add click event method to Update the Cache manually whenever the button is clicked.

JavaScript
// Update button Click
    manualUpdate.click(
        function (event) {
            $(".eventTriggered").addClass("eventNotTriggered");
            $(".eventTriggered").removeClass("eventTriggered");
            progBar.val(0);
            progText.text("");
            event.preventDefault();
            // Status of the network.
            appStatus.text(navigator.onLine ? "Online" : "Offline");
            appStatus.addClass(navigator.onLine ? "eventTriggered" : "eventNotTriggered");
            appStatus.removeClass(navigator.onLine ? "eventNotTriggered" : "eventTriggered");
            // Manually update the Cache.
            appCache.update();
        }
    );

Step9: Add event Listeners for the Online/Offline, Checking and Downloading events, we will only Log the events and change the CSS class for those spans designated for these events.

JavaScript
appCache.addEventListener('online offline', function (event) {
    appStatus.text(navigator.onLine ? "Online" : "Offline");
    appStatus.addClass(navigator.onLine ? "eventTriggered" : "eventNotTriggered");
    appStatus.removeClass(navigator.onLine ? "eventNotTriggered" : "eventTriggered");
    logEvent("<span class='event'>" + event.type + "</span>" + " : " + "Network Status Changed");
}, false);

appCache.addEventListener('checking', function (event) {
    logEvent("<span class='event'>" + event.type + "</span>" + " : " + "Checking for manifest");
    $("#CHECKING").addClass("eventTriggered");
    $("#CHECKING").removeClass("eventNotTriggered");
}, false);

appCache.addEventListener('downloading', function (e) {


    logEvent("<span class='event'>" + e.type + "</span>" + " : " + "Downloading cache");

    $("#DOWNLOADING").addClass("eventTriggered");
    $("#DOWNLOADING").removeClass("eventNotTriggered");
}, false);

Step10: Add the event listener for the progress event and since will have access to the Loaded and Total files attribute we will also track the progress by changing the progress bar value and text. And repeat the steps of logging and Class changing.

JavaScript
appCache.addEventListener('progress', function (e) {
    logEvent("<span class='event'>" + e.type + "</span>" + " : " + "File "+e.loaded+" downloaded");
    progBar.val((e.loaded / e.total) * 100);
    progText.text("Total files : " + e.total + " - Downloaded Files : " + e.loaded + " - Progress : " + Math.round(((e.loaded / e.total) * 100)*100/100) + "%");
    $("#PROGRESS").addClass("eventTriggered");
    $("#PROGRESS").removeClass("eventNotTriggered");

}, false);

Step11: Add event listeners for Cached, UpdateReady, Error and noUpdate events. All of them are doing the same steps of logging and changing class. However, UpdateReady event will have an extra step to Swap the Cache.

JavaScript
appCache.addEventListener('cached', function (e) {
        logEvent("<span class='event'>" + e.type + "</span>" + " : " + "All files downloaded");
        $("#UPDATEREADY").text("Cached");
        $("#UPDATEREADY").addClass("eventTriggered");
        $("#UPDATEREADY").removeClass("eventNotTriggered");
    }, false);

    appCache.addEventListener('updateready', function (e) {
        logEvent("<span class='event'>" + e.type + "</span>" + " : " + "New cache available");
        appCache.swapCache();
        $("#UPDATEREADY").text("UpdateReady");
        $("#UPDATEREADY").addClass("eventTriggered");
        $("#UPDATEREADY").removeClass("eventNotTriggered");
    }, false);

    appCache.addEventListener('noupdate', function (e) {
        logEvent("<span class='event'>" + event.type + "</span>" + " : " + "No cache updates");
        $("#noupdate").addClass("eventTriggered");
        $("#noupdate").removeClass("eventNotTriggered");
    }, false);

    appCache.addEventListener('obsolete', function (e) {
        logEvent("<span class='event'>" + e.type + "</span>" + " : " + "Manifest cannot be found");
    }, false);

    appCache.addEventListener('error', function (e) {
        logEvent("<span class='event'>" + e.type + "</span>" + " : " + "An error occurred");
    }, false);

Step12: And that’s it, let’s include this js in our page head section and run the application.

HTML
<script src="@Url.Content("~/Home/js/jquery.min.js")"></script>
<script src="@Url.Content("~/Home/js/config.js")"></script>
<script src="@Url.Content("~/Home/js/skel.min.js")"></script>
<script src="@Url.Content("~/Home/js/skel-panels.min.js")"></script>
<script src="@Url.Content("~/Home/js/AppCacheEvents.js")"></script>

Below is the Log of the First Run of the Page in Chrome and also an image showing the progress on the page(Have used a breakpoint to slow down the page updation so we can see the events happening in realtime)

As you can see below the first time the status stopped at Cached and the next time ended with a UpdateReady status.

Image 10

To simulate the Obsolete status and event, lets return a 404 from fiddler and see that the Obsolete event is triggered and updated on the page.

Image 11

As a Final step let’s add a FallBack section to our Manifest to return a static page and see how that works.

Manifest Action:

C#
manifest = manifest + "#File_Count=" + (JSfiles.Length+CSSfiles.Length) + Environment.NewLine +
        Environment.NewLine+"FALLBACK:"+Environment.NewLine+"/ /Home/FallBack"+Environment.NewLine + "NETWORK:" + Environment.NewLine +
       "*";

FallBack Action:

C#
public ActionResult FallBack()
        {
            return View();
        }

Now let’s test this by following below steps

Step1: Load the Page

Step2: Click on the first article so its Cached

Step3: Cut off network using fiddler

Step4: Click on the second article which was not cached

Step5: Verify that the fallback page is loaded.

Image 12

That’s it, the webpage now Caches all the previously visited articles and can be read anytime while you are offline.

You are ready to Cache and Carry your favorite CodeProject Articles.

Some Tips, tricks and best practices

Before using App Cache in your projects, do take a look at some Gotchas and tips below. *(Some of them might have been repeated earlier but listing them to have them for quick reference)

  1. Browser always loads the cache version of the page before looking for an updated one, you need to refresh manually or programmatically on UpdateReady to get the latest updates on the page.
  2. Every page in the project will need the manifest attribute if they all need to be cached.
  3. The page being cached itself need not be listed in the Manifest file, they will be cached automatically
  4. All the resources have to be downloaded successfully to update the Cache, else none of them will be update. All or none policy in effect
  5. Some browsers need SwapCache to be called explicitly on UpdateReady event, it’s a good idea to include it anyway
  6. HTTP Cache and Application Cache is different and should be avoided being used together.
  7. Even if your application has changed and no change in manifest then the browser will not consider it a change. Only a changed manifest file will trigger an Update
  8. Use Fiddler to test your site. A quick step by step instruction is included above.
  9. If you ever want to see what sites are currently Cached in your Chrome browser then follow below steps
    1. Type chrome://chrome-urls/ in your addressbar
    2. Click on chrome://appcache-internals/
    3. This will list all the Cached sites
    4. If you want to see a Particular Site’s entries click on view entries
    5. To clear the cache for a site, just click Remove link'
    6. Below is a quick image for reference
    7. Image 13
  10. Browser caches based on URL and is case sensitive, so if your URL has a change including querystring it will be cached separately
  11. If you have linked images on a Cached page then they will not be cached and in turn will not be loaded during offline browsing
  12. Use Chrome or Firefox Console to debug or look at the event log and any errors. Shortcut Key in Chrome [ Ctrl + Shift + J]. Below is a quick screenshot of the console with event log

Image 14

  1. Effectively App Cached never expire.
  2. Explicit instruction not to cache this page with HTTP Headers like Cache-Control: No-Cache are ignored when App Cache enabled
  3. Manifest files should always be served with mime type “text/cache-manifest”
  4. Always add a comment with Version number in your Manifest, this version number can even come from your Application Build timestamp
  5. Always serve your Manifest with No-Cache setting. The manifest file itself should never be cached. Another way to ensure this is to set expires headers on your web server for *.appcache files to expire immediately. This will eliminate the chances of caching manifest files.
  6. If the request for Manifest file itself returns a 404 then the status is updated to Obsolete and the cache is deleted.
  7. In Chrome's incognito mode the browser cannot write to the Cache and your site will not be cached, using modernizer can help fix this to some extent

Using the code

Download the Zip file and extract the contents, open the solution file in Visual Studio 2010/2012. You will need to install HTMLAgility pack and Json.Net efor the first time as I am not including the referenced DLLs in the download zip.

Once done, you are all set. Click Run and Cache-In

History

Initial Version - 5/3/2014 - Guruprasad K Basavaraju

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)