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

HTML5 Offline MVC: Part 2

0.00/5 (No votes)
6 Apr 2013CPOL4 min read 20.4K   315  
Offline WebApp MVC

Introduction

This is the second part of my HTML5 offline MVC articles. The goal here is to create an offline running iOS webapp. So we're disconnected from all the .NET MVC coodies. But we can still try to maintain a similar structure. In essence, we're making a reusable MVC framework. This part of the article series will create a Model on top of our Controller.

Article Layout

  • Create a JavaScript MVC structure
  • Connect and interact with the WebSql database
  • Take the whole thing offline through applicationCache

Implementation

In the previous article, we created the source to have a simple but effective router/controller/view structure. Now we'll need to focus on making the modals. In the source download of the previous article, I had a contacts controller. In that, I simply put an array of contacts. An array is still a datasource and has no business being in the controller.

Models can be designed in various ways. But in essence, they all should have the following methods:

  • select (get all)
  • get(id) (get this one)
  • insert
  • update
  • delete

The modal should hold all the business logic. I won't be adding any in this example, but in a real situation this would be the place to say, "no this is not allowed".
But the data itself is not inside the modal! Here we will create a modal structure that could have data inside a WebSql, localStorage or maybe just an online WCF service.
All our models will have a store object on top of them. This is the piece that actually holds the data.

Let's just take a look at the end result of our contacts model.

JavaScript
namespace("Demo.Models", function(NS) {
    //Our simple model

    var Contacts={
        //Tel the CoreModal singleton which fields we've got and what their attributes should be
        fields: [
            { name: "id", attributes: "INTEGER PRIMARY KEY AUTOINCREMENT"},
            { name: "name"},
            { name: "lastname"},
            { name: "phonenumber"},
            { name: "email" }
        ],
        //Select all
        select: function(config)  {
            //Right now this is to simple were just passing on the success and failure functions,
            //but this is the model, so if we would have business logic 
            //we would trap the success handler in here first
            //apply the business logic an than return the success function 
            //that came in through the config object
            var success=config.success;
            var failure=config.failure;
            this.store.select(config);
        },
        //Get one
        get: function(id, config)   {
            var success=config.success;
            var failure=config.failure;
            this.store.get(id, config);
        },
        //add one
        insert: function(record, config)   {
            var success=config.success;
            var failure=config.failure;
            this.store.insert(record, config);
        },
        //update one
        update: function(id, record, config)  {
            var success=config.success;
            var failure=config.failure;
            this.store.update(id, record, config);
        },
        //delete one
        delete: function(id, config)    {
            this.store.delete(id, config);
        }
    }

    Demo.Models.Contacts=Contacts;
});

As you might notice, this is all async ready. Even if we would just dump the records in an array, we're still going to apply success/failure handlers. This is because we want our controllers and models to always have the same structure. Regardless of how the data is stored. And for online WCF/httpRequests and WebSql, we'll need this to be asynchronised and thus have a success/failure callback structure.

Let's look at part of our contacts controller to see how we would use this model.

JavaScript
namespace("Demo.Controllers", function(NS) {

    var Contacts=function()    {

        var _self=this;
        //tell the parent class which models to prepare, 
        //this is an array of string representations
        //that match the namespace/objects where you're models reside.
        _self.models=["Demo.Models.Contacts"];

        Core.apply(_self, Core.Controller.prototype);

        return Core.Controller.apply(_self, arguments);
    };

    Contacts.prototype.index=function()    {
        //contacts/index
        var _self=this;

        //if the select succeeds we'll show the data
        var selectSuccess=function(data)  {
            _self.viewBag.contacts=data;
            _self.view();
        };
        //Ask our model to select all the records
        _self.models["Contacts"].select({success:selectSuccess});
    }

    Contacts.prototype.contact=function(action, id)    {
        //contacts/contact/{id}
        var _self=this;
        //ask our model to get a single record
        _self.models["Contacts"].get(id, {
            //We can also code the success function inline
            success: function(data) {

                //Stick the data in the viewbag
                _self.viewBag=data[0];
                //Run our view;
                _self.view();
            }
        });
    }
    ........

How Are We Going to Make this Work?

Now this looks cool. But we're obviously going to need some code around this to make it actually work.

  • In the previous article, we made the Core.Controller class that held all the stuff to make our Controller itself look as simple as possible. That class needs to be somewhat extended.
  • We're going to need a new Singleton called Core.Model that would return the modal and create/query the store.
  • We're going to need a store class that actually holds the data.

In the previous article, I tried to explain and show as many sources as I could. But we still have a long way to go. So for some details, you will have to download the source code.

I will now give a short explanation of what the Core.Controller does.

Core.Controller

In our Contact controller, we specified that we'd like to have a model of Contacts. The Core.Controller class needs to be extended to ask the Core.Model Singleton to return that model as a working object.
When you've downloaded the source, you can see this as a loop in the constructors' return function.

JavaScript
//The models that are ready
var readyModels={};

//We'll need to know how many models there are
var numberOfModels=_self.models.length;

//loop through the models
for(var i=0, j=_self.models.length; i<   j; i++)    {
    //container for the model
    var success=function(name, model)  {

        //add the current mode to readyModels
        readyModels[name]=model;

        //count down, for if we had more than one model
        //the controllerAction should be ran after all of them are ready
        //we cannot reference i because this is asynchronised
        numberOfModels--;

        if(numberOfModels==0)   {
            //all the models are ready

            //replace the string array of models with the ready models
            _self.models=readyModels;
            //set the modelsReady boolean
            _self.modelsReady=true;
            //run the controller action
            _self[controllerAction].apply(_self, callArguments);
        }

    }
    //get the model
    Core.Model.getModel(_self.models[i], success);
}  

Core.Model

The Core.Model singleton will return the model asked for by the Core.Controller and bind the store class on top of the model.

JavaScript
namespace("Core", function(NS)    {
    //Core.Model has the task of binding the models to the controllers
    //and to hide the database interaction from the models

    Core.Model={

        //What models that we already create we'd like to prevent doing it again
        models: {},

        //Return the model to the controller
        getModel: function(name, callback)    {

            //This will get the object out of a string like "Demo.Namespace1.Object1"
            var obj=Core.getObject(name);

            //We can not namespace our tables in something like WebSql.
            //This is a concern it's not possible to have
            //multiple PERSON tables in different namespaces.
            //Here we cut of the namespace and work just with the classname.
            //This might be a point where perfection is needed.
            var shortName=Core.getLastNsPart(name);

            var _callback=function()    {
            callback(shortName, obj);
            }

            if(obj!==undefined)    {
                if(this.models.hasOwnProperty(shortName))    {
                    //We've already got one. So we don't have to make it again. Just do callback
                    _callback();
                }
                else    {
                    //What type of store do we need to create
                    switch(obj.type)    {
                        case "MEMORY":
                            obj.store=new Core.Data.MemoryDataStore();
                        break;
                        case "PERSISTENT":
                        //this line is a sneak preview. We'll be adding this in the next article
                        obj.store=new Core.Data.PersistentDataStore();
                        break;
                        default:
                            obj.store=new Core.Data.MemoryDataStore();
                        break;
                    }

                    //Send in the fields specified in the Model so the store can create itself
                    obj.store.create({ name: shortName }, obj.fields, _callback);

                    //Remember this model to prevent executing this code more than once
                    this.models[shortName]=obj;
                 }
            }
            else    {
                throw new Error("Model " + name + " doesn't exist!");
            }
        },
    }
});

The DataStore Classes

Phew, this has been a long story to just make something simple. An array of objects. In this article, I'll create the DataStore class that just holds the data in memory. So it's essentially useless, but it will give us a good understanding of what such a store needs to be able to do.
After that, we can create our WebSql datastore. But because dealing with WebSql in itself is worth an article, we'll split that up.

What does a DataStore need to do?

  • create
  • select
  • get
  • insert
  • update
  • delete

Our class will inherit from a superclass that I'm not going to elaborate on.

JavaScript
namespace("Core.Data", function(NS) {

    var MemoryDataStore=function()  {
        //The name of our store
        this.name=null;
        //The fields
        this.fields=null;
        //The keyname
        this.key=null;
        //What holds the actual items
        this.items=new Array();
        //The key increment
        this.autoincrement=0;

        return this;
    };

    MemoryDataStore.prototype=new Core.Data.DataStore();

    MemoryDataStore.prototype.create=function(config, fields, callback) {
        //copy some base information
        this.name=config.name;
        //copy the fields
        this.fields=fields;

        //We'll need a primary key. We cannot just use the array index.
        //For it will change when deleting a record.
        var getPrimaryKey=function(fields)  {
            for(var i=0, j=fields.length; i<j; i++)  {
                //look for the primary key

                if(fields[i].key)   {
                    //we found it
                    return fields[i].name;
                }
            }
            return null;
        };

        //Get the key field.
        this.key=getPrimaryKey(fields);

        //the primary key is important, we cannot just use the array index, 
        //because that will change
        //when a entry is deleted
        if(this.key==null)  {
            throw new Error("No primary key found for : " + this.name);
        }

        //do a callback
        callback();
    };

    MemoryDataStore.prototype.select=function(config)   {
        //Since this is just an array it'll be rather simple
        config.success(this.items);
    };

    MemoryDataStore.prototype.get=function(id, config)  {
        //gets a record with the primary key id
        var _self=this;
        var record=null;

        var getRecord=function(id)  {
            for(var i=0, j=_self.items.length; i<j; i++)    {

                if(_self.items[i][_self.key]==id)    {
                    return _self.items[i];
                }
            }
            return null;
        }

        record=getRecord(id);

        if(record==null)    {
            config.failure("Record not found!");
        }
        else    {
            config.success([record]);
        }
    };

    MemoryDataStore.prototype.insert=function(record, config)   {
        //Inserts record
        this.autoincrement++;
        record[this.key]=this.autoincrement;
        this.items[this.items.length]=record;
        //We will return the record in case the model needs to know what id we gave it.
        config.success(record);
    };

    MemoryDataStore.prototype.update=function(id, record, config)   {
        //Updates a record
        var _self=this;
        var index=null;

        var getIndex=function(id)   {
            //Find the record with primary key id
            for(var i=0, j=_self.items.length; i<j; i++) {
                if(_self.items[i][_self.key]==id)   {
                    return i;
                }
            }
            return -1;
        }
        index=getIndex(id);
        if(index==-1)   {
            config.failure("Record not found!");
        }
        else    {
            this.items[index]=record;
            config.success();
        }
    };

    MemoryDataStore.prototype.delete=function(id, config)   {
        //Deletes a record
        var _self=this;
        var index=null;
        var getIndex=function(id)   {
            //Find the record with primary key id
            for(var i=0, j=_self.items.length; i<j; i++) {
                if(_self.items[i][_self.key]==id)   {
                    return i;
                }
            }
            return -1;
        }
        index=getIndex(id);

        if(index==-1)   {
            config.failure("Record not found!");
        }
        else    {
            //Delete the actual record
            this.items.splice(index, 1);
            config.success();
        }
    };


    NS["MemoryDataStore"]=MemoryDataStore;

}); 

What's in the Download?

It's a demo implementation on top of what we've created so far.
It will have all the Core classes in the assets/js folder.

  • assets
    • css
    • js
      • core
        • data
          • core.data.datastore.js
          • core.data.memorydatastore.js
        • core.controller.js
        • core.js
        • core.model.js
        • core.router.js
  • controllers
    • home.js
    • contacts.js
  • models
    • contacts.model.js
  • views
    • home
      • index.html
    • contacts
      • addform.html
      • contact.html
      • editform.html
      • index.html

Conclusion

I'm still hoping there are people hanging in there. This is becoming a long story. But if this is finished, we will have a simple little framework which is easy to extend and implement. I will probably end this series with a documentation of how to use this. But this series is about how I made this. The next article will focus on making a persistent datastore on top of WebSql.

License

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