Air is 1.0

Back in my hotel room after the Air 1.0 launch. Was a great event, this year is going to be a wonderful time to be a developer, especially in the Adobe space. There was a good turnout but almost none of the cf / flex people i usually see around - shame on you all ;p

If anyone hasn't gotten into air yet give it a shot - it's well worth your time and I think in the next 6 months we're going to see some massive changes in our industry.

Helping Hands

i have made it a point over the years to keep in touch with developers I meet and offer help wherever I can. I'm not great at posting to the mailing lists because I usually get so caught up in what I'm doing that I forget to read them :)

Lots of people ping me during the day (and sometimes night) for help with cf / flex/ javascript / css etc etc, and there are a bunch of people on my list who I have asked for help with the same when I'm having a brain fart or when I'm doing something i've never done before. Some of the people on my list have spent an awful lot of time helping me on occasions, and I always try to return the favour.

The point I'm making is this - network with other developers, stay in touch and reach out when you need help. But more importantly, be ready to help someone out if they ask you a question too - this doesn't mean drop everything you're doing and do their work for them, but if you've got 5 minutes then share it. A lot of people, including experienced developers, don't realize how powerful a tool your network of tech savvy friends can be. I'll be going into more detail about this in my talk at Scotch and (hopefully) WebDU, but I just wanted to throw a quick word out to any who might be listening.

Today I was doing battle with some really appalling actionscript 1 code. I love actionscript 3.... a lovely fellow named Campbell Anderson, who has answered more than a few of my irritating as1 questions lately, has quite patiently helped me through some very weird issues and saved me a lot of time and frustration. I'll be making sure he knows he's appreciated :)

This is one of the great strengths of the cf community, and i'm finding it more and more in flex and flash too. Be generous with your time, and you'll find that when that moment comes when you really are stuck in a hole for whatever reason, you have allies on hand to help you out.

After all, many minds make light work.

Speaking at Scotch on the Rocks

Yup I'll be speaking at Scotch on the Rocks in Edinburgh, Scotland in june. I'm very very excited, and hopefully won't be completely boring - I'm speaking alongside a raft of the top people in our industry so a little intimidating :D Maybe I'll just get drunk before my presentation...

If you're going to Scotch I'll look forward to seeing you there!

Bender Revived

After a 6 month hiatus or so where I've been flat out working with another group and haven't had time to look at Bender, I was speaking last night with Adam Lehman and a few others who have inspired me to dig out Bender and get back into it. I want to get it working as soon as possible so the code is releasable and allows the basic features of Transfer - Mark Mandel and I also chatted about potentially using the data services included with CF to do live syncing between Transfer and Bender although not yet sure whether that's a good idea :)

Anyways - Bender is back and I'll be finishing it off and looking for testers. If anyone has requests or suggestions fire away.

Toby

I hate dates and locales

Yes Yes Bad Blogger, I know. smack. Anyways....

I'm having a problem where i'm using a date selector in flex and selecting a date (client running on australian machines), but when its saved to the server it seems to be going in as a US date based on the current server time. How do I fix this? I've tried mucking about with the locale setting in the compiler arguments, I've tried just using CF_SQL_DATE instead of _TIMESTAMP but to no avail. If there are any readers left on this blog I'd appreciate any ideas.

I hate working with dates and regions, it's definitely on my top 5 least enjoyable things in programming.

Flex goes open source!

Wow. look at Ted Patrick's blog - Flex is being made open source. What a brilliant decision! The guys on the team must have done some very hard work to get this to happen, and I am absolutely over the moon - not only does it open flex development to more people it means the potential contributions and leaps and bounds to come from the community could be staggering. It's a good time to be in development :)

Bender Alpha Release

Finally! Ok you can now download an alpha release of Bender:

Bender Alpha 01 (Bender_alpha01.zip)

Please bear in mind this is a first release, and while it works for me your mileage may vary wildly. I would really appreciate hearing from anyone who had time to try this out, and has bugs, suggestions, corrections or criticisms.

Download the zip file, and expand it into your webroot, or somewhere that a cf mapping points to as "com". You shouldn't need to setup anything specific for flex, it will just use the default flash remoting adapters.

