Create an offline HTML5 application for IOS with ASP.NET MVC3

 You need to build an application for the IPad or IPhone but you don’t have the time to learn objective-c and cocoa.  The good news is that you don't need to! 

This walkthrough demonstrate how to use the .Net web technology stack to create applications for the IOS platform.  Because we want to be capable of using the application also offline, the UI need to rely purely on HTML and Javascript and we can't use any server side markup generation. Nevertheless we'll need ASP.NET MVC for the creation of web services and his brand new model binding feature to deserialize the JASON message back to a server side model.

We'll first create a new Visual Studio Asp.Net MVC3 empty web application and add an “index.htm” page to the project root:

<!DOCTYPE html>
<html>
<head>
    <title>Subscription Form</title>
    <link href="Content/Site.css" rel="stylesheet" type="text/css" />
    <script src="/Scripts/jquery-1.5.1.min.js" type="text/javascript"></script>
    <script src="/Scripts/jquery.validate.min.js" type="text/javascript"></script>
</head>
<body>
    <div class="page">
        <header>
            <div id="title">
               Subscription Form
            </div>
 
        </header>
        <section id="main">
        <span id="resultmessage"></span>
        <form id="sub_form" method="" action="">
            <table>
                <tr>
                    <td>
                        Firstname:
                    </td>
                    <td>
                        <input type="text" id="firstname" name="firstname" />
                    </td>
                    <td>
                        Lastname:
                    </td>
                    <td>
                        <input type="text" id="lastname" name="lastname" />
                    </td>
                </tr>
                <tr>
                    <td>
                        E-mail:
                    </td>
                    <td>
                        <input type="text" id="email" name="email" />
                    </td>
                    <td>
                        Phone:
                    </td>
                    <td>
                        <input type="text" id="phone" name="phone" />
                    </td>
                </tr>
                <tr>
                    <td>
                        <input type="button" id="create_subscription" value="Create" >
                    </td>
                    <td>
                        <input type="button" id="send_data" value="Send" />
                    </td>
                </tr>
            </table>
        </form>
        <footer>
            <div>
                <span id="busy"><img src="/Content/wait.gif" alt="wait" /></span>
            </div>
        </footer>
        </section>
    </div>
    <script src="/Scripts/index.js" type="text/javascript"></script>
</body>
</html> 

The first line uses the Html5 doctype and the header links to jQuery. We uses two buttons, one to create the profiles in the browser local storage and another the send the profiles to the server.

To create red boxes arround our input boxes when jQuery detects some validation we need to add these few lines of css to stylesheet:

input.error { border: 2px solid red; }

Now it’s time to implement our UI logic with javascript. By creating a separate file for our javascript we keep the  html page clean.

Add a “index.js” to the script folder:

var root = '/Subscription';
var subscriptionsUploaded = 0;
 
//jQuery on document ready handler
$(function () {
    //Show the busy indicator during ajax calls
    $("#busy").hide();
    $("#busy").ajaxStart(function () { $("#busy").show(); })
    $("#busy").ajaxStop(function () { $("#busy").hide(); })
    var x = $("#createSubscription");
    //When the createSubscription button is clicked create a subscription
    $("#create_subscription").click(createSubscriptionClick);
 
    //When the sendData button is clicked retrieve the subscriptions stored in the local storage and send the data to the server
    $("#send_data").click(sendDataClick);
 
    //Define jQuery validation rules and execute the validation when the form is validated 
    $("#sub_form").validate({
        rules: {
            firstname: {
                required: true,
                minlength: 2
            },
            lastname: {
                required: true,
                minlength: 2
            },
            email: {
                required: true,
                email: true
            },
            phone: {
                required: true,
                number: true,
                minlength: 9,
                maxlength: 9
            }
        },
        messages: {
            firstname: "",
            lastname: "",
            email: "",
            phone: ""
        }
    });
});
 
function sendDataClick() {
    //Iterate over the subscriptions stored in the local storage
    for (var i = 0; i < window.localStorage.length; i++) {
        //Retrieve the serialized subscription  
        var json = window.localStorage.getItem(window.localStorage.key(i));
        try {
            //Send the subscription to the server 
            sendData(
                    json,
            //On success remove the subscription from the local storage 
                    function (data) {
                        window.localStorage.removeItem(window.localStorage.key(data.Message));
                        subscriptionsUploaded++;
                        $("#resultmessage").html(subscriptionsUploaded + " subscriptions uploaded!");
                    },
            //On error
                    function (xhr) {
                        alert(xhr.responseText);
                    });
        }
        catch (e) { alert(e); }
    };
}
 
//Stores a subscription into the local storage
function createSubscriptionClick() {
    //check the jQuery validation rules 
    if ($("#sub_form").valid()) {
        var person = getSubscription();
        //seialize the subscription 
        var jsData = JSON.stringify(person);
        //store the subscription
        window.localStorage.setItem($("#email").val(), jsData);
        //update the resultMessage
        $("#resultmessage").html($("#email").val() + " stored in local storage");
        clearAll();
    }
}
 
