The First and Only Magazine for Macromedia MX™
   
 
Notes from the Flash, Flex and ColdFusion trenches

Category List

Quick Poll

Where are you located?
North America
South America
Europe
Asia
Africa
Australia

My RSS Feeds








Hit Counter

Total: 1,350,997
since: 20 Jan 2005

Search Box

 

Search The Web

Google

MAX 2008

Fun Stuff

Mailing List

Got A Question?

Got A Question?

Leave a comment by the appropriate Entry, or email me

Tag Cloud

                                                                                                                       

Creating a DataManager in ActionScript 3.0 for Flex 2.0

posted Friday, 2 December 2005

I've updated the DataManager with a fix for concurrency issues over here.


When building Flex applications, I like to centralize data access, to remove the need for mx:WebService tags to be sprinkled throughout the application.  While the FAST data services helped out in this respect for Flex 15, it is not yet available for Flex 2 (and I've found porting it over manually to be a non-trivial task).


What I've created is a Singleton class for the DataManager, which will allow 1 and only 1 instance to be created for each unique WSDL.  Developers call a "makeRemoteCall" method on the appropriate instance, and pass it the name of the method to be called, as well as a uniquely named event that will be invoked when the results are back.


2/3/05 Note - The code below has been updated to run in the recently released beta 1




package managers {
import flash.events.EventDispatcher;
import mx.rpc.soap.WebService;
import mx.rpc.events.ResultEvent;
import mx.rpc.events.FaultEvent;
import mx.rpc.AbstractOperation;
import events.DataManagerResultEvent;
import flash.util.*;
/** DataManager - singleton class which enforces only
a single object is created for eachwsdl. To
access DataManager, use getDataManager(wsdl:String) */

public class DataManager extends EventDispatcher {
private var ws:WebService;
private var eventName:String;
// hashmap of instances for each wsdl
private static var instanceMap:Object = new Object();
public function DataManager(pri:PrivateClass, wsdl:String){
this.ws = new WebService();
ws.wsdl = wsdl;
ws.loadWSDL();
ws.useProxy = false;
}
public static function getDataManager(wsdl:String):DataManager{
if(DataManager.instanceMap[wsdl] == null){
DataManager.instanceMap[wsdl] = new DataManager(new PrivateClass(),wsdl);
}
var dm:DataManager= DataManager.instanceMap[wsdl];
if(dm.ws.canLoadWSDL()){
return dm;
} else {
throw new Error("BAD WSDL:"+wsdl);
}
}
public function makeRemoteCall(methodName:String,eventName:String, ...args:Array):void{
this.eventName = eventName;
var op:mx.rpc.AbstractOperation = ws[methodName];
ws.addEventListener("result",doResults);
ws.addEventListener("fault",doFault);
if(args.length >0){
op.send.apply(null,args);
} else {
op.send();
}
}
private function doResults(result:ResultEvent):void{
var e:DataManagerResultEvent = new DataManagerResultEvent( eventName, result.result);
this.dispatchEvent(e);
}
private function doFault(fault:FaultEvent){
this.dispatchEvent(fault);
}
public override function toString():String{
return "DataManager";
}
}
}
/** PrivateClass is used to make DataManager constructor private */
class PrivateClass{
public function PrivateClass() {
}
}



This class starts simply enough, with a package declaration (I've set this to be in a managers.* directory), followed by imports for each of the classes I'll use here (AS3 requires explicit imports of all classes).


Next the class is defined as a subclass of EventDispatcher (a fairly lightweight class, which instantiates the framework for broadcasting events).  Three private properties are then declared, ws to hold an instance of the WebService class; eventName which is the name of the event to be broadcast when results are received; and a static property instanceMap, which holds a HashMap (just an Object in AS) of instances of the web services.


Since AS3 doesn't currently have private constructors (I'm still hoping this gets added into the language before its released), the constructor takes an instance of PrivateClass as an argument.  Since PrivateClass is defined outside of the package declaration, its only available within this same file.  This ensures that the constructor can not be called externally, essentially giving us a private constructor.  The other argument to the constructor is the WSDL for the WebService to use.  Internally, the constructor creates an instance of the WebService class, sets the wsdl, and calls loadWSDL() to initially load the WSDL (this is required any time you manually instantiate WebServices in ActionScript, but happens automatically when you use the WebService MXML tag.)


Next, the public static method getDataManager is defined.  This takes a WSDL URL as an argument.  Internally, this method determines if a WebService instance already exists for that WSDL, or if it needs to be created.  Either way, a handle to the WebService is generated, it validates that it can retrieve the WSDL (using the canLoadWSDL() method) and returns the appropriate instance (or throws a run time error if it can't retrieve the wsdl)


The instance method makeRemoteCall is defined next, which takes a minimum of 2 arguments: the name of the remote method, and the name of the event to be broadcast when results are retrieved.  Any extra arguments passed in (as indicated by the ... args:Array) will be passed on to the remote method.  An AbstractOperation instance is created to refer to the method on the remote object.  Event Listeners are added for results and faults, and the operation is triggered.  Note, the conditional statement determines if there is data to be passed to the server or not, if so, the apply() method is used to use the args array as a series of arguments (see this entry for details on apply), otherwise, the method is called with no arguments.


The two event handlers follow.  If results are successfully returned, the doResults method fires.  This creates an instance of the DataManagerResultEvent, and dispatches it.  If a fault is returned, it is simply re-dispatched.


Here is the definition of the DataManagerResultEvent:


package events {
import mx.rpc.events.ResultEvent;
import flash.events.Event;
import flash.util.*;
public class DataManagerResultEvent extends Event {
public var result:Object;
public function DataManagerResultEvent(type:String,result:Object){
super(type);
this.result = result;
}
public override function clone():Event{
return new DataManagerResultEvent(type, result);
}
}
}

This is is simple Event subclass, which holds the result object.  We didn't need a class like this back in the Flex 1.5/AS2 days, as events were not strongly typed, but in the strongly typed world of AS3, I find myself creating more custom Event classes.


Finally, we can test it with a MXML page.  Here is the tester:


<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.macromedia.com/2005/mxml" creationComplete="initApp()">
<mx:Script>
<![CDATA[
import mx.rpc.events.FaultEvent;
import managers.DataManager;
import flash.events.Event;
import events.DataManagerResultEvent;
import mx.controls.Alert;
private var catManager:DataManager;
private var prodManager:DataManager;
private function initApp():void{
// get data managers
var catWsdl:String = "http://www.flexgrocer.com/cfcs/category.cfc?wsdl";
var prodWsdl:String = "http://www.flexgrocer.com/cfcs/prodByCategory.cfc?wsdl";
catManager = DataManager.getDataManager(catWsdl);
prodManager= DataManager.getDataManager(prodWsdl);
// setup event listeners
catManager.addEventListener("catsRetrieved",handleCats);
catManager.addEventListener("fault",showFault);
prodManager.addEventListener("prodsRetrieved",handleProds);
prodManager.addEventListener("fault",showFault);
//get categories
catManager.makeRemoteCall("getCategories","catsRetrieved");
}
private function getProdsForCat(event:Event){
var catId:int = List(event.currentTarget).selectedItem.CATEGORYID;
prodManager.makeRemoteCall("getProdsByCategory","prodsRetrieved",catId);
}
private function handleProds(event:DataManagerResultEvent){
prods.dataProvider = event.result;
}
private function handleCats(event:DataManagerResultEvent){
cats.dataProvider = event.result;
}
private function showFault(event:FaultEvent):void{
Alert.show("Web Service Error: \n "+event.fault.description);
}
]]>
</mx:Script>
<mx:List id="cats"
labelField="CATEGORY"
change="getProdsForCat(event)"/>
<mx:DataGrid id="prods"
width="100%"/>
</mx:Application>

If you have the Flash Player 8.5 installed, you can see this up and running here (source code can be obtained from a right click on that page). 


Tien Nguyen  created a variation on this to use RemoteObject for connecting to CFC's with the CFAdapter.  When I have more time, I'll work on creating a class which can make use of both

links: digg this    del.icio.us    technorati    reddit




1. Sudeep left...
Thursday, 22 December 2005 1:16 pm

Hi, I copied you class code put in a actionscript file. I put the file in the directory which has MXML. While runnning MXML, Flex 2 Builder complains that it cannot find the actionscript. What am I doing wrong? What should I do to make Flex 2 Builder find the actionscript that has your code.

Thank you.


2. Jeff Tapper left...
Thursday, 22 December 2005 1:26 pm

The DataManger class needs to be in a directory named managers. The MXML application file should be in the folder above the managers diretory. Also, be aware, the DataManagerResultEvent class should be in a diretory called events.

All told, the structure should look like this: Flex app root diretory conatins main mxml file root diretory has 2 subdirectories, events and managers.

DataManager.as is in the managers directory, DataManagerResultEvent should be in the events director.


3. Tim left...
Monday, 26 December 2005 9:08 pm

Hi Jeff,

Great work, I setup the example above and it works like a charm.

Was wondering if you have experience with CFadapter.

Cheers,

Tim


4. Tim left...
Saturday, 31 December 2005 11:46 pm

Modified your code and got CFadapter to work.

If anyone is interested, I can post the code.

Tim


5. Thunder left...
Friday, 20 January 2006 6:44 pm

I considered doing this, however it did not seem to fit my problem.

The case I have is that I am making repeated calls from different controls all to the same wsdl, and often to the same operation. say I have 3 instances of the same data-driven chart control that all use the same web service operation to get their data. Keep in mind that the single instance of the WS will be creating multiple simultaneous calls to the same operation. When the results are returned, it won't be able to distinguish which result went with which call...

Still haven't thought of a good solution to this.

Thunder


6. Nicolas left...
Friday, 3 February 2006 8:25 am

Hi Jeff, I just installed FlexBuilder2 and try to test your exemple. (I'm novice) But I have 2 errors with the compilation in DataManager.as, in private class PrivateClass:

1.A file found in an actionscript-classpath must have an externally visible definition. If a definition in the file is meant to be externally visible, please put the definition in a package.

2.'private' attribute may only be used on class property definitions.

Do you have an idea ? Thanks Nicolas


7. Jeff Tapper left...
Friday, 3 February 2006 12:22 pm

Hmm, the new public beta doesnt want to support a Private class. I'll dig into this and post an updated version. a few other errors will crop up when you get past this first one, based on other differences with the alpha and beta, such as Void vs void.


8. Jeff Tapper left...
Friday, 3 February 2006 2:17 pm

Ok, the code is updated to run in beta 1. Here are the changes i had to make:

  • - Void is now void (all lowercase)

  • - Means to declare a private class has changed

  • - fetchWSDL() is now loadWSDL()

  • - canFetchWSDL() is now canLoadWSDL()


9. Serge van den Oever left...
Friday, 3 February 2006 7:11 pm :: http://weblogs.asp.net/soever

In beta1 I have trouble with the ProvateClass. I can't use the private keyword (not supporten on class level), and if I specify nothing like you did I get:

A file found in an actionscript-classpath can not have more than one externally visible definition. managers:SharePointServiceManager;managers:PrivateClass

Any ideas to solve this? I want to make my constructor "private", like you did!


10. Jeff Tapper left...
Friday, 3 February 2006 8:17 pm

Serge - if you take a look at the release notes... http://labs.macromedia.com/wiki/index.php/Flex:Alpha_1_to_Beta_1_Changes#Ac cess_specifiers

You can no longer have a private or internal class inside a package. If you have a public class which uses helper classes that don't need to be public, place them after the package statement.

package foo.bar{  
    public class MyClass{  
    }  
}  
class MyHelperClass{  
}  
This pattern makes MyHelperClass visible to code only in this one file, not to all code in package foo.bar.


11. richard boelen left...
Tuesday, 7 February 2006 9:11 am :: http://www.everflash.com

Thanks for this example ! I am working on connecting flex to .Net webservices, which works fine, except that when I use multple arguments in a webservice call, I get 'HTTP request error'.

After some investigating the makeRemoteCall function of datamanager, I put a Alert.show(args.length.toString) in this call, and the call then works fine,...strange behaviour to me, any idea ?

changed code:

	 	public function makeRemoteCall(methodName:String, eventName:String, 
...args:Array):void{
	 		this.eventName = eventName;   
	 		var op:mx.rpc.AbstractOperation = ws[methodName];   
	 		ws.addEventListener("result",doResults);   
	 		ws.addEventListener("fault",doFault);   
 			Alert.show(args.length.toString());
	 		if(args.length >0){   

	 			op.send.apply(null,args);   
	 		}else{    
	 			op.send();  
	 		}  
	 	}


12. Nicolas left...
Tuesday, 7 February 2006 9:41 am

Hi Jeff

In Flex 1.5, it was easy to consum a webservice method which return complex type like .NET Dataset, Vector in Java, custom class... Now with Flex 2.0 (directly with mxml or in actionscript with your datamanager), Flex can't parse this type of data I have this message: Element http://www.mysite.com/webservices/ws/:myTypeResponse not resolvable at mx.rpc.soap::WSDLParser/http://www.macromedia.com/2005/flex/mx/internal ::parseMessage() at mx.rpc.soap::WSDLOperation/parseMessages() at mx.rpc.soap::Operation/http://www.macromedia.com/2005/flex/mx/internal: :invokePendingCall() at mx.rpc.soap::Operation/send() at mx.rpc.soap.mxml::Operation/send()

Do you know if Adobe will do something ?

I dont know coldfusion but I saw that your webservices result are consumned like an array of Object. Do you have some suggestion about the result type I have to use in a Java webservice ?

However thanks for your blog, very interresting Nicolas


13. richard boelen left...
Tuesday, 7 February 2006 4:20 pm :: http://www.everflash.com

Hi all,

I have a strange problem going on call .Net webservices. It seems that when I use multiple arguments in a webservice method, the webservice api mixes up the arguments in the call, resulting in a http reguest error.

Is it possible to pass the arguments to the "makeRemoteCall' by specifying their names?


14. Jeff Tapper left...
Tuesday, 7 February 2006 5:38 pm

Richard -

I dont think its possible with the way this is built, since I'm using the funtion.apply() method, its specifically going to pass an array as a list of arguments.


15. richard boelen left...
Friday, 10 February 2006 4:14 pm :: http://www.everflash.com

Hi Jeff,

you can change the makeRemoteCall function to this

	 	public function makeRemoteCall(methodName:String, eventName:String, 
args:Object):void{
	 		this.eventName = eventName;   
	 		var op:mx.rpc.AbstractOperation = ws[methodName];   
	 		ws.addEventListener("result",doResults);   
	 		ws.addEventListener("fault",doFault);   
	 		
	 		if(args){    
	 			op.arguments = args;
	 		}    
	 		op.send();  
	 	}

getProdsForCat becomes

		private function getProdsForCat(event:Event){
			var catId:int = List(event.currentTarget).selectedItem.CATEGORYID;
			var args:Object = new Object();
			args.CategoryId = catId;
			prodManager.makeRemoteCall("getProdsByCategory","prodsRetrieved",args);
		}

where args.CategoryId is the argument defined by the webservice WSDL. So no mixed up arguments anymore.


16. TimHoff left...
Tuesday, 21 February 2006 6:54 pm

Hi Jeff,

I followed all of your directions for creating the DataManager, but I can't get past a compile-time error. It appears that Flex is recognizing the DataManager class, but will not let me declare a new variable based on the class. The error is: "Type annotation is not a compile-time constant:DataManager". The classes are in the correct directories and are showing-up as code-hinting options. The following code is where the compile-time error is occuring. Any ideas?

import mx.rpc.events.FaultEvent; import managers.DataManager; import flash.events.Event; import events.DataManagerResultEvent; import mx.controls.Alert; public var catManager:DataManager; <-error on this line.

I have to say that working with data from web services was a lot easier in 1.5.

Thank you for the example and any help, Tim


17. TimHoff left...
Tuesday, 21 February 2006 11:41 pm

No problem Jeff. I figured it out. Thank you and the others again, for the code examples. Handling web service calls in this manner is much cleaner.


18. Marcus Baffa left...
Wednesday, 1 March 2006 5:22 pm

Hi All,

I have the same problem as richard boelen. I have developed a logIn WebService in .NET that has 2 parameters, userName and userPassword.

Before makeRemoteCall calls the WebService the args are in the correct order but when it reaches the Webservice the parameters, sometimes are swapped, that is userName receives the password and userPassword the user name.

This is not deterministic, there are times that the WebService receives the parameters OK.

Could you please help me !!!!!


19. Marcus Baffa left...
Wednesday, 1 March 2006 10:35 pm

Ok Jeff, I have implemented the sugestions of Richard and It worked. Thanks anyway


20. Marcus Baffa left...
Thursday, 9 March 2006 8:05 am

Hi everybody,

I have a .NET wbeservice that has a complex type (structure) as parameter. When I call the service Flex 2 returns the message: HTTP request Error.

The service works OK. I have a version with independent parameters.

How can I call a Webservice that has complex type as parameters ????

By the way the webservices that return complex types works OK. The problem is to call from flex to .NET

Thanks in advance


21. TimHoff left...
Friday, 10 March 2006 4:11 pm

For those that want to show the BUSY cursor while web service data is retrieved, the following code may be useful. It’s more complicated than just setting the mxml showBusyCursor property to “true”, but this property doesn’t seem to be available in action script. There’s probably a better way to do this, but it works.

import mx.managers.CursorManager;

private function showBusyCursor()

  • {

    • CursorManager.setBusyCursor();

  • }

  • private function hideBusyCursor()

  • {

    • CursorManager.removeAllCursors();

  • }

Call the showBusyCursor(); function before or in the makeRemoteCall function. Call the hideBusyCursor(); function after or in the handleResults function.


22. Jeff Tapper left...
Friday, 10 March 2006 9:21 pm

Tim -

Thats pretty much what I just did on a recent project, added calls to CursorManager.setBusyCursor(); in the makeRemoteCall method and calls to CursorManager.removeBusyCursor(); in both the result and fault handlers.
You probably don't want to use removeAllCursors, as you suggest, in cases where there may be multiple silmotaneous calls.


23. Beppino left...
Monday, 20 March 2006 10:08 am

Same problem with .NET and parameters order. I've tried the Richard Boelen solution, but still doesn't works for me. The order it's absolutely random. Just a note: I'm not sure if the webservice it's a .NET or a MSSOAP because I'm not the developer of this: surely it's a SAP system. Thanks -g


24. Jeff Tapper left...
Monday, 20 March 2006 10:55 am

Beppino -

There seems to be difference between how flex interacts with RPC encoded web services vs how it handles doc/lit ones. To the best of my understanding, RPC type services work properly with ordered arguments, while doc/lit services require specific name value pairs to be set. My original code should run fine for RPC based services, while you will need to use the mods posted by Richard, if its doc/lit.

Can you provide more information about what went wrong when you tried Richard's version? If you passed through an object with name value pairs, it shouldnt matter if the order is mixed up, since it specifically names each argument coming in.


25. Beppino left...
Tuesday, 21 March 2006 5:20 am

Thanks Jeff!



The SAP responsible say me that the webservices it's RPC encoded.


This it's what happens in my cases:
suppose I want to send the parameters:
name1="a", name2="b", name3="c";


if I use your originale code, calling

materialManager.makeRemoteCall("methodName", "event", ["a", "b", 
"c"]);
the output soap it's correct about the values order, but not in the name of the vars order. i.e.
<name3>a</name3>
<name1>b</name1>
<name2>c</name2>
not always in this order.

If I use the Richard code, the results are correct in the name valu pair, but non again i the order: i.e:

<name2>b</name2>
<name1>a</name1>
<name3>c</name3>

To control this I'm using ServiceCapture tool. Sorry for my tremendous english



Thanks


-g


26. Beppino left...
Friday, 24 March 2006 8:49 am

Hello everybody. I've installed, like surely all of you, the new beta 2 of Flex 2. Ok, the "problem" it's always here! Can someone please help me to resolve this? Why with the Jeff cose Flex still swap the parameters name and leave in the correct order the values? Anyone note this? It's my fault?

Thanks a lot

-g


27. Akeem left...
Saturday, 25 March 2006 3:48 am

you can use: (NB args is an array see february 7th post by richard) public function makeRemoteCall(methodName:String, eventName:String, args:Array):void{ this.eventName = eventName; var op:mx.rpc.AbstractOperation = ws; ws.addEventListener("result",doResults); ws.addEventListener("fault",doFault);

if(args){ op.arguments = args; } op.send(); }


28. luveh left...
Monday, 27 March 2006 10:19 am

Thank you Timoff and Jeff for the code and details, it works for me!


29. TimHoff left...
Monday, 27 March 2006 11:55 pm

Thanks for the reference LUVEH, but Jeff Tapper gets all the credit for the data manager code. Jeff, do you have any plans to make this a service that is based on Cairngorm? I'm still getting my head completely around the concepts, but everything that I've read so far is very impressive.


30. Simon left...
Sunday, 3 June 2007 4:19 am :: http://www.nutrixinteractive.com/blog

Hi and thanks so much for the awesome guidence here and in you publications. I am trying to rewrite your class for remoting using the rpc.RemoteObject as opposed to the WSDL and am hitting walls, below is my simple ammended script (simple as in simple changes) and I was wondering if you could offer any guidance....?

Cheers Simon

package managers {

import flash.events.EventDispatcher; import mx.rpc.events.ResultEvent; import mx.rpc.events.FaultEvent; import mx.rpc.AbstractOperation; import events.DataManagerResultEvent; import flash.utils.*; import mx.rpc.AsyncToken; import mx.rpc.remoting.RemoteObject;

/* DataManager - singleton class which enforces onlya single object is created for each wsdl. * To access DataManager, use getDataManager(wsdl:String) */ public class DataManager extends EventDispatcher { private var ro:RemoteObject; private static var instanceMap:Object = new Object();

public function DataManager(pri:PrivateClass, strArg:String){ this.ro = new RemoteObject(); ro.destination = strArg; }

public static function getDataManager(strArg:String):DataManager{ if(DataManager.instanceMap == null){ DataManager.instanceMap = new DataManager(new PrivateClass(),strArg); } var dm:DataManager = DataManager.instanceMap;

return dm; /* CANNOT FIND AN EQUIVALENT FOR THE REMOTEOBJECT var dm:DataManager = DataManager.instanceMap; if(dm.ro.canLoadWSDL()) {return dm;} else {throw new Error("BAD WSDL:"+wsdl);}*/ } public function makeRemoteCall(methodName:String,eventName:String, args:Object):void{ trace("DataManager.makeRemoteCall("+methodName+","+ev entName+","+args+")");

var op:mx.rpc.AbstractOperation = ro; ro.addEventListener("result",doResults); ro.addEventListener("fault",doFault);

if (args){ op.arguments = args; }

var token:AsyncToken = op.send(); token.eventName = eventName; }

private function doResults(event:ResultEvent):void{ var e:DataManagerResultEvent = new DataManagerResultEvent( event.token.eventName, event.result); this.dispatchEvent(e); }

private function doFault(fault:FaultEvent):void{ this.dispatchEvent(fault); }

public override function toString():String{ return "DataManager"; } } }

/* PrivateClass is used to make DataManager constructor private */ class PrivateClass{ public function PrivateClass(){} }


31. Simon left...
Sunday, 3 June 2007 10:00 am :: http://www.nutrixinteractive.com/blog

oop, futher seaching revealed your mention of tien nguyens variation, sorry bout that ;)


32. David left...
Sunday, 2 September 2007 10:24 am

Jeff,

  • Thanks for the code. It worked really well for me until I started using the datamanager with a module that I embedded in an application. The datamanger ended up as single instance in the application shared by two versions of the module which accessed the same web service (actually I was using multiple instances of the same module). As a result when one module triggered a web service call the other module was also reacting to the event fired by the Datamanager. This was not an easy bug to track down.

  • To fix the issue I added an optional guid parameter to the getDataManager call and passed the component.parentApplication as my guid so that each module would force the datamanager to create a new instance of the datamanager for that module. The fix works, but I'm wondering if my approach is correct here. Anyone have any ideas?

Here is a snippet of the code modification to allow for a separate DM instance per module public static function getDataManager(wsdl:String,guid:String = "default"):DataManager{ if(DataManager.instanceMap == null) { DataManager.instanceMap = new DataManager(new PrivateClass(),wsdl); }

var dm:DataManager = DataManager.instanceMap;


33. eric belair left...
Wednesday, 3 October 2007 3:40 pm

Jeff, this is great. So much functionality in such a small amount of code. Now I only need 4 lines of AS code to create/call a WebService from any component in my application. Thanks a lot. One question: what is the purpose of creating the "PrivateClass"? Security?