The following steps should get you up and running with Bender:

  1. Make sure Transfer release 0.6.3 or higher is installed and working
  2. Make sure /com points to the com directory from the bender zip file
  3. Browse to http://yourserver/com/ls/bender/tools/BenderClassGenerator.cfm
  4. Change the paths to match your settings and submit the form - this will create an actionscript class file for each definition in your Transfer.xml
  5. I haven't made the auto compiled swc bit yet, so for the moment, make sure you drop these class files into your flex application, along with com/ls/bender/as/Bender.as and import them in your code
  6. In your flex code you can now run operations like: bender.read(oUser, "User", userPK);

Operations:

bender.read(Class class, String TransferClassName, String primaryKey);

The first parameter for a read is an already instantiated object - one of the classes bender generated for you. You pass this in, and when the read is complete the object will contain the relevant data. The second parameter is the name of the object definition - the "class" attribute in your Transfer.xml The third is the unique identifier you'd normally use to retrieve that object via transfer.

make whatever changes you like to the data in the As objects then use:

bender.save(Class class);

just pass in the actionscript instance and this will automatically save back to the db.

MAPPINGS You can map functions to a custom CFC if you choose. For example, if you wanted a UserManager.cfc to do something with the user objects before they went to Transfer for saving, you could create a Bender.xml like so:

<?xml version="1.0" encoding="UTF-8"?>
<bender>
   <mappings>
       <mapping class="User" action="save" mapobject="UserManager" mapmethod="save" />
   </mappings>
</bender>

This will mean that when you use bender.save(myObj) in flex, the data will go to Bender on the coldfusion side and get translated into an appropriate TransferObject which will then be passed on to UserManager.save(TO). If you don't create a mapping for a particular method and class, it defaults to sending straight to Transfer.

CAVEATS: I had some issues in actionscript using methods named get and delete, so in Bender you call bender.read() and bender.del() instead. This may be something I did wrong and if so I'll fix it - but for the moment that's how it works.

Delete works the same way as save.

EVENTS When you have instantiated one of the bender-generated actionscript classes, you can set oMyObj.OLNDATA() and oMyObj.ONFAULT() to provide functions to which Bender will map upon data population or fault. For example, if you want a message shown to the user when you save something, you could do this:

public function showResults(obj : *) : void {
result_text.text = "Save Successful";
}
public function showFailure(obj : *) : void {
result_text.text = "Save Failed";
}

oObj.ONDATA(showResults);
oObj.ONFAULT(showFailure);
bender.save(oObj);

Please download the code and take a look - and I'd love to hear from anyone on ways to improve it. Let me know what works and what doesn't and contact me on this blog or on skype / icq / email etc etc.

P.S. I haven't added the SWC generation yet, but am working on it now and hopefully that will make integration of auto-generated as classes and the bender code into your flex app much easier. If anyone has any preferences or suggestions for this bit, please let me know.

Mind-Bender

A quick update - the Bender alpha release *is* imminent, but I got held up on some recursive code that was being obstreperous and I'm having to rethink the way I was handling cfc mappings.

I wanted to set it up so you could instantiate Bender and pass it in all the cfc's you want to map to, or be able to setup a ColdSpring definition with the relevant auto-wiring, but I realised that the flex component is talking directly to the bender component so this wouldn't work.

What I'm doing now is creating BenderService.cfc - a facade that manages the instantiation of Bender. You change the instance code at the top of this cfc to however you want to instantiate bender (EG a CS getBean() call) and the flex component talks to BenderService.cfc, which just passes traffic back and forth to Bender.cfc.

At the moment I'm wrestling with the best way to make it all happen, trying to pass in instantiated components and have them stored - it's getting there but a few finicky details with unnamed arguments etc are causing me grief. More as soon as it comes.

PS. btw any comments on this would be greatly appreciated - any suggestions on how best to set it up, especially from anyone who knows coldspring would be fantastic.

Bender has deep composition

Ok just a quick update for those who care - Bender now has working deep recursions on composite objects in both directions. This means if you have for example a manytomany in your object definition, and within the objects that get put into that array you *also* have a manytomany definition (etc etc ad nauseum) all will be retrieved and translated back and forth between AS and coldfusion.

If this makes no sense, feel free to ignore - but composite objects work properly now :) (both struct and array collections).

I'm now moving on to the mapping of custom CFCs - more updates to come.

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!

Bender Explained

