First, let me just cop to the fact that this post draws a lot from Jayesh Viradiya’s post on this ColdFusion/AIR Offline support. I had to simplify his stuff to wrap my mind around it enough to talk about it. So hats off to Jayesh, he’s done some awesome work here.
I was doing a demo on ColdFusion and AIR integration at the keynote for CFUnited, and I figured I would go into a little more detail here.
First off, to call it “ColdFusion and AIR integration” is to do it a bit of a disservice. What our engineers have accomplished is nothing short of “ORM for AIR.” It handles:
- Creating and maintaining the SQLite database
- Saving objects to SQLite without SQL statements
-
Handling relationships:
- one-to-one
- one-to-many
- many-to-one
- many-to-many
- Syncing those records back to ColdFusion
So I have a sample application that shows a basic demo of this, without the relationships. Jayesh’s demo has the relationships. I’ve attached the code here, so if you want to look at it, just download the whole thing. Otherwise, let me take you through it. One little note, this stuff was written for ColdFusion 9 Beta. There are some bugs. I’ll point them out where I can, and assure you that the bugs are being worked on.
So let’s start in the ColdFusion. First I define an ORM cfc:
component persistent="true"{
property name="personID" fieldtype="id";
property name="firstName";
property name="lastName";
property name="twitter";
public string function serial(){
return "#This.getFirstName()#|#This.getLastName()#|#This.getTwitter()#|#This.getpersonID()#";
}
}
Pretty straightforward; then I define a corresponding ActionScript class:
package airdemo
{
[Bindable]
[RemoteClass(alias="AIRDemo.person")]
[Entity]
public class Person
{
[Id]
public var personID:int;
public var firstName:String;
public var lastName:String;
public var twitter:String;
}
}
Note the RemoteClass mapping to the CFC on the back end. Now let’s go to the application. I have a simple data grid and form designed to show and edit the details of these person objects. I’ll skip that and go right to the part where I hook up AIR to the ColdFusion server:
private function init():void
{
// Provide Credentials for Server side Connection and CFC
syncmanager = new SyncManager();
syncmanager.cfPort = 80;
syncmanager.cfServer = "centaur.dev";
syncmanager.syncCFC = "AirDemo.personManager";
// THis handler will be called when any COnflict
// occurs while writing back changes on serverside
syncmanager.addEventListener(ConflictEvent.CONFLICT, conflictHandler);
//Kick off the application.
getRemote();
}
This connects this application to the ColdFusion server centaur.dev on port 80 and wires the syncmanager to AIRDemo.personmanager. More on that later. But it also kicks off getRemote which takes care of populating this application with data from the server. So getRemote() fires:
//GET records from BACKEND SERVER
private function getRemote():void{
var token:AsyncToken= syncmanager.fetch("fetch");
token.addResponder(new mx.rpc.Responder(fetchSuccess, fetchFault));
}
Syncmanager.fetch calls the fetch method of the ColdFusion CFC we set as syncmanager.syncCFC above. That method just retrieves the records using ColdFusion ORM.
remote Array function fetch(){
return EntityLoad('person');;
}
In most cases it is successful, in which case fetchSuccess calls createLocalDB:
//CREATE the actual SQLite DB
private function createLocalDB():void{
//Create a pointer to actual SQLite db file
dbFile = File.userDirectory.resolvePath("AirDemo.db");
var sessiontoken:SessionToken =syncmanager.openSession(dbFile,017916);
sessiontoken.addResponder(new mx.rpc.Responder(connectSuccess,connectFault));
}
In most cases that is successful and connectSuccess calls createLocalCacheFromRemote
//PUT records from BACKEND SERVER in SQLite DB
private function createLocalCacheFromRemote():void{
var savetoken:SessionToken = session.saveUpdateCache(pc);
savetoken.addResponder(new mx.rpc.Responder(saveCacheSuccess, savefault));
}
Those three ActionScript functions in concert with the ColdFusion one:
- Got the data from the ColdFusion server
- Created a local SQLite database for the data
- Populated the local SQLite data with that data
Okay, so now I go through the application, update some data, and want to save it back to the SQLite database:
//SAVE to SQLite DB
private function saveLocal():void
{
//Generate person object from form.
var person:Person = convertFormToObject();
//session.saveUpdate saves record to SQLite Database
var savetoken:SessionToken = session.saveUpdate(person);
savetoken.addResponder(new mx.rpc.Responder(saveSuccess, savefault));
}
ConvertFormToObject does exactly what it sounds like, converting values from a form into a Person object, then session.saveUpdate() handles saving the record back to the SQLite store. No SQL required. Then we need to send it off to the ColdFusion server:
//SAVE to BACKEND SERVER
private function saveRemote():void
{
var committoken:SessionToken = session.commit();
committoken.addResponder(new mx.rpc.Responder(commitSuccess, commitFault));
}
That’s it. That transmits all of that changes made in this session to the ColdFusion server, where it is processed:
remote any function sync(required array operations,
required array clientobjects,
array originalobjects = ArrayNew(1)){
var i= 0;
var conflicts = ArrayNew(1);
var conflictcount = 1;
for (i=1; i <= ArrayLen(operations); i++ ){
var operation = operations[i];
var clientobject = clientobjects[i];
var originalobject = originalobjects[i];
if (operation eq "INSERT" ){
var obj = ORMGetSession().merge(clientobject);
EntitySave(obj);
}
else{
if (isinstanceOf(originalobject,"person")){
var serverobject = EntityLoadByPK("person",originalobject.getpersonID());
}
else{
throw "Invalid Object";
}
if (not isdefined('serverobject')){
var text="CONFLICT::SERVER OBJECT NOT FOUND, RECORD MAY BE DELETED ALREADY";
var conflict = New CFIDE.AIR.conflict();
conflict.clientobject = clientobject;
conflict.originalobject = originalobject;
conflict.operation = operation;
conflicts[conflictcount++] = conflict;
continue;
}
var isNotConflict = (originalobject.serial() eq serverobject.serial());
if (isNotConflict){
if (operation eq "UPDATE"){
obj = ORMGetSession().merge(clientobject);
EntitySave(obj);
}
else if (operation eq "DELETE"){
obj = ORMGetSession().merge(originalobject);
EntityDelete(obj);
}
}
else{
var text="is a conflict";
var conflict = New CFIDE.AIR.conflict();
conflict.clientobject = clientobject;
conflict.originalobject = originalobject;
conflict.operation = operation;
conflicts[conflictcount++] = conflict;
continue;
}
}
}
if (conflictcount gt 1){
return conflicts;
}
}
}
So this is a lot of code, but basically it performs the following steps:
-
Check to see if the record is new
- If so insert it
-
Then check to see if the update is in conflict
- If not, delete or update accordingly
- If so, send a conflict back to the AIR client
Now back in the client you have to handle the conflict, in this version of the application, I just replace the client details with the server details:
//OVERWRITE from BACKEND SERVER
public function conflictOverwrite(conflicts:ArrayCollection):void{
var token:SessionToken = session.keepAllServerObjects(conflicts);
token.addResponder(new mx.rpc.Responder(conflictSuccess, conflictFault));
}
Again, one function, session.keepAllServerObjects, handles overwriting everything on the client.
So that is the gist of what I was going to show at CFUnited. The code is attached, (Down at the bottom there is a “download” link) feel free to give it a try and see what you can do with it.
Thanks for posting this… I think this can help me jumpstart.
LikeLike
Great example, thank you!
I can’t find the download link though.
/Lars Lindholm
LikeLike
Thanks for the post! Is LCDS needed to make this work?
LikeLike
Hello, Is there a download link for this example?
Thank you,
Brian
LikeLike