One of the projects we just finished had two requirements (well, the third was it had to be a web site that ran on the iPad). The first one was for the web site to be able to display a lot of data in a quick manner, and that this data should be available offline. The second requirement was for that data that is manipulated by the user to be available offline and sync back to the server a.s.a.p.
Offline pages
This part was easier to solve; what we did was to expose a bunch of pages using asp.net mvc that would render on the server and then send back those pages to the client (without any ajax in them). When the pages were finished, we added a button on the profile page which would force the server to add or remove a cookie, indicating if we wanted offline pages to be available or not. Then in _Layout.cshtml file, we added the following :
@{
if (ViewBag.IsOfflinePage == null)
{
ViewBag.IsOfflinePage = false;
}
if (Request.IsAuthenticated && ViewBag.IsOfflinePage)
{
this.WriteLiteral(string.Format("<html manifest=\"{1}/{0}\">", User.Identity.GetUserName(), Url.Content("~/home/offlineManifest")));
}
else
{
this.WriteLiteral("<html>");
}
}
This allowed us to dynamically include the manifest or not, and have a manifest per “user”. The “IsOfflinePage” property is only set to true on site`s main page (and only if the cookie is present) so that the system only tries to update the offline pages when the user is on the home page…
[AllowAnonymous]
public ActionResult Index()
{
if (!User.Identity.IsAuthenticated)
return View("Index_NotAuthorised");
if (Request.Cookies.AllKeys.Contains("Offline") &&
Request.Cookies["Offline"].Value == User.Identity.GetUserId())
ViewBag.IsOfflinePage = true; //send the manifest
return View();
}
In order to provide feedback, here is what we have in the _layout.cshtml page…
$(function () {
if (window.applicationCache) {
var appCache = window.applicationCache;
appCache.addEventListener('error', function (e) {
$('#cacheStatus').text("- Offline Mode - Error-");
console.log(e);
}, false);
appCache.addEventListener('checking', function (e) {
$('#cacheStatus').text("- Offline Mode - Verifying -");
}, false);
appCache.addEventListener('noupdate', function (e) {
$('#cacheStatus').text("- Offline Mode - You have the lastest version -");
}, false);
appCache.addEventListener('downloading', function (e) {
$('#cacheStatus').text("- Offline Mode - Downloading -");
}, false);
appCache.addEventListener('progress', function (e) {
$('#cacheStatus').text("- Offline Mode - Downloading " + e.loaded + " / " + e.total + " -");
}, false);
appCache.addEventListener('updateready', function (e) {
$('#cacheStatus').html("- Offline Mode - New version downloaded, <a href='javascript:window.location.reload()'>click here to activate</a> -");
notifier.show("New version downloaded, click button in the footer to activate");
}, false);
appCache.addEventListener('cached', function (e) {
$('#cacheStatus').text("- Offline Mode activated -");
}, false);
appCache.addEventListener('obsolete', function (e) {
$('#cacheStatus').text("- Offline Mode Deactivated-");
}, false);
}
});
The only thing missing was dynamic creation of the offline manifest, for that we added an action called “offlineManifest” to our home controller with the matching cshtml file. This is a sample of the cshtml file, not how we are including the bundled stuff :
CACHE MANIFEST
@{
Layout = null;
}
# OfflineIndex: @ViewBag.OfflineIndex
CACHE:
@Url.Content("~/img/homebg.png")
@Styles.RenderFormat("{0}", "~/bundles/css")
@Styles.RenderFormat("{0}", "~/bundles/kendo-css")
@Scripts.RenderFormat("{0}", "~/bundles/modernizr")
@Scripts.RenderFormat("{0}", "~/bundles/jquery")
@Scripts.RenderFormat("{0}", "~/bundles/kendo")
@Scripts.RenderFormat("{0}", "~/bundles/js")
@Url.Content("~/home/kendouidatasources")
@Url.Content("~/fonts/glyphicons-halflings-regular.ttf")
@Url.Content("~/fonts/glyphicons-halflings-regular.eot")
@Url.Content("~/fonts/glyphicons-halflings-regular.svg")
@Url.Content("~/fonts/glyphicons-halflings-regular.woff")
@Url.Content("~/content/kendo/Bootstrap/sprite.png")
@Url.Content("~/content/kendo/Bootstrap/loading-image.gif")
@Url.Content("~/bundles/Bootstrap/sprite.png")
@Url.Content("~/bundles/Bootstrap/loading-image.gif")
@Url.Content("~/")
@Url.Content("~/home/about")
@Url.Content("~/home/notavailableoffline")
@Url.Content("~/catalog/products")
@{
foreach (var id in ViewBag.CatalogProductIds)
{
Write(@Url.Content(string.Format("~/catalog/productimage/{0}", id)) + "\r\n");
Write(@Url.Content(string.Format("~/catalog/product/{0}", id)) + "\r\n");
}
}
NETWORK:
*
FALLBACK:
/ @Url.Content("~/home/notavailableoffline")
And here is our controller action code, note that we need special handling because for the manifest to be valid, we need to “trim()” the response before if got sent as razor was adding a blank space at the beginning, which was generating an error client side.
public ActionResult OfflineManifest(string id)
{
if (!Request.Cookies.AllKeys.Contains("Offline") ||
Request.Cookies["Offline"].Value != User.Identity.GetUserId())
return HttpNotFound();
var offlineIndex = db.Parameters.Single().OfflineIndex;
ViewBag.OfflineIndex = offlineIndex;
//catalog stuff
ViewBag.CatalogProductIds = db.Products.ToList();
var partial = true;
var viewPath = "~/views/home/OfflineManifest.cshtml";
return GetCacheManifestContent(partial, viewPath, null);
}
private ActionResult GetCacheManifestContent(bool partial, string viewPath, object model)
{
// first find the ViewEngine for this view
ViewEngineResult viewEngineResult = null;
if (partial)
viewEngineResult = ViewEngines.Engines.FindPartialView(ControllerContext, viewPath);
else
viewEngineResult = ViewEngines.Engines.FindView(ControllerContext, viewPath, null);
if (viewEngineResult == null)
throw new FileNotFoundException("ViewCouldNotBeFound");
// get the view and attach the model to view data
var view = viewEngineResult.View;
ControllerContext.Controller.ViewData.Model = model;
string result = null;
using (var sw = new StringWriter())
{
var ctx = new ViewContext(ControllerContext, view,
ControllerContext.Controller.ViewData,
ControllerContext.Controller.TempData,
sw);
view.Render(ctx, sw);
result = sw.ToString().Trim();
}
return Content(result, "text/cache-manifest");
}
Here is a bunch of links that got us going with offline pages…
http://www.whatwg.org/specs/web-apps/current-work/multipage/browsers.html#concept-appcache-master
http://www.dullroar.com/detecting-offline-status-in-html-5.html
http://www.dullroar.com/html-5-offline-caching-gotcha-with-ipad.html
http://sixrevisions.com/web-development/html5-iphone-app/
http://www.webreference.com/authoring/languages/html/HTML5-Application-Caching/index.html
http://www.html5rocks.com/en/tutorials/appcache/beginner/
One nasty bug we did find with IE is that if the offline manifest file contains more than 1000 lines, it simply generates an error. The limit can be raised via group policies and I hope that they remove this limit in IE12 (I declared the bug at Microsoft). Here is the link to how to modify group policies : http://technet.microsoft.com/en-us/library/jj891001.aspx.
Offline data
This second requirement was a bit harder to fix. To have offline data we used the “DataSource” component from Telerik which itself uses LocalStorage (storage that continues to exist after the browser is closed).
LocalStorage is a very basic key-value store, and in order to save something complexe, we need to use JSON to represent that data as a string. The project also used Telerik’s KendoUI technology to make the online-offline transition almost code free…
Basically, a web page is loaded, and a “DataSource” is instantiated. I initialize is with “.offline(true)” and execute a “.fetch()” to force it to grab data right away from LocalStorage.
I then hookup some code to monitor for online and offline events. If I go online, I ping a web service to make sure it is true. If it turns out that the app is really online, I perform a “.offline(false)” which syncs data to the server and then a “.read()” on the datasource for it to flush it’s data and go grab fresh one. Note that I only perform the “.read()” on the datasource if I am on the home page, so that way i am only hitting the server when the site is opened and not when the user is working on the different pages.
All of this works great. Operations on the datasource are sent to the server in real time if I am online and queued offline for later sending if I am offline.
One problem I got, is that the DataSource transforms the data when it is read. i.e. it receives json from the webserver and converts any data according to what is written in the model (i.e. parse dates…). When the data is saved to local storage, everything is serialised to json. The problem occurs when the DataSource reads it’s data from LocalStorage, it doesn’t apply the same rules as when receiving the data from a web service and so, dates appear as strings in the model instead of dates. The problem is that json doesn’t have the metadata to tell the deserializer how to handle a particular field, and so custom code must be written. I am pretty sure this will be fixed with the next version of kendoui.
The second problem I got is that I must wait for the “.offline(false)” to finish before fetching new data with “.read()”. If they go in parallel, chances are I won’t get the new/modified data. Right now there are no promises and so I need a timer. What I do is if there were changes “.hasChanges()”, then I put the timer to 2-3 seconds, otherwise I put it on 500 ms. I am also hoping this will be fixed in the next version f kendoui.

Leave a comment