Data Marshalling is a common task that nearly every ActionScript 3 developer will face at some point; put simply it is the process of converting an Object into a data interchange format (for example XML, JSON or AMF) and likewise converting it back into an Object that your application can use again. This tutorial will focus on XML.
Data Modelling
So let’s start nice and simple, with the XML that we want to unmarshall into an ActionScript Object – this could be coming from a local file, or from a server (referred to as an endpoint).
The SlideShow XML Document
<slideshow title="LolCats">
<slide url="http://checkmybelly.com/wp-content/uploads/2007/11/lolcat-funny-picture-moderator1.jpg" caption="Moderator Kitteh" />
<slide url="http://www.lolcats.com/images/u/07/24/lolcatsdotcom5x02srq751692std.jpg" caption="Wait... I'll fix it" />
<slide url="http://icanhascheezburger.files.wordpress.com/2007/01/i-can-has-cheezburger.jpg" caption="Can Has Cheezburger?" />
</slideshow>
So, we need to start by looking at what Objects we create when the parse this XML. This one is quite easy; the XML document defines a Slideshow which contains a number of slides; with this in mind it would seem appropriate that we create two Classes, one which represents a single Slide, and another which represents the entire SlideShow.
The Slide model is straight forward as it just needs to contain the URL of the image and the caption; both of these can be represented as Strings which keeps everything very straight forward:
The Slide Model Object
package slideshow.model
{
public class Slide
{
private var url : String;
private var caption : String;
/**
* Creates a new Slide Model Object which represents a single image in a SlideShow.
*
* @param url The URL of the image which makes up this Slide.
* @param caption Text which accompanies this Slide.
*/
public function Slide(url : String, caption : String) {
// Ensure this object does not get instantiated with invalid data.
if (url == null) throw new ArgumentError("url must not be null");
if (caption == null) throw new ArgumentError("caption must not be null");
this.url = url;
this.caption = caption;
}
/**
* The URL of the image which makes up this Slide.
*/
public function getUrl() : String {
return url;
}
/**
* Text which accompanies this Slide.
*/
public function getCaption() : String {
return caption;
}
public function toString() : String {
return "[Slide url=" + url + ", caption='" + caption + '"]';
}
}
}
The model is nice and straight forward; there are a couple of points to note from the design of my Slide Model Object:
- The URL and Caption properties are passed in the constructor; this way I can ensure that my Slide Model is immutable and can not be changed once is has been created.
- When the URL and Caption are supplied in the constructor I perform a null reference check to ensure that non-null values were passed; this is important because it means that any other object (known as a “client”) which makes use of my Slide object knows that it will never get a null value back from the getUrl() and getCaption() methods.
- The Slide Model provides a toString() method – this will be called automatically whenever a Slide model object is concatenated to a String – for example in a trace() statement – this will aid debugging and development.
Next up is the SlideShow model; we know from the XML that a SlideShow has a title and a number of Slides; my SlideShow Class looks like this:
The SlideShow Model Object
package slideshow.model
{
public class SlideShow
{
private var title : String;
private var slides : Array;
/**
* Represents a collection of Slide images which make up a SlideShow.
*
* @param title Description of this SlideShow.
* @param slides Array of Slide model instances which make up this Slideshow.
*/
public function SlideShow(title : String, slides : Array) {
// Ensure this object does not get instantiated with invalid data.
if (title == null) throw new ArgumentError("title must not be null");
assertSlides(slides);
this.title = title;
this.slides = slides.concat(); // Take a defensive copy.
}
/**
* Custom assertion method which ensures that the supplied Slide array is in a valid state.
*/
private function assertSlides(slides : Array) : Array {
if (slides == null) throw new ArgumentError("slides must not be null");
// Check that the supplied Slides array contains only Slide models.
for each (var slide : Slide in slides) {
if (slide == null) throw new ArgumentError("slide array must only contain instances of slideshow.model.Slide.");
}
// Everything is OK, return the supplied Slides array for convenience.
return slides;
}
/**
* Description of this SlideShow.
*/
public function getTitle() : String {
return title;
}
/**
* An Array of Slide model instances which make up this SlideShow, note that this is a copy, so any changes
* made will not persist.
*/
public function getSlides() : Array {
return slides.concat(); // Return a defensive copy.
}
public function toString() : String {
return "[SlideShow title='" + title + "', slides={" + slides.join(", ") + "}]";
}
}
}
Like the Slide model, the SlideShow model is immutable; it achieves this by ensuing that all of the data that it encapsulates (contains) can not be modified once the object has been constructed and that all the supplied data is valid upon construction; as we saw in the Slide model, this is nice and easy with String values, however, as you can see from the SlideShow class definition; it’s a bit trickier to achieve this with an Array as it’s a complex data type.
- First, in the constructor, we ensure that the supplied slides Array is valid; as this is more involved than a simple null check, we pass it off to a helper function, assertSlides() – if assertSlides() encounters invalid data, it will throw an Error which will prevent the instance from being constructed.
- Next, we take a defensive copy of the supplied slides Array with the line: this.slides = slides.concat(). This is to prevent modifications to the supplied slide array from modifying the state of our SlideShow model object, here’s an example of this in action:
The Dangers Of Not Taking Defensive Copies of Complex Data Types
// Create a new Array of Slide models.
const slides : Array = new Array();
slides.push(new Slide("http://img.com/cat.png", "A Cat"));
slides.push(new Slide("http://img.com/dog.png", "A Dog"));
// Create a new SlideShow.
const slideShow : Slideshow = new SlideShow("Animals", slides);
// Outputs [SlideShow title='Animals', slides={
// [Slide url=http://img.com/cat.png, caption='A Cat'],
// [Slide url=http://img.com/dog.png, caption='A Dog']
// }]
trace(slideShow);
// Add a new slide to our slides Array - should this change the contents of the SlideShow model?
slides.push(new Slide("http://img.com/car.png", "A Car (not an animal!)"));
// Yes it does! Our SlideShow model object now looks like this:
// [SlideShow title='Animals', slides={
// [Slide url=http://img.com/cat.png, caption='A Cat'],
// [Slide url=http://img.com/dog.png, caption='A Dog'],
// [Slide url=http://img.com/car.png, caption='A Car (not an animal!)']
// }]
trace(slideShow);
- We also return a defensive copy of the slides Array in the getSlides() method – this is done for exactly the same reason – if we didn’t return a defensive copy then our SlideShow model would no longer be immutable, for example, what would happen to our SlideShow model if the client called: slideShow.getSlides().length = 0?
UnMarhshalling – Converting the XML into a Model Object
OK, so now we have both our transmission format (XML) and our ActionScript Model Objects (SlideShow and Slide) which represent the data in our application – let’s look at how we go from one to the other (and back again!); let’s start by unmarshalling – converting from the transmission format to the ActionScript Objects – this will be handled by our SlideShowMarshaller.
The XMLSlideShowMarhshaller Class
package slideshow.model
{
public class XMLSlideShowMarshaller implements SlideShowMarshaller
{
/**
* Converts the supplied SlideShow transmission format document into a SlideShow Model object.
*/
public function unmarshall(transmissionFormat : *) : SlideShow {
// First we need to convert the incoming transmission format data to an XML Object so we can parse it.
const slideShowXML : XML = new XML(transmissionFormat);
// Now we can parse the data...
const title : String = slideShowXML.@title;
const slides : Array = parseSlides(slideShowXML..slide);
// Ensure that the require attributes have been set.
if (title == "") throw new Error("SlideShow XML was missing @title attribute");
// And finally return the new SlideShow model object.
return new SlideShow(title, slides);
}
/**
* Parses an Array of Slide model objects from the supplied XMLList, if there are no slides in the supplied
* XMLList, an empty Array will be returned.
*/
private function parseSlides(slidesXML : XMLList) : Array {
const slides : Array = [];
for each (var slideNode : XML in slidesXML) {
// As the Slide model will throw an error if supplied with invalid data, we need to wrap it in a
// try/catch block and log that an error occured - note that we do not consider this to be "fatal"
// to the Unmarshalling operation as we can recover from it (skip over the offending slide).
try {
slides.push(new Slide(slideNode.@url, slideNode.@caption));
}
catch (e : Error) {
trace("Failed to parse Slide node: " + slideNode.toXMLString());
}
}
return slides;
}
}
}
Note how our XMLSlideShowMarshaller implements the SlideShowMarshaller interface; this is done so we can create future implementations should we wish to change our transmission format. The XMLSlideShowMarshaller class is pretty straight forward, first it converts the incoming raw data into an XML Object, and then it sets about creating and returning a new SlideShow instance. Should anything go awry during this process, an Error will be thrown which the Client will need to handle. Talking of Clients, let’s take a look at the Service object which will make use of our SlideShowMarshaller:
The SlideShowLoaderService Class
package slideshow.service
{
// imports omitted...
public class SlideShowLoaderService extends EventDispatcher
{
private var loader : URLLoader;
private var slideShow : SlideShow;
private var slideShowMarhsaller : SlideShowMarshaller;
/**
* Static factory method which should be used to load XML SlideShow Data.
*/
public static function createXMLLoader() : SlideShowLoader {
return new SlideShowLoader(new XMLSlideShowMarshaller());
}
/**
* Creates a new SlideShowLoaderService which will use the supplied SlideShowMarshaller object to convert the
* loaded data into a SlideShow model.
*
* @param slideShowMarhsaller Converts the raw data at the service endpoint into a SlideShow model object.
*/
public function SlideShowLoader(slideShowMarhsaller : SlideShowMarshaller) {
this.slideShowMarhsaller = slideShowMarhsaller;
}
/**
* Begins the load operation; if the load operation is successful Event.COMPLETE will be dispatched, should it
* fail, ErrorEvent.ERROR will be dispatched.
*
* @param endpointUrl URL which returns the SlideShow transmission data which will be converted into a
* SlideShow Model object.
*/
public function load(endpointUrl : String) : void {
// Clear out any pre-existing state.
slideShow = null;
loader = new URLLoader();
loader.addEventListener(Event.COMPLETE, onLoaderComplete);
loader.addEventListener(IOErrorEvent.IO_ERROR, onLoaderError);
loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onLoaderError);
try {
loader.load(new URLRequest(endpointUrl));
}
catch (e : Error) { {
handleError(e.message);
}
}
private function onLoaderComplete(event : Event) : void {
removeLoaderListeners();
// Use our marshaller to create the SlideShow model instance from the loaded data.
try {
slideShow = slideShowMarhsaller.unmarshall(loader.data);
handleComplete();
}
catch (e : Error) {
handleError("Marshalling error: " + e.message);
}
}
private function removeLoaderListeners() : void {
loader.removeEventListener(Event.COMPLETE, onLoaderComplete);
loader.removeEventListener(IOErrorEvent.IO_ERROR, onLoaderError);
loader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, onLoaderError);
}
private function onLoaderError(event : ErrorEvent) : void {
removeLoaderListeners();
handleError("Loading error: " + event.text);
}
private function handleError(message : String) : void {
dispatchEvent(new ErrorEvent(ErrorEvent.ERROR, false, false, message));
}
private function handleComplete() : void {
dispatchEvent(new Event(Event.COMPLETE));
}
/**
* Returns a SlideShow model object which was parsed from the service endpoint; note that this method will
* always return null unless a succesful call to load() was made.
*/
public function getSlideShow() : SlideShow {
return slideShow;
}
}
}
The SlideShowLoaderService class provides a clean and simple Facade to ActionScript’s own URLLoader class and our SlideShowMarshaller class; it serves as a specialisation of these two objects, providing a simple interface which only deals with loading, and marshalling from the Slide Show transmission format into a SlideShow Model Object; the Client (the class which makes use of the SlideShowLoaderService) doesn’t need to know that a URLLoader or an XMLSlideShowMarshaller are being used.
One interesting choice about the design of the SlideShowLoaderService is the way that the default constructor requires the Client to supply an instance of the SlideShowMarshaller interface. The reason is does this is to make the SlideShowLoader as flexible as possible; if we are loading our SlideShow data from XML, then we would invoke it like so:
Creating a SlideShowLoaderService instance which works with XML
const slideShowLoader : SlideShowLoaderService = new SlideShowLoaderService(new XMLSlideShowMarhshaller());
slideShowLoader.addEventListener(Event.COMPLETE, onSlideShowLoaderComplete);
slideShowLoader.addEventListener(ErrorEvent.ERROR, onSlideShowLoaderError);
slideShowLoader.load("slideshow.xml");
Likewise, if we needed to work with a JSON service endpoint, we could modify our client code to provide a JSONSlideShowMarshller (of course, you would need to create this class first!) – but you will notice that the SlideShowLoader works in exactly the same way!
Creating a SlideShowLoaderService instance which works with JSON
const slideShowLoader : SlideShowLoaderService = new SlideShowLoaderService(new JSONSlideShowMarhshaller());
slideShowLoader.addEventListener(Event.COMPLETE, onSlideShowLoaderComplete);
slideShowLoader.addEventListener(ErrorEvent.ERROR, onSlideShowLoaderError);
slideShowLoader.load("slideshow.json");
To make the developer’s life easier, we supply a Static Factory Method, SlideShowLoaderService.createXMLLoader() – this means the Client doesn’t have to know that they need to provide an XMLSlideShowMarhshaller instance in the constructor, the difference is subtle, but it makes it much easier to use:
Creating a SlideShowLoaderService instance using the Static Factory Method
const slideShowLoader : SlideShowLoaderService = SlideShowLoaderService.createXMLLoader();
slideShowLoader.addEventListener(Event.COMPLETE, onSlideShowLoaderComplete);
slideShowLoader.addEventListener(ErrorEvent.ERROR, onSlideShowLoaderError);
slideShowLoader.load("slideshow.xml");
Marhshalling – Converting the Model Object into XML
OK, so now we’ve seen how to unmarshall the transmission format into an ActionScript Object, let’s look at how to do the reverse and convert our Object back into the transmission format so we can send it over to a server. As a side note, you may know this concept as serialisation; however there is a very subtle difference between the two (at least according to the Java world); in my own view, serialisation concerns converting the object into a ByteArray (raw bytes) which is transmitted to the service layer, whereas marshalling deals with converting the object into a transmission format (which in itself is turned into raw bytes, but by another layer). This aside, lets look at how we get our SlideShow model back into XML; as we are still dealing with the marshalling process, let’s add a new method to our XMLSlideShowMarshaller in the form of marshall(slideshow):
Adding the marshall Method to XMLSlideShowMarshaller
// ... rest of class omitted for brevity
/**
* Converts the supplied SlideShow model object into an XML representation.
*/
public function marshall(slideShow : SlideShow) : * {
const result : XML = <slideshow title={slideShow.getTitle()} />;
for each (var slide : Slide in slideShow.getSlides()) {
result.* += <slide url={slide.getUrl()} caption={slide.getCaption()} />;
};
return result;
}
By making use of E4X we make light work of creating the XML representation of the Slideshow; this is also helped by the fact that we have guaranteed immutability of both the SlideShow and Slide objects, we know they are both in a valid state which certainly makes things easier and more concise.
Making use of this marhshall() method is nice and straight forward; all we need to do is pass a populated SlideShow model and we get the XML back:
Invoking the SlideShow Marshaller
// Create a SlideShow Model object instance.
const slideShow : SlideShow = new SlideShow("LolCats", [
new Slide("http://checkmybelly.com/wp-content/uploads/2007/11/lolcat-funny-picture-moderator1.jpg", "Moderator Kitteh"),
new Slide("http://www.lolcats.com/images/u/07/24/lolcatsdotcom5×02srq751692std.jpg", "Wait… I'll fix it"),
new Slide("http://icanhascheezburger.files.wordpress.com/2007/01/i-can-has-cheezburger.jpg", "Can Has Cheezburger?")
]);
// Convert the SlideShow Model object to our transmission format.
const xml : XML = new XMLSlideShowMarshaller().marshall(slideShow);
// Outputs:
// <slideshow title="LolCats">
// <slide url="http://checkmybelly.com/wp-content/uploads/2007/11/lolcat-funny-picture-moderator1.jpg" caption="Moderator Kitteh"/>
// <slide url="http://www.lolcats.com/images/u/07/24/lolcatsdotcom5×02srq751692std.jpg" caption="Wait… I'll fix it"/>
// <slide url="http://icanhascheezburger.files.wordpress.com/2007/01/i-can-has-cheezburger.jpg" caption="Can Has Cheezburger?"/>
//</slideshow>
trace(xml.toXMLString());
To wrap up the marshalling phase, let’s have a look at a potential client, for this example I’ve created a SlideShowSaverService which could be used to transmit a user generated SlideShow to a server.
The SlideShowSaverService Class
package slideshow.service
{
// imports omitted...
public class SlideShowSaverService extends EventDispatcher
{
private var loader : URLLoader;
private var slideShow : SlideShow;
private var slideShowMarshaller : SlideShowMarshaller;
/**
* Static factory method which will transmit the SlideShow to the service endpoint as XML.
*/
public static function createXMLSaver(slideShow : SlideShow) : SlideShowSaverService {
return new SlideShowSaverService(slideShow, new XMLSlideShowMarshaller());
}
/**
* Creates a new SlideShowSaverService which will send the supplied slideShow Model Object to a service endpoint
* in the transmission format dictated by the supplied SlideshowMarshaller.
*
* @param slideShow The SlideShow instance to transmit to the service endpoint.
* @param slideShowMarhsaller Converts the SlideShow model object into a transmission format.
*/
public function SlideShowSaverService(slideShow : SlideShow, slideShowMarshaller : SlideShowMarshaller) {
this.slideShow = slideShow;
this.slideShowMarshaller = slideShowMarshaller;
}
/**
* Contacts the Service Endpoint; if the operation is succesful Event.COMPLETE will be dispatched, should the
* operation fail, ErrorEvent.ERROR will be dispatched.
*
* @param endpointUrl URL to transmit the marhshalled SlideShow Model object to.
*/
public function save(endpointURL : String) : void {
const request : URLRequest = createURLRequest(endpointURL, slideShowMarshaller.marshall(slideShow));
loader.addEventListener(Event.COMPLETE, onLoaderComplete);
loader.addEventListener(IOErrorEvent.IO_ERROR, onLoaderErrror);
loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, onLoaderErrror);
loader.load(request);
}
/**
* Creates a URLRequest object which will be used by the URLLoader instance, this default implementation
* will make use of a POST request, the body of which will be the transmission data format.
*/
protected function createURLRequest(endpointURL : String, data : *) : URLRequest {
const request : URLRequest = new URLRequest(endpointURL);
request.method = URLRequestMethod.POST;
request.data = data;
return request;
}
private function onLoaderComplete(event : Event) : void {
removeLoaderListeners();
dispatchEvent(new Event(Event.COMPLETE));
}
private function removeLoaderListeners() : void {
loader.removeEventListener(Event.COMPLETE, onLoaderComplete);
loader.removeEventListener(IOErrorEvent.IO_ERROR, onLoaderErrror);
loader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, onLoaderErrror);
}
private function onLoaderErrror(event : ErrorEvent) : void {
removeLoaderListeners();
handleError("Loader error: " + event.text);
}
private function handleError(message : String) : void {
dispatchEvent(new ErrorEvent(ErrorEvent.ERROR, false, false, message));
}
}
}
Next Steps
To complete this example I’ve created a simple GUI which allows you to see the SlideShowLoaderService and SlideShowMarshaller in action, you can download the complete project here; to recompile the project you will need to modify the user.properties file and point the FLEX_HOME property at your Flex SDK. It makes use of Keith Peter’sexcellent MinimalComps UI library.
There are plenty of avenues to explore from here as this has just scratched the surface; you could start by extending the application to load, and marshall JSON data; prehaps taking advantage of the Adobe JSON Serialisation library (part of AS3 CoreLib), or you could reduce the amount of code you need to write by annotating your Model with Metadata and using an automatic marshalling library like ASAXB. I hope to cover these topics in later blog posts (time permitting)