Flash Article

Getting a Handle on Web Services in Macromedia Flash MX Professional 2004

Vera Fleischer

 

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!

Requirements

To complete this tutorial you will need to install the following software and files:

Product

For the web server proxy example:

Tutorials and sample 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.

Types of Web Services That Work with Flash MX Professional 2004

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.

Limitations of Handling Web Services in Flash MX Professional 2004

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.

Differences Between the WebServiceConnector, WebServices API, and Flash Remoting

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.

Web Service Proxies as an Alternative to Flash Remoting

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.

Building a Generic Web Service Proxy in PHP

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:

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.

  1. Open Flash MX Professional 2004 and create a new Flash document. Name it MillionaireQuiz and save it.
  2. Create two layers. Name one code and the other one stuff. You will put code on the code layer and visual elements on the stuff layer.
  3. Drag two Button components from the Components Panel to the Stage.
  4. Label them new question and check answer and give them instance names of questionButton and checkButton.

    MillionaireQuiz file showing layers and interface elements

    Figure 1: MillionaireQuiz file showing layers and interface elements

  5. Create a dynamic status text field that will tell the user when data is loading. Give this text field an instance name of statusText.
  6. Create a movie clip and name it feedback. Label three of its frames default, correct, and incorrect, and place a 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.
  7. Drag a RadioButton component from the Components panel to the Stage and then delete it. This places it in the library. You will add RadioButtons with ActionScript; the symbol needs to be present only in the library.
  8. In the Library panel, create a movie clip with a horizontal line in it. Give it a linkage id of hr (for horizontal rule).
  9. Add the following code to the first frame of the code layer. I know it looks like a lot, but comments make up most of it, I promise!
// 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!

About the author

Vera Fleischer has been a slave to Flash since 2000. She first started playing with it after she accidentally opened it at a small e-learning shop in Charlottesville, Virginia. After about two years of developing educational content with Flash, she moved to San Francisco so that she could be closer to Macromedia, the home of Flash. In San Francisco, she loitered near the Macromedia building until somebody finally hired her. Vera is now a QA Engineer on the Flash team. When she is not at work, you can find Vera at www.mediasparkles.com, and she welcomes any questions you may have about this article.