|
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
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.
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.
Hi Jeff,
Modified your code and got CFadapter to work.
I considered doing this, however it did not seem to fit my problem.
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:
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.
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()
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:
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
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.
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'.
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();
}
}
Hi Jeff
Hi all,
Hi Jeff,
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.
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.
Ok Jeff, I have implemented the sugestions of Richard and It worked.
Thanks anyway
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.
{
CursorManager.setBusyCursor();
}
private function hideBusyCursor()
{
CursorManager.removeAllCursors();
}
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
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
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
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);
Thank you Timoff and Jeff for the code and details, it works for me!
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.
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....?
oop, futher seaching revealed your mention of tien nguyens variation, sorry
bout that ;)
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?
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?