In my last post I mentioned in passing some code we wrote to allow DWR calls inside Flex2 code. My goal with this post is to provide a short, clear example of doing this and then describe how we applied that to our project. This doesn’t handle timeouts, call batching, reverse Ajax/Comet, and probably some other DWR features I am forgetting. There are many improvements that could be made to it and it was only used in FireFox. You may want to be familiar with the ExternalInterface of Flash (which hopefully will have its bug fixed soon) and with Javascript and DWR before reading on. I apologize for any faulty formatting - still using vanilla, hosted wordpress and it doesn’t make code especially easy to work with.
There are 3 pieces to making it work:
- An Html/JS container that your swfs must be run in.
- An AS3 class that talks to the Html/JS container.
- A layer of JS to make the DWR calls and route callbacks to AS3.
The Html/JS Container
For the sake of brevity here is a wrapper that will work. For a more robust wrapper take a look at the Html/JS wrappers that FlexBuilder generates and base your custom DWR wrapper files off of those. You need 2 wrappers - one for run mode and one for debug.
DWRContainer.html & DWRContainer-debug.html
<html>
<head>
<!– Core JS Stuff –>
<script type=”text/javascript” src=”dwrWrapper.js”>
</script>
<script type=”text/javascript” src=”/ajax/engine.js”>
</script>
<!– DWR Controllers –>
<script type=”text/javascript”
src=”/ajax/someDWRController.js”></script>
</head>
<body style=”padding: 0px; margin: 0px”>
<object id=’flexObject’
classid=”clsid:d27cdb6e-ae6d-11cf-96b8-444553540000″
codebase=’http://download.macromedia.com/pub/
shockwave/cabs/flash/swflash.cab#
version=9,0,0,0′
height=’100%’
width=’100%’>
<param name=’src’ value=’MyFlexApp.swf’/>
<param name=”allowScriptAccess” value=”always”/>
<embed name=’flexObject’ src=’MyFlexApp.swf’
allowScriptAccess=’always’ pluginspage=’http://
www.macromedia.com/shockwave/download/
index.cgi?P1_Prod_Version=ShockwaveFlash’
height=’100%’
width=’100%’/>
</object>
</body>
</html>
The details to notice are the inclusion of dwrWrapper.js, the DWR engine.js file, and then any number of DWR controllers that you want to use in Flex2. Ideally these DWR controller JS files would have their content injected dynamically but they are small files so I don’t think it is a big issue to include them all and this makes the example simpler. Now to create the debug version just copy the above contents but use MyFlexApp-debug.swf for the src values. Once you have your wrappers built, you will need to tell FlexBuilder to use these when running/debugging. To set this go to the Run menu, click either the Run or Debug option that is expandable, then click Other. In the “Url or path to launch” section you can enter your run and debug wrapper locations.
Now lets look at the JS file.
The Javascript Layer
dwrWrapper.js
/**
* Receives requests from AS3 for a Dwr call
* @param wrapUUID The unique identifier for the wrapper
* used to make the call
* @param jsMethodName Name of JS method to call
* @param jsMethodArguments Arguments for JS method
* @param asCallbacks The callback object containing AS3
* function UUIDs
*/
var dwrCall = function(wrapUUID, jsMethodName,
jsMethodArguments, asCallbacks){
var methodToCall = eval(jsMethodName);
// add callback to arguments
jsMethodArguments.push(buildDwrCallback(
wrapUUID, asCallbacks.callback,
asCallbacks.errorHandler));
// call the desired JS method
methodToCall.apply(null, jsMethodArguments);
}
/**
* Constructs a callback map for a Dwr call.
* @param wrapUUID The UUID of the AS3 wrapper
* @param asCallback UUID of the AS3 function to call
* upon successful callback
* @param asErrorHandler The UUID of the AS3 function to
* call if there is an error
* @returns A JS callback object for a DWR call
*/
var buildDwrCallback = function(wrapUUID, asCallback,
asErrorHandler){
var jsCallback = handleDwrCallback;
var jsErrorHandler = handleDwrError;
var wrappedJsCallback = jsCallback;
var wrappedJsErrorHandler = jsErrorHandler;
// wrap the callbacks in functions that call them
// with dwr response + AS3 callback info
jsCallback = function(dwrResponse){
return wrappedJsCallback.call(null, wrapUUID,
dwrResponse, asCallback);
}
jsErrorHandler = function(dwrResponse){
return wrappedJsErrorHandler.call(null, wrapUUID,
dwrResponse, asErrorHandler);
}
return {callback: jsCallback,
errorHandler: jsErrorHandler};
}
/**
* Called when a DWR function returns -
* sends response to AS3 code
* @param wrapUUID UUID for the wrapper
* @param dwrResponse The actual response received
* from the server through Dwr
* @param asCallback UUID for the AS3 function to call
*/
var handleDwrCallback = function(wrapUUID, dwrResponse,
asCallback, extraArguments){
var asCallbackCall = “document['flexObject'].
handleJsCallback” + wrapUUID.replace(/-/gi, “”) +
“(dwrResponse, asCallback)”;
eval(asCallbackCall);
}
/**
* Called when a DWR call fails -
* sends error to AS3 code
* @param wrapUUID UUID for the wrapper
* @param dwrResponse The actual error response received
* from the server through Dwr
* @param asErrorHandler UUID for the AS3 function
*/
var handleDwrError = function(wrapUUID, dwrResponse,
asErrorHandler){
var asErrorCall = “document['flexObject'].
handleJsError” + wrapUUID.replace(/-/gi, “”) +
“(dwrResponse, asErrorHandler)”;
eval(asErrorCall);
}
Some comments on this JS code:
- Instead of passing the string names for the AS3 functions to use for callbacks and errors I used UUIDs. On the AS3 side a mapping from UUID to function reference is maintained.
- I use eval above to call the AS3 functions registered on ExternalInterface because the apply function isn’t available.
- I assigned each usage of the wrapper a UUID as well that is taken into account when calling back into AS3. By appending these wrapper UUIDs to the callback and errorHandler AS3 functions registered on the ExternalInterface we can ensure the Dwr callbacks are going to the correct AS3 code. Each DWR controller will have a separate wrapper instance on the AS3 side each with its own UUID.
- In the
buildDwrCallback function we are using the technique described here to pass the AS3 callback/errorhandler information through the DWR call such that we can use it in the handleDwrCallback/handleDwrError functions. If the above makes sense (and it will look weird to someone new to JS) you should be able to see how extra arguments could be passed on top of the callback info for the benefit of your AS3 code.
- I strip the dashes from the wrapper UUIDs because they were causing problems with ExternalInterface and calling the registered AS3 functions. You will notice below that the dashes are stripped to match on the AS3 side of things.
So that covers the Html/JS wrappers for running your Flex2 code against DWR. The last step is the AS3 class that knows how to communicate with this wrapper.
The Actionscript Layer
DwrWrap.as
package someLib
{
import flash.external.ExternalInterface;
import mx.collections.ArrayCollection;
import mx.utils.UIDUtil;
/**
* This class provides access to Dwr. It assumes
* that flex is being used in an Html container that
* has access to dwrWrapper.js.
* Each instance wraps a single Dwr controller.
*/
public class DwrWrap
private var functionMap:Array;
private var wrapUID:String;
public var controllerName:String;
/**
* Constructor for DwrWrap. Initializes function
* map and registers ExternalInterface callbacks
* @param controllerName The name of the
* JS/Dwr controller to use
*/
public function DwrWrap(controllerName:String){
this.controllerName = controllerName;
this.functionMap = new Array();
this.wrapUID = UIDUtil.getUID(this);
// register AS3 callbacks as name + UUID
ExternalInterface.addCallback(”handleJsCallback” +
this.wrapUID.replace(/-/gi, “”),
handleJsCallback);
ExternalInterface.addCallback(”handleJsError” +
this.wrapUID.replace(/-/gi, “”),
handleJsError);
}
/**
* Method that calls JS using ExternalInterface.
* @param jsMethodName JS/Dwr function to call
* @param jsArgs A single argument or an
* ArrayCollection of arguments to use in JS call
* @param callback An untyped object with callback
* and errorHandler properties
*/
public function serverCall(jsMethodName:String,
jsArgs:*, callback:Object = null):void{
var jsCallback:Object = new Object();
// map UUIDs to AS3 callback function references
var callbackUID:String =
UIDUtil.getUID(callback.callback);
jsCallback.callback = callbackUID;
functionMap[callbackUID] = callback.callback;
var errorUID:String =
UIDUtil.getUID(callback.errorHandler);
jsCallback.errorHandler = errorUID;
functionMap[errorUID] = callback.errorHandler;
// make sure jsArgs is an array - assumption in JS
if (jsArgs is ArrayCollection){
jsArgs = jsArgs.toArray();
}
else if (!(jsArgs is Array)){
var argArray:Array = new Array();
argArray.push(jsArgs);
jsArgs = argArray;
}
else if (jsArgs == null){
jsArgs = new Array();
}
ExternalInterface.call(”dwrCall”, this.wrapUID,
(controllerName + “.” + jsMethodName), jsArgs,
jsCallback);
}
/**
* This method receives the callback from JS and
* then calls the appropriate AS3 callback
* @param dwrResponse The response from Dwr
* @param asCallback UUID of AS3 function to call
*/
public function handleJsCallback(dwrResponse:*,
asCallback:String):void{
// use the UUID to look up the function reference
var callbackFunction:Function =
functionMap[asCallback];
if (callbackFunction != null){
delete functionMap[asCallback];
callbackFunction(dwrResponse);
}
}
/**
* This method receives errors from JS and
* then calls the appropriate AS3 errorHandler
* @param dwrResponse The error response from Dwr
* @param asErrorHandler UUID of the AS3 function
*/
public function handleJsError(dwrResponse:*,
asErrorHandler:String):void{
// use the UUID to look up the function reference
var errorFunction:Function =
functionMap[asErrorHandler];
if (errorFunction != null){
delete functionMap[asErrorHandler];
errorFunction(dwrResponse);
}
}
}
}
Some comments about the AS3 code:
- Notice that by routing all callbacks and errorHandlers through 2 AS3 functions we only have to register those 2 function on the ExternalInterface. These two functions need to be registered for each DWR controller so we append a UUID to the names to ensure uniqueness of function names.
- AS3 functions to use for DWR callbacks and errors are sent to JS as UUID strings. When we get calls back from the JS we can then look up the actual function references and call them directly. This is nice because it allows the AS3 callback object to contain actual references and not strings.
Finally, you can make DWR calls from AS3 that look like this. Assume you have a DWR controller exposed as someDWRController.js and that this controller has an addNumbers method that takes 2 arguments and returns the sum.
Usage Example
// instantiate a wrapper for the DWR controller
var wrap : DwrWrap = new DwrWrap(”someDWRController”);
// call a function on that controller
wrap.serverCall(”addNumbers”, 5, 6,
{callback: someASCallback, errorHandler: someASErrorHandler});
// the callback and errorHandler
public function someASCallback(theSum): void{
Alert.show(”Should be 11: ” + theSum);
}
public function someASErrorHandler(errorInfo): void{
Alert.show(”Stuff broke”);
}
This should work for making basic DWR calls. In our project Mark cleaned this code up, allowed extra passthrough arguments to be passed to callback functions, injected the DWR Controller JS file content dynamically, and then integrated this logic into our Cairngorm-based application. DWR services were then listed and looked up from a Services.mxml file and executed by delegates like any other service. The callback and errorhandler functions for DWR calls were bound to the result/fault methods of our command objects and upon return the results from Dwr were translated into typed objects. I won’t share that code because I didn’t write it.
Hopefully just this will help someone stuck with Flex2 in an environment where DWR is being used. If you haven’t already read my opinions on Flex2 and you are trying to decide between Flex2 and Html/JS you should.
If you are determined to use Flex2 with DWR you really need to read about the ExternalInterface bug that may force you to refactor your value objects depending on their complexity.