//Create a subscription object and bind to the input boxes values
function getSubscription() {
    var firstname = $("#firstname").val();
    var lastname = $("#lastname").val();
    var email = $("#email").val();
    var phone = $("#phone").val();
    return { Firstname: firstname, Lastname: lastname, Email: email, Phone: phone };
}
 
//Clear the input boxes values
function clearAll() {
    $("#firstname").attr("value", "");
    $("#lastname").attr("value", "");
    $("#email").attr("value", "");
    $("#phone").attr("value", "");
}
 
//Ajax: post the json serilized subscriptions 
function sendData(json, success, error) {
    $.ajax({
        url: root + '/save',
        type: 'POST',
        dataType: 'json',
        data: json,
        contentType: 'application/json; charset=utf-8',
        success: success,
        error: error
    });
}

When the user click on the create button the createSubscription function is called, this function is responsible for serializing the data into a Json object and to store it in the isolated storage.  Isolated storage is a feature defined in the Html5 spec. For each domain the browser provide access  to private key/value pair collection where data can be stored  and retrieved. The sendData button is attached with an anonymous function that iterate over the key/value pair collection that contains the JSON serialized subscriptions and post the JSON serialized subscription object.

The MVC 3 includes built-in JSON binding support that enables action methods to receive JSON-encoded data and model-bind it to action-method parameters. We will now create our Model to bind to, add a SubscriptionModel class to the models of the MVC app:

public class SubscriptionModel
{
    [Required]
    [Display(Name = "Firstname")]
    public string Firstname { get; set; }
 
    [Required]
    [Display(Name = "Lastname")]
    public string Lastname { get; set; }
 
    [Display(Name = "Phone")]
    public string Phone { get; set; }
 
    [Required]
    [DataType(DataType.EmailAddress)]
    [Display(Name = "Email address")]
    public string Email { get; set; }
 
    public void Save()
    {
        //store the object in db
        //...
    }
}

The MVC3 model binder will automatically bind all properties that matches the properties in the JSON encoded data.  Now we will define our action method to post to, add a SubscriptionController class to the controllers folder:

[HttpPost]
public ActionResult Save(SubscriptionModel inputModel)
{
    //Server side validation
    if (ModelState.IsValid)
    {
        //If the model is valid we can call the save method
        inputModel.Save();
        string message = inputModel.Email;
 
        //We return the email so that when a subscription has been successfully stored we can remove it from the localstorage.
        //--> SendData function in index.js-> window.localStorage.removeItem(window.localStorage.key(data.Message));
        return Json(new { Message = message });
    }
    else
    {
        //When server side validation fails we return in the details about the validation rules that failed.
        string errorMessage = "<div class=\"validation-summary-errors\">"
                + "The following errors occurred:<ul>";
        foreach (var key in ModelState.Keys)
        {
            var error = ModelState[key].Errors.FirstOrDefault();
            if (error != null)
            {
                errorMessage += "<li class=\"field-validation-error\">"
                    + error.ErrorMessage + "</li>";
            }
        }
        errorMessage += "</ul>";
        return Json(new { Message = errorMessage });
    }
}

Now you should be able to test the application. 

Set a breakpoint on the Save method of the SubscriptionModel. When you enter data in the form and push on create profiles are added to the local storage, when you click on send you should hit the breakpoint and all properties of the model should been set.

To be able to load the application when we are offline we need to use a new feature available in the latest browser like IE9, the Html5:”offline application”.  More info of this feature can be found here.

Add a cache.manifest file to the root of the application:

CACHE MANIFEST
/index.htm
/Content/Site.css
/Scripts/index.js
/Scripts/jquery-1.5.1.min.js
/Scripts/jquery.validate.min.js
/Content/wait.gif

Change the doctype of the index.htm file:

<html manifest="cache.manifest">

You will also need to change the http header that is returned by the IIS server when serving the manifest file.  Therefore in IIS7 you need to click on HTTP Headers, open IIS console manager, navigate to the cache.manifest file, click on http headers, add for the name set-> manifest,value->text/cache-manifest.

If you want an optimal experience when deploying your app on the IPhone or IPad you should also add the following meta tags inside the HTML header:

<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link rel="apple-touch-icon" href="/Content/applogo.gif" />
<link rel="apple-touch-startup-image" href="/Content/startuplogo.gif" />

You should also add your own image files (applogo & startuplogo) in the content folder so that the startup and app icon is set on the IPhone.

Now you can navigate to the page with your IOS device, click on “+” button of the safari browser, add shortcut and voilà you’ve an offline application for your IPhone/IPad made entirely in HTML5/Javascript/ASP.NET MVC3.

Have fun!

kick it on DotNetKicks.com

HTML5 Local Storage

There is a feature in HTML5 that enables you to store named key/value pairs in the local storage of your browser.  The advantage over using regular cookies is that your data is never send to the remote web server and that you can store a lot more compared to the 4KB limitation of cookie storage space.

As the local storage is a feature making part of the HTML5 specification, only modern browser supports is;
Safari 4.0, Opera 10.5,IE8, FireFox 3.5, Chrome 4.0 and IOS above version 2.0.

As for any feature of HTML5 before using it you should check if your browser supports the feature:

   1: function can_use_html5() {
   2:    try {
   3:        return 'localStorage' in window && window['localStorage'] !== null;
   4:    } 
   5:    catch (e) {
   6:        return false;
   7:    }
   8: }

To store or retrieve an item from the local storage:

   1: //Retireve an item:
   2: var item = localStorage.getItem("mykey");
   3: //Store an item:
   4: localStorage.setItem("myKey", "myValue");

To clear an item from the local storage or remove all items:

   1: //Remove an item
   2: localStorage.removeItem("myKey");
   3: //Remove all
   4: localStorage.clear();

 

With this feature we are now able to build web applications that can work offline.  Here under I build an application that stores persons profiles in the local storage.  I use the JSON.stringify method to serialize the persons before storing the JSON strings in the local storage and JQuery to retrieve the values and bind the events:

   1: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
   2: <html xmlns="http://www.w3.org/1999/xhtml">
   3: <head>
   4:     <title>Local Storage</title>
   5:     <script src="http://ajax.microsoft.com/ajax/jQuery/jquery-1.5.1.js" type="text/javascript"></script>
   6: </head>
   7:  
   8: <body>
   9:     <div id="resultMessage">&nbsp;</div>
  10:     <table style="position: absolute; left: 10px;">  
  11:         <tr>
  12:             <td>
  13:             Firstname:
  14:             </td>
  15:             <td><input type="text" id="firstname" />
  16:             </td>
  17:         </tr>
  18:         <tr>
  19:             <td>
  20:             Lastname:
  21:             </td>
  22:             <td><input type="text" id="lastname" />
  23:             </td>
  24:         </tr>
  25:         <tr>
  26:             <td>
  27:             E-mail: 
  28:             </td>
  29:             <td><input type="text" id="email" />
  30:             </td>
  31:         </tr>
  32:          <tr>
  33:             <td>
  34:                 <input type="button" id="save"  value="Create" />
  35:             </td>
  36:             <td>
  37:                 <input type="button" id="print"  value="Print"/>
  38:             </td>
  39:         </tr>
  40:     </table>
  41:     <script type="text/javascript">
  42:         $(function () {
  43:             if (!can_use_html5()) {
  44:                 alert("This browser does not support Html5 storage, upgrade or change your browser!");
  45:             }
  46:         });
  47:  
  48:         $(function () {
  49:             $("#save").click(function () {
  50:                 
  51:                 var person = getPerson();
  52:                 
  53:                 //simple validation
  54:                 if (person == null) {
  55:                     alert("Fill in all fields please!");
  56:                     return;
  57:                 }
  58:                 
  59:                 var jsData = JSON.stringify(person);
  60:                 localStorage.setItem($("#email").val(), jsData);
  61:                 
  62:                 $("#resultMessage").html($("#email").val() + " stored in local storage");
  63:                 clearAllText();
  64:             })
  65:         });
  66:  
  67:         $(function () {
  68:             $("#print").click(function () {
  69:                 for (var i = 0; i < window.localStorage.length; i++) {
  70:                     document.write('<div id="record_' + i + '"> ' + localStorage[localStorage.key(i)] + '</div>');
  71:                 }
  72:                 localStorage.clear();
  73:             })
  74:         });
  75:  
  76:         function getPerson() {
  77:             var firstname = $("#firstname").val();
  78:             var lastname = $("#lastname").val();
  79:             var email = $("#email").val();
  80:             //simple validation
  81:             return (firstname == "" || lastname == "" || email == "") ? null : { Firstname: firstname, Lastname: lastname, Email: email };
  82:         }
  83:  
  84:         function can_use_html5() {
  85:             try {
  86:                 return 'localStorage' in window && window['localStorage'] !== null;
  87:             } 
  88:             catch (e) {
  89:                 return false;
  90:             }
  91:         }
  92:  
  93:         function clearAllText() {
  94:             
  95:             $("#firstname").attr("value", "");
  96:             $("#lastname").attr("value", "");
  97:             $("#email").attr("value", "");
  98:         }
  99:     </Script>
 100: </body>
 101: </html>

HTML5 enables new possibilities for all types of scenarios like creating IPAD/IPhone web applications without having to program in objective c and/or upload the app to the app store.  In a future post I will demonstrate how you can extend this application to create such an application.   

kick it on DotNetKicks.com