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.
namespace("Demo.Models", function(NS) {
var Contacts={
fields: [
{ name: "id", attributes: "INTEGER PRIMARY KEY AUTOINCREMENT"},
{ name: "name"},
{ name: "lastname"},
{ name: "phonenumber"},
{ name: "email" }
],
select: function(config) {
var success=config.success;
var failure=config.failure;
this.store.select(config);
},
get: function(id, config) {
var success=config.success;
var failure=config.failure;
this.store.get(id, config);
},
insert: function(record, config) {
var success=config.success;
var failure=config.failure;
this.store.insert(record, config);
},
update: function(id, record, config) {
var success=config.success;
var failure=config.failure;
this.store.update(id, record, config);
},
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.
namespace("Demo.Controllers", function(NS) {
var Contacts=function() {
var _self=this;
_self.models=["Demo.Models.Contacts"];
Core.apply(_self, Core.Controller.prototype);
return Core.Controller.apply(_self, arguments);
};
Contacts.prototype.index=function() {
var _self=this;
var selectSuccess=function(data) {
_self.viewBag.contacts=data;
_self.view();
};
_self.models["Contacts"].select({success:selectSuccess});
}
Contacts.prototype.contact=function(action, id) {
var _self=this;
_self.models["Contacts"].get(id, {
success: function(data) {
_self.viewBag=data[0];
_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.
var readyModels={};
var numberOfModels=_self.models.length;
for(var i=0, j=_self.models.length; i< j; i++) {
var success=function(name, model) {
readyModels[name]=model;
numberOfModels--;
if(numberOfModels==0) {
_self.models=readyModels;
_self.modelsReady=true;
_self[controllerAction].apply(_self, callArguments);
}
}
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.
namespace("Core", function(NS) {
Core.Model={
models: {},
getModel: function(name, callback) {
var obj=Core.getObject(name);
var shortName=Core.getLastNsPart(name);
var _callback=function() {
callback(shortName, obj);
}
if(obj!==undefined) {
if(this.models.hasOwnProperty(shortName)) {
_callback();
}
else {
switch(obj.type) {
case "MEMORY":
obj.store=new Core.Data.MemoryDataStore();
break;
case "PERSISTENT":
obj.store=new Core.Data.PersistentDataStore();
break;
default:
obj.store=new Core.Data.MemoryDataStore();
break;
}
obj.store.create({ name: shortName }, obj.fields, _callback);
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.
namespace("Core.Data", function(NS) {
var MemoryDataStore=function() {
this.name=null;
this.fields=null;
this.key=null;
this.items=new Array();
this.autoincrement=0;
return this;
};
MemoryDataStore.prototype=new Core.Data.DataStore();
MemoryDataStore.prototype.create=function(config, fields, callback) {
this.name=config.name;
this.fields=fields;
var getPrimaryKey=function(fields) {
for(var i=0, j=fields.length; i<j; i++) {
if(fields[i].key) {
return fields[i].name;
}
}
return null;
};
this.key=getPrimaryKey(fields);
if(this.key==null) {
throw new Error("No primary key found for : " + this.name);
}
callback();
};
MemoryDataStore.prototype.select=function(config) {
config.success(this.items);
};
MemoryDataStore.prototype.get=function(id, config) {
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) {
this.autoincrement++;
record[this.key]=this.autoincrement;
this.items[this.items.length]=record;
config.success(record);
};
MemoryDataStore.prototype.update=function(id, record, config) {
var _self=this;
var index=null;
var getIndex=function(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) {
var _self=this;
var index=null;
var getIndex=function(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.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
- models
- views
- home
- 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.