So you have heard that Macromedia Flash MX Professional 2004 has built-in support for web services. Great! But is this unconditionally true? Are all web services now supported in Flash? Are there exceptions? Workarounds? What if the server on which the web service is hosted doesn't have a policy file?
I have answers for you. For two whole weeks I did nothing but scour the web for different kinds of web services to see how Flash would handle them. In this article I will discuss most of my findings. In light of the fact that there are all kinds of different web services out there, I will also do a quick comparison of the advantages of the WebServiceConnector component, the WebService classes, and Macromedia Flash Remoting. For a more detailed comparison, please refer to Choosing Between XML, Web Services, and Remoting for Rich Internet Applications by Steven Webster. Finally, I am going to walk you through the creation of a generic web service proxy in PHP to solve the cross-domain problem. This proxy will let you hook up to any SOAP web service, with or without a policy file. Let's do it!
To complete this tutorial you will need to install the following software and files:
To get the most out of this tutorial, you should be familiar with the WebServiceConnector component and the WebService API in Flash MX Professional 2004. If you aren't familiar with them, please browse through the web services resource links on the Macromedia Developer Center, making sure to check out Jeffrey Hill's article, Using the Flash MX 2004 Web Service Classes, on flash-db.com. To complete the proxy server example, you should also have some experience with a server-side scripting language such as PHP or ColdFusion.
Macromedia Flash MX Professional 2004 has native support for SOAP and XML Remote Procedure Call (XML-RPC) web services. It doesn't matter which programming platform the web services are written in. One thing to pay attention to, however, is that Flash MX Professional 2004 doesn't natively support ASP.NET DataSets as of this release. You can work around this by parsing web service responses yourself, but you cannot bind directly to the .NET DataSet.
You can consume SOAP-based web services with the WebServiceConnector component or by using the ActionScript WebService API. You can consume XML-RPC web services through the RPC ActionScript API, a lower-level API on which the SOAP API is based.
An alternative to a SOAP-based web service is a Representational State Transfer (REST) web service. A REST-based web service is different from SOAP in that calls to it incorporate the method name directly into the URI and pass parameters through a query string. The web service will return an XML response. The WebService API in Flash does not support REST-based web services. However, you can still consume them in Flash if you use a server-side script to connect to the web service and the Flash-native XML class to parse the XML response.
Under the SOAP umbrella, which includes the majority of web services
available at the moment, Flash supports RPC-style and document-style web
services. Remote Procedure Call (RPC) style means that the SOAP message
formats for both requests and responses include a wrapper element containing
the operation or method name. In document style, request and response
SOAP messages do not contain this wrapper element. The two styles are
distinguished in the WSDL by the style
attribute within the
soap:binding
and/or the soap:operation
tag.
Here is an excerpt from a WSDL document describing an RPC-style web service:
<binding name="sampleService"> <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http" /> <operation name="sampleMethod"> <soap:operation style="rpc" soapAction="http://sample_service_action.com/" /> <input> <soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" /> </input> <output> <soap:body use="encoded" encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" /> </output> </operation> </binding>
Here is an excerpt from a WSDL describing a document-style web service.
<binding name="sampleService"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" /> <operation name="sampleMethod"> <soap:operation style="document" soapAction="http://sample_service_action.com/" /> <input> <soap:body use="literal" /> </input> <output> <soap:body use="literal" /> </output> </operation> </binding>
Notice the different values of the style attributes. Flash supports both styles.
As for the types of data that the web service can process or return, Flash supports any web services that accept and return strings, numbers, objects, arrays and other data types native to Flash. If the data returned is in a foreign language, Flash will render it correctly as long as the XML response is UTF-8 encoded. Lastly, Flash can even invoke web services that process binary data, such as a file comparison web service, as long as the return type is something that Flash can understand, such as a boolean.
There are exceptions to the types of SOAP web services Flash can consume. Flash only supports SOAP-based web services when they are transported over the HTTP protocol. Flash does not support SOAP web services over SMTP or FTP as of this release.
Flash also does not support web services with non-SOAP ports, such as MIME. The name attribute within the port element must point to a SOAP port. In the excerpt from the service definition of a WSDL document below, the port that bears the name SamplePort must be a SOAP port.
<service name="SampleService"> <port name="SamplePort"> <soap:address location="http://sample.com/SampleService"/> </port> </service>
Flash also does not support the import
tag. You can use
the import
tag to keep parts of a WSDL description in separate
files. You can reuse those parts, such as schemas and other definitions.
Flash MX Professional 2004 does not support web services that use the
import
tag.
You may also run into problems with web services that require complex data, such as objects containing arrays, as an input parameter. If you try to pass complex data to a web service using the WebService API, you will receive an error saying that the endpoint URL could not be opened. To send complex data, use Flash Remoting. Complex data in output parameters, on the other hand, does not cause any problems.
This is probably pretty obvious, but I'll point it out anyway: Flash does not support web services that return data it can't handle, such as highly formatted HTML. If a web service returns an interactive HTML map, for example, Flash will not be able to render it correctly due to the limitations of its HTML capabilities. On the other hand, if the web service returns simply the building blocks for an interactive map, such as an XML structure describing label strings, the URLs of image files, and so on, Flash, with some effort, can recreate the map.
Finally, there are some types of web services that the WebService API supports that will fail if you use the WebServiceConnector component. The WebServiceConnector does not support web services with more than one SOAP port defined in the WSDL. The WSDL excerpt below defines a service with two SOAP ports.
<service name="SampleService"> <port binding="tns:Service1" name="Service1"> <soap:address location="http://samplesite.com/SampleService" /> </port> <port binding="tns:Service1" name="Service2"> <soap:address location="http://samplesite.com/SampleService" /> </port> </service>
If you try to invoke this type of web service with the WebServiceConnector, the compiler will throw an error: "There are multiple possible ports in the WSDL file; please specify a service name and port name!" The call fails because the WebServiceConnector API doesn't allow the developer to specify the port. When there is only one port, this does not become an issue.
Even though this is undocumented, the WebService API provides a workaround for handling multiple ports.
// instantiate the WebService object var ws:WebService = new WebService('http://sampleWSDL.wsdl'); // specify the port name ws._portName = 'Service1'; // call an operation on the service ws.getInfo('94103');
The example consumes a fictitious web service named Sample Service,
which defines an operation named getInfo
. If you use the
code above, the web service call will no longer fail because it specifies
the port. Note that this code requires the WebServiceClasses to be in
your movie's library. Select Other Panels > Common Libraries > Classes
and drag the WebServiceClasses compiled clip into the library of your
Flash file.
The WebServiceConnector component also does not support web services with more than one service defined in the WSDL. Web services with more than one service often have more than one SOAP port as well, since each service element in the WSDL contains a port element. Here is an excerpt from a WSDL description that describes a web service with more than one service:
<service name="SampleService"> <port binding="tns:SampleService" name="SampleService"> <soap:address location="http://samplesite.com/SampleService" /> </port> </service> <service name="AnotherService"> <port binding="tns:AnotherService" name="AnotherService"> <soap:address location="http://samplesite.com/AnotherService" /> </port> </service>
Each service usually encompasses several operations. When you add the URL of the WSDL description of a web service with more than one service to the Web Services panel, Flash only parses the first service and its operations and displays them in the panel. This is because the Web Services panel only supports web services with exactly one service. The Web Services panel doesn't display more than one service per WSDL.
If you call one of the web services of a multiservice definition using the WebServiceConnector component, the compiler reports the same multiple ports error mentioned above. Again, there is an undocumented workaround through the WebService API. If you use this API to specify the service name and port name (as shown in the following code), the web service call will succeed.
import mx.services.* ; // instantiate the WebService object var ws:WebService = new WebService('http://sampleWSDL.wsdl'); // specify the service name ws._name = 'SampleService'; // specify the port name ws._portName = 'Service'; // call an operation on the service ws.invokeMethod('94103');
Note: This code requires the WebServiceClasses to be in the library of your Flash file. Select Other Panels > Common Libraries > Classes and drag the WebServiceClasses compiled clip into the Library panel.
You may be thinking: How do I choose between the different methods of connecting to a web service? Below are some quick guidelines for each type and their strengths and weaknesses.
WebServiceConnector: The WebServiceConnector component is great for RAD (Rapid Application Development) because it allows developers to quickly prototype an application using the Web Service panel and data binding. It is also good for beginners and designers, as it doesn't require any ActionScript. But its methods and properties are accessible through ActionScript if you so choose.
WebServiceClasses: The WebService API comprises the ActionScript classes that drive the WebServiceConnector component, made available for direct access to developers. It has better, more flexible debugging capabilities than the WebServiceConnector, as you can specify BRIEF, VERBOSE, and DEBUG error log levels.
The WebService API also results in smaller file sizes than the WebServiceConnector component since no component overhead is involved. Finally, as explained above, the WebService API can handle web services the WebServiceConnector component can't, such as web services with more than one port or more than one service.
Macromedia Flash Remoting: An advantage of Remoting is that it is much faster than either the WebServiceConnector component or the WebService API thanks to its use of the binary Action Message Format (AMF). The WebServiceConnnector component and WebService API are both based on ActionScript classes and XML, which aren't as efficient. Due to its superior speed, you should use Flash Remoting when handling and sending large amounts of data through web services.
Another advantage is that Remoting bypasses the need for placing a cross-domain policy file on the remote server. Both the WebServiceConnector component and the WebService API require such a policy file when working with remote web services. For more information on Flash Player security and the policy file, please see Deneb Meketa's article, Security Changes in Macromedia Flash Player 7.
When working with web services developed in-house and hosted on internal servers, you have full control over the policy file. The cross-domain policy only becomes an issue when consuming public web services hosted on remote servers that have not granted you access through a policy file.
In this case, you can use Macromedia Flash Remoting to act as a web service proxy and bypass the cross-domain policy file. Another approach is to create your own web service proxy. A web service proxy is simply an object on your server that will handle the interaction with the web service. Your Flash file calls this object just as it calls any other server-side script.
In general, it's better to use Flash Remoting than to create your own web service proxy. You can use Macromedia Flash Remoting or the open-source AMFPHP version. But there are a few reasons you might want to create your own web service proxy: Flash Remoting is simply not feasible, or your existing application platform is in a language that Flash Remoting does not support and you want to stay consistent with one server technology, or you simply prefer to manage your own code. In each case, you can create a web service proxy in the server-side language of your choice.
You can find some solutions for ColdFusion, PHP, ASP, and Java server-side proxies in the Server-Side Proxy section of the Macromedia TechNote, Loading Data Across Domains. These proxies simply let the server-side script do the reading of the remote file. Then, Flash calls the server side script instead of the remote file.
Unfortunately, this trick does not work with the WebService API. If a developer uses the WebService API, the only remote URL he or she comes in contact with directly is the WSDL description URL. However, the WSDL merely describes the web service. To actually interact with the web service, the WSDL specifies and calls out to other remote URLs. It is those URLs that violate the cross-domain policy; they are the reason the simple proxy from the TechNote doesn't work with the WebService API.
I will now show you how to set up an alternative, generic proxy with PHP. This proxy lets you to tap into any SOAP web service directly from Flash by using PHP as a bridge. I will use the MillionaireQuiz web service written by Konrad Wulf. It accesses a database of multiple-choice questions similar to those typical of the "Who Wants To Be A Millionaire" television show. Check out a live example.
The generic proxy API consists of two ActionScript classes and two PHP files. The files are as follows:
LoadVars
object to send and receive web service data to and from GenericProxy.php. It uses the Serializer class to serialize the data before sending it out and to unserialize it after receiving it from PHP. You should save this file in one of your Classes directories, such as C:\Documents and Settings\{your username}\Local Settings\Application Data\Macromedia\Flash MX 2004\en\Configuration\Classes\com\ws\.Open a text editor of your choice to write the following scripts:
GenericProxy.php:
<?php // Grab values from REQUEST and unserialize $wsdl = unserialize(urldecode($_REQUEST['wsdl'])); $methodName = unserialize(urldecode($_REQUEST['methodName'])); $paramArray = array_reverse(unserialize(urldecode(stripslashes($_REQUEST['paramArray'])))); // Include nusoap.php file. require_once('nusoap.php'); // Define new object via nusoap.php and specify location of WSDL $soapclient = new soapclient($wsdl,'wsdl'); // Set timeouts, nusoap default is 30 $soapclient->timeout = 500; $soapclient->response_timeout = 500; // Call the web service operation/method and accept the result $SOAPResult = $soapclient->call($methodName,$paramArray); // Serialize the result before sending back to Flash $serializedResult = utf8_encode(serialize($SOAPResult)); // Send result back to Flash print "&myResult=" . urlencode($serializedResult); ?>
Put GenericProxy.php on the server in the same directory as nusoap.php.
GenericProxy.as:
// import the Serializer class import it.sephiroth.Serializer; class com.ws.GenericProxy { // object to un/serialize data before sending/receiving to/from PHP private var serial:Serializer; // LoadVars object to send and receive the data private var lv:LoadVars; // WSDL for the web service public var wsdl:String; // unserialized result sent back from web service via PHP public var myResult:Object; // function that is called when result comes back from PHP; // defined by user public var onResult:Function; // constructor function GenericProxy(wsdlURI) { wsdl = wsdlURI; } // method that calls the web service via PHP public function makeMethodCall(methodName, paramArray) { // define new Serializer object serial = new Serializer(); // define the LoadVars object lv = new LoadVars(); // prepare the variables that the LoadVars object will pass // to GenericProxy.php by defining and serializing them lv.wsdl = escape(serial.serialize(wsdl)); lv.methodName = escape(serial.serialize(methodName)); lv.paramArray = escape(serial.serialize(paramArray)); // create reference to class instance to be available // inside the LoadVars object lv.gp = this; // function that is called when the LoadVars object // receives data back from GenericProxy.php lv.onLoad = function(success) { if (success) { // invoke onResult method and pass unserialized result to it this.gp.onResult(this.gp.serial.unserialize(this.myResult)); } else { // The data didn't load right. // Note: this has nothing to do with the actual web service trace("LoadVars failure"); } }; // call GenericProxy.php to pass the LoadVars variables // to it lv.sendAndLoad("GenericProxy.php", lv, "POST"); } }
Save this file at C:\Documents and Settings\{your username}\Local Settings\Application Data\Macromedia\Flash MX 2004\en\Configuration\Classes\com\ws\. Note that while doing local testing, you may need to replace the following statement:
lv.sendAndLoad("GenericProxy.php", lv, "POST");
with this one:
lv.sendAndLoad("http://localhost/GenericProxy.php", lv, "POST");
Now, create the FLA file that will use the GenericProxy class
to connect to the MillionaireQuiz web service.
Label them new question and check answer and give them instance names of questionButton and checkButton.
stop()
action on each of these frames. Leave the first frame blank and place graphics or text indicating that the user has answered the question correctly or incorrectly on the two others. Give this movie clip the instance name feedback.// import the generic proxy class import com.ws.GenericProxy; // import the controls classes so we can add UI components // at runtime import mx.controls.*; // define the location of the WSDL wsdl = "http://java.rus.uni-stuttgart.de/quiz/quiz.wsdl"; // create a new GenericProxy object var proxy:GenericProxy = new GenericProxy(wsdl); // set global depth that will be incremented every time // a new visual element is added dynamically _global.MAXDEPTH = 5; // create a reference to the timeline var host:MovieClip = this; // create an array with possible answers var answerRefs:Array = ["A", "B", "C", "D"]; // set some spacing variables for laying out the content var xSpacing:Number = 20; var xSpot:Number = 20; var ySpacing:Number = 50; var ySpot:Number = 50; // initiate two more variables var answerCount:Number; var questionRef:Object; // initiate the app by getting the first question getQuestion(); // what to do when the "new question" button is clicked questionButton.onRelease = function() { cleanUp(); getQuestion(); } // what to do when the "check answer" button is clicked checkButton.onRelease = function() { checkAnswer(); } // get a new question from the database function getQuestion() { // disable all UI elements disable(); // make a method call to getRandomQuestion() // via the proxy service object proxy.makeMethodCall("getRandomQuestion"); // assign a method to the proxy's onResult proxy.onResult = getQuestionResult; } // function that is called when web service result arrives function getQuestionResult(WSResult:Object) { answerCount= 0; ySpot = 50; statusText.text = ""; questionRef = WSResult; // create a text field for the question host.createTextField("q", MAXDEPTH++, xSpot, ySpot, 250, 50); host.q.multiline = true; host.q.wordWrap = true; host.q.text = questionRef.question; ySpot += 50; for (var i in questionRef) { if (i.substr(0, 6) == "answer") { // create a radio button for each multiple-choice answer host.createClassObject(RadioButton, "rb"+answerCount, MAXDEPTH++); var rb = host["rb"+answerCount]; rb.groupName = "answerGroup"; if (answerCount == 0) { rb.selected = true; } rb._x = xSpot; rb._y = ySpot+3; // create a text field for each answer host.createTextField("answer"+answerCount, MAXDEPTH++, xSpot+xSpacing, ySpot, 200, 20); var a = host["answer"+answerCount]; a.text = questionRef["answer"+answerRefs[answerCount]]; // create a horizontal rule after each answer host.attachMovie("hr", "hr"+answerCount, MAXDEPTH++); var hr = host["hr"+answerCount]; hr._x = xSpot; hr._y = a._y+a._height+10; ySpot += ySpacing; answerCount++; } } // re-enable UI elements enable(); } // check the answer the user selected function checkAnswer() { disable(); // prepare the parameters to be sent to web service var id = questionRef.id; var guessedAnswer = answerRefs[host.answerGroup.selection._name.substr(2, 1)]; parameters = [id, guessedAnswer]; // make the web service call to checkCorrectAnswerById(), // and pass the parameters proxy.makeMethodCall("checkCorrectAnswerById", parameters); // assign the onResult method for the proxy proxy.onResult = checkAnswerResult; } // function that is called when result comes back from call // to checkCorrectAnswerById() function checkAnswerResult(WSResult:String) { statusText.text = ""; // decide whether to display "correct" or "incorrect" visual if (WSResult == true) { feedback.gotoAndStop("correct"); } else { feedback.gotoAndStop("incorrect"); } enable(); } // cleans up question and answers when a new one is requested function cleanUp() { for (var i = 0; i < answerCount; i++) { host["rb" + i].removeMovieClip(); host["answer" + i].removeTextField(); host["hr" + i].removeMovieClip(); } host.q.removeTextField(); } // two functions to en/disable UI elements function enable() { checkButton.enabled = true; questionButton.enabled = true; } function disable() { checkButton.enabled = false; questionButton.enabled = false; lilMessage = "<talking to web service, please wait>"; statusText.text = lilMessage.toUpperCase(); feedback.gotoAndStop(1); }
When you test movie now, you will first see a message indicating that you are connecting to the web service. After a moment, you will see the first question and answer set dynamically constructed on the screen by createClassObject
, createTextField
, and attachMovie
.
The important lines of code are the following:
wsdl = "http://java.rus.uni-stuttgart.de/quiz/quiz.wsdl"; var proxy:GenericProxy = new GenericProxy(wsdl); proxy.makeMethodCall("getRandomQuestion"); proxy.onResult = getQuestionResult;
Here you establish an instance of the GenericProxy
ActionScript class by passing to its constructor the WSDL of the web service you would like to connect to. You then use its makeMethodCall()
method to call the web service's getRandomQuestion()
operation. You also define the proxy's onResult
method to determine what should happen when the result comes back from the web service. Remember that the GenericProxy
ActionScript class and its PHP partner GenericProxy.php handle all the rest, such as passing the parameters to nusoap.php and serializing and deserializing the data sent to and from PHP. The generic proxy you have created lets you make any web service call in just four lines of code.
PHP is of course not the only way to build a generic web service proxy. You can create one in any server-side language. To build one in ASP.NET, Chris Peiris' article, Creating a Proxy Web Service Object, is a good place to start. If, on the other hand, you prefer Java, Axis might be an option to explore. Whichever programming language you choose, you will be well-advised to build on an existing server-side object that can connect to a web service. In my example, nusoap.php worked great. You could write something similar yourself, but it would be a lot more work.
I hope this has given you some insight into the wonderful world of web services in Flash MX Professional 2004. Now go show those web services who's boss!