Ok Bender seems to be generating some interest and I've found that a lot of people aren't really understanding my explanation of what it's supposed to achieve, so here's an attempt at a clearer definition.

If you're writing a flex app with database connectivity, your workflow should look like this:

  1. Install Transfer on server
  2. Write Transfer.xml
  3. Install Bender on Server
  4. Run Bender AutoGenerating Tool
  5. Include the resulting SWC library in your Flex App
  6. Instantiate the generated classes in your flex app, fill 'em with data, and make calls to Bender as you would to Transfer - for example Bender.save(myProductClass)

That's it - nothing more complicated. The primary idea is to give flex developers a seamless access to the benefits of database persistence via Transfer. In theory, if you can setup Transfer, you can add db persistence to your flex app with Bender, without any CF knowledge.

Bender itself is comprised of 3 components - Bender.cfc, Bender.as and BenderAutoGenTool.cfm. Bender.as gets compiled into a swc along with classes for all your objects defined in the Transfer.xml. Bender.cfc sits on the server. Looks like this:

Here are a couple of examples of how something might happen:

Creating and Saving an object:

The steps in this workflow are as follows (assuming Product.class is one of the autogenerated actionscript classes):

  1. Instantiate Product.class and use setters to fill with data
  2. Call Bender.save(myProduct);
  3. Bender makes a RemoteObject call to the Bender.cfc save() method, passing the actionscript class, which Flash Remoting will convert into arrays and structures
  4. Bender.cfc looks at the received data, determines which TransferObject it needs and instantiates the relevent TransferObject
  5. Bender loops through the data received, calling matching setters on the new TransferObject
  6. We check to see if we have a specific hander (CFC and method name) for save() on Product objects
  7. If we don't have a specific handler registered then we pass directly to Transfer itself, using Transfer.save(ProductTransferObject) - END
  8. If we DO have a specific handler registered, we pass the TransferObject to that cfc and method instead, for example ProductManager.save(ProductTransferObject)
  9. ProductManager.cfc does it's thing, making any required manipulations to the ProductTransferObject
  10. ProductManager.cfc then passes to Transfer using Transfer.save(ProductTransferObject)

Before anyone asks, in the above example I haven't bothered setting out the steps for the case in which ProductManager.cfc does validation and has to pass it back with messages etc, but the ability would be there (I haven't decided yet on any specific way to handle this, but that will come).

Here's an example of retrieving an object from the flex app - imagine you have a form in your flex app allowing the user to enter the id number of a product, and you want the app to then retrieve that product.

  1. User enters an ID and requests the product
  2. Call Bender.get("product", productID);
  3. Bender makes a RemoteObject call to the Bender.cfc get() method, passing the productID as a parameter, also passing the classname of Product and the action GET
  4. Bender.cfc looks at the received data to determine the Transfer classname we're looking for
  5. Bender checks to see if there is a specific handler registered for this class and action
  6. If we don't have a specific handler registered then we pass directly to Transfer itself, using Transfer.get(classname, arguments.productID) - SKIP TO 10
  7. If we DO have a specific handler registered, we pass the cfc and method to that handler instead, for example ProductManager.get(arguments.productID)
  8. ProductManager.cfc does it's thing, making any required manipulations to the ProductTransferObject
  9. ProductManager.cfc then passes to Transfer using Transfer.get(classname, arguments.productID)
  10. Transfer returns the requested TransferObject back to either Bender.cfc or ProductManager.cfc accordingly (10.5 - PageManager.cfc then passes the Product TransferObject back to Bender.cfc)
  11. Bender.cfc pulls the data out of the TransferObject and puts it into structs and arrays
  12. Bender.cfc passes the resulting struct back to Bender.as as the result of the RemoteObject call
  13. Bender.as takes the received data (which Flash Remoting has converted into a simpe AS Object) and instantiates the required AS Class (IE Product), using the setters on the the newly instatiated Product to pass in the received data then passes the ProductClass back to the Flex app
  14. Whatever handlers are in the flex app for this event are fired and the flex app does it's thing

Hopefully this all makes a bit more sense. Please bear in mind that it's not 100% finished yet, and when it is, I'll be asking for feedback and may change the way some of this works if somebody sees a better way to do it. I hope that for now this will serve as a better explanation of what I'm trying to achieve.

More Entries