Bender Code Preview

Well we’re still a ways from havinga working prototype but I thought I’d post some code to give people an idea of where I’m heading (and to get Mark off my back ;p). Much thanks go Justin McLean and Robin Hilliard for answering stupid questions.

here is the current Bender.as:

package com.ls.bender {
 
	public class Bender {
		import mx.rpc.events.ResultEvent;
		import mx.rpc.events.FaultEvent;
		import mx.rpc.remoting.RemoteObject;
 
		private var Remote : RemoteObject = new RemoteObject();
		private var RequestQueue : Object = new Object();
 
		public function Bender() {
			Remote.destination = "ColdFusion";
			Remote.source = "com.ls.bender.Bender";
		}
 
		public function read(obj : *, classname : String, key : String) : * {
			if (obj.getONDATA()) {
				Remote.getOperation("read").addEventListener(ResultEvent.RESULT, readResultHandler);
			}
			if (obj.getONFAULT()) {
				Remote.getOperation("read").addEventListener(FaultEvent.FAULT, readFaultHandler);
			}
			RequestQueue[Remote.read(classname, key)] = obj;
		}
 
		private function readResultHandler(event : ResultEvent) : void {
			RequestQueue[event.token].fill(event);
			RequestQueue[event.token].getONDATA()(RequestQueue[event.token]);
		}
 
		private function readFaultHandler(event : FaultEvent) : void {
			RequestQueue[event.token].getONFAULT()(RequestQueue[event.token]);
		}
 
		public function save(obj : Object) : void {
			if (obj.getONDATA()) {
				Remote.getOperation("save").addEventListener(ResultEvent.RESULT, saveResultHandler);
			}
			if (obj.getONFAULT()) {
				Remote.getOperation("save").addEventListener(FaultEvent.FAULT, saveFaultHandler);
			}
			var args : Object = new Object();
			args.OBJECT = obj.data();
			RequestQueue[Remote.save(args)] = obj;
		}
 
		private function saveResultHandler(event : ResultEvent) : void {
			RequestQueue[event.token].getONDATA()(RequestQueue[event.token]);
		}
 
		private function saveFaultHandler(event : FaultEvent) : void {
			RequestQueue[event.token].getONFAULT()(RequestQueue[event.token]);
		}
 
		public function del(obj : Object) : void {
			if (obj.getONDATA()) {
				Remote.getOperation("del").addEventListener(ResultEvent.RESULT, delResultHandler);
			}
			if (obj.getONFAULT()) {
				Remote.getOperation("del").addEventListener(FaultEvent.FAULT, delFaultHandler);
			}
			var args : Object = new Object();
			args.OBJECT = obj.data();
			RequestQueue[Remote.del(args)] = obj;
		}
 
		private function delResultHandler(event : ResultEvent) : void {
			RequestQueue[event.token].getONDATA()(RequestQueue[event.token]);
		}
 
		private function delFaultHandler(event : FaultEvent) : void {
			RequestQueue[event.token].getONFAULT()(RequestQueue[event.token]);
		}
	}
}

Here is Bender.cfc:

<!--- $ID $
 
	author : 		Toby Tremayne (toby@lyricist.com.au)
	written: 		2007-04-08"
	description: 	Creates a bridge between standard Transfer CRUD methods and actionscript objects
 
	Copyright 2007 Lyricist Software
 
	Unless required by applicable law or agreed to in writing, this software
	distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
	KIND, either express or implied.
 
	All rights to this code belong to Lyricist Software
 
	$Header $
 --->
<cfcomponent name="Bender" displayName="Bender : Transfer-ActionScript Bridge" hint="serves as a bridging service between ActionScript and Transfer">
 
	<cfset variables.instance = structNew() />
	<cfset variables.instance.Transfer = createObject("component", "transfer.TransferFactory").init("Datasource.xml", "Transfer.xml", "/Transfer/resources/definitions").getTransfer() />
	<cfif not isDefined("application")><cfapplication name="Bender" sessionManagement="true" /></cfif>
 
	<cffunction name="init" access="public" returntype="com.ls.bender.Bender" hint="default constructor">
		<cfreturn this />
	</cffunction>
 
	<cffunction name="get" access="remote" returntype="any" hint="">
		<cfargument name="class" type="string" required="true" />
		<cfargument name="key" type="any" required="true" />
 
		<cfscript>
			var oObj = instance.Transfer.get(arguments.class, arguments.key);
			stTemp = oObj.getMemento();
			stTemp.ASCLASSNAME = "com.ls.bender.transferobjects.#listLast(replace(oObj.getClassName(), """", "", "all"), ".")#";
			return stTemp;
		</cfscript>
	</cffunction>
 
	<cffunction name="save" access="remote" returntype="void" hint="">
		<cfargument name="object" type="any" required="true" />
 
		<cfscript>
			var key = getObjectPrimaryKeyValue(arguments.object);
			var oObj = instance.Transfer.get(arguments.object.className, key);
			var omd = instance.Transfer.getTransferMetaData(arguments.object.className);
			var iterator = omd.getPropertyIterator();
 
			writeoutput(key);
			// loop over the properties
			while (iterator.hasNext()) {
				name = iterator.next().getName();
				writeoutput("oObj.set#Name#(""#arguments.object["#name#"]#"")<br>");
				// copy the data from the struct we received into the Transfer Object
				evaluate("oObj.set#name#(""#arguments.object["#name#"]#"")");
			}
			name = omd.getPrimaryKey().getName();
			// set the primary key
			evaluate("oObj.set#name#(""#arguments.object["#name#"]#"")");
			// save the changes
			instance.Transfer.save(oObj);
		</cfscript>
	</cffunction>
 
	<cffunction name="del" access="remote" returntype="void" hint="">
		<cfargument name="object" type="any" required="true" />
		<cfscript>
			var key = getObjectPrimaryKeyValue(arguments.object);
			var oObj = instance.Transfer.get(arguments.object.className, key);
			instance.Transfer.delete(oObj);
		</cfscript>
	</cffunction>
 
	<cffunction name="getObjectPrimaryKeyValue" access="private" returntype="any" hint="">
		<cfargument name="object" type="any" required="true" />
		<cfscript>
			// get the TO metadata
			var omd = instance.Transfer.getTransferMetaData(arguments.object.className);
			if (isStruct(arguments.object)) {
				return arguments.object[omd.getPrimaryKey().getName()];
			} else {
				// set the primary key
				return evaluate("oObj.get#name#(""#arguments.object["#name#"]#"")");
			}
		</cfscript>
	</cffunction>
 
</cfcomponent>

and here is a sample autogenerated class for a “User” object (User.as):

package com.ls.bender.transferobjects {
	public class User {
 
		import mx.rpc.events.ResultEvent;
 
		private var userUUID : String;
		private var firstName : String;
		private var lastName : String;
		private var emailAddress : String;
		private var password : String;
		private var userToRole : Array;
		private var TransferClassName : String = "mglogin.User";
		private var ASClassName : String = "com.ls.bender.transferobjects.User";
		private var onData : Function;
		private var onFault : Function;
 
		public function User() {
			setUSERUUID("");
			setFIRSTNAME("");
			setLASTNAME("");
			setEMAILADDRESS("blah@blah.com");
			setPASSWORD("");
			setUSERTOROLE(new Array);
		}
 
		public function setUSERUUID(userUUID : String) : void {
			this.userUUID = userUUID;
		}
 
		public function getUSERUUID() : String {
			return this.userUUID;
		}
 
		public function setFIRSTNAME(firstName : String) : void {
			this.firstName = firstName;
		}
 
		public function getFIRSTNAME() : String {
			return this.firstName;
		}
 
		public function setLASTNAME(lastName : String) : void {
			this.lastName = lastName;
		}
 
		public function getLASTNAME() : String {
			return this.lastName;
		}
 
		public function setEMAILADDRESS(emailAddress : String) : void {
			this.emailAddress = emailAddress;
		}
 
		public function getEMAILADDRESS() : String {
			return this.emailAddress;
		}
 
		public function setPASSWORD(password : String) : void {
			this.password = password;
		}
 
		public function getPASSWORD() : String {
			return this.password;
		}
 
		public function setUSERTOROLE(userToRole : Array) : void {
			this.userToRole = userToRole;
		}
 
		public function getUSERTOROLE() : Array {
			return this.userToRole;
		}
 
		public function getCLASSNAME() : String {
			return this.TransferClassName;
		}
 
		public function getASCLASSNAME() : String {
			return this.ASClassName;
		}
 
		public function setCLASSNAME(className : String) : void {
			this.TransferClassName = className;
		}
 
		public function setASCLASSNAME(asclassname : String) : void {
			this.ASClassName = asclassname;
		}
 
		public function setONDATA(func : Function) : void {
			this.onData = func;
		}
 
		public function getONDATA() : Function {
			return this.onData;
		}
 
		public function setONFAULT(func : Function) : void {
			this.onFault = func;
		}
 
		public function getONFAULT() : Function {
			return this.onFault;
		}
 
		public function fill(event : ResultEvent) : void {
			this.setUSERUUID(event.result.USERUUID);
			this.setFIRSTNAME(event.result.FIRSTNAME);
			this.setLASTNAME(event.result.LASTNAME);
			this.setEMAILADDRESS(event.result.EMAILADDRESS);
			this.setPASSWORD(event.result.PASSWORD);
			this.setUSERTOROLE(event.result.USERTOROLE);
			this.setCLASSNAME(event.result.CLASSNAME);
			this.setASCLASSNAME(event.result.ASCLASSNAME);
		}
 
		public function data() : Object {
			var Obj : Object = new Object();
			Obj.userUUID = getUSERUUID();
			Obj.firstName = getFIRSTNAME();
			Obj.lastName = getLASTNAME();
			Obj.emailAddress = getEMAILADDRESS();
			Obj.password = getPASSWORD();
			Obj.userToRole = getUSERTOROLE();
			Obj.ClassName = getCLASSNAME();
			Obj.ASClassName = getASCLASSNAME();
			return Obj;
		}
	}
}

Finally, here’s an example of the use of Bender in mxml. I’m not doing anything clever here, just a simple get and a delete, but hopefully it gives you an idea.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
	<mx:Script>
		<![CDATA[
            import com.ls.bender.transferobjects.User;
            import com.ls.bender.Bender;
 
			private var oUser : User = new User();
			private var bender : Bender = new Bender();
 
			public function saveIt(e : Event) : void {
				oUser.setONDATA(showResults);
				bender.read(oUser, "mglogin.User", 'CF47B443-FD21-C25E-910A87B22004637A');
				oUser.setONDATA(showDeleted);
				oUser.setONFAULT(showDeleteFault);
				bender.del(oUser);
			}
 
			public function showDeleted(obj : *) : void {
				result_text.text = "Deleted";
			}
 
			public function showDeleteFault(obj : *) : void {
				result_text.text = "Delete Failed";
			}
 
			public function showResults(obj : *) : void {
				result_text.text = obj.getEMAILADDRESS();
			}
		]]>
	</mx:Script>
 
	 <mx:Panel title="RPC Test" paddingTop="20" paddingLeft="20" paddingBottom="20" paddingRight="20">
        <mx:TextArea x="10" y="36" width="319" height="113" id="result_text"/>
        <mx:Button x="250" y="157" label="saveObject" width="79" click="saveIt(event);"/>
    </mx:Panel>
</mx:Application>

All comments are welcome – it works with what you see here, but various things haven’t been done yet. No list functions are in there, complex properties aren’t being dealt with yet, mapping to custom cfcs is not done yet. Now that I have the basic concept working, I am going to expand the cfc next to handle validation returns and custom cfcs.

Feedback would be appreciated!

Post to Twitter Tweet This Post

Related posts:

  1. Templar Code
  2. Bender Explained
  3. Bender Expansion
  4. Bender Revived
  5. Mind-Bender
  • Trackbacks are closed
  • Comments (3)
  1. Just some thoughts -

    Probably better that you take Transfer as an arguments, rather than initialising it in Bender.

    That and I’ll have a chat to you about getMemento() function, you may have some issues with composition and lazy loading.

    Otherwise, looks pretty good!

    [Reply]

  2. Not all your questions were stupid :-)

    [Reply]

  3. Is there a reason you’re creating getter and setter methods?

    The AS3 convention is to use implicit get/set property methods instead.

    Probably best to downcase those property names. user.email is much better than user.EMAIL, which should be a constant, and lots better than having to call user.getEMAIL() which is really against the spirit of AS3 and Flex. It’s also much faster to access these properties than to access them on the dynamic Object returned from data() since the compiler knows the type and receiver. This means code completion will work in the IDE too!

    It’s really better to do initialization of defaults in the class body instead of the constructor as well since the compiler can optimize that away, but if you call methods in the constructor the compiler needs to assign a default value (null in non-primitive cases) to the properties and then set them to be the default value at runtime on each instantiation.

    I’d also make those onFault and onData functions into real broadcast events that you can listen for and make all transfer objects either extend a base class with the base shared methods, or an interface, so that you can return a generic TransferRecord of any type if you need to. That’ll get rid of the (obj : *) parameter on the Bender component which means the compiler can optimize access to the functions on the transfer object and it moves all that shared code outside the CF generator and into AS code you can update without digging around in the guts of the code generator.

    The way I’ve handled nice access in a project of mine was to define a const property on each generated object with a name like PROPERTIES, and then use that for figuring out what the properties are for objects inside the handler classes. That way you can move the fill() method into the Bender class and iterate over the const properties collection. Or if you’re feeling really dynamic you could just iterate over the properties in event.result and use hasOwnProperty() to check if the Transfer object has the correct method and call it that way. This saves you from generating lots and lots of code too.

    Overall this looks nice a pretty nifty project. :)

    [Reply]