Sunday, December 21, 2008

FlexB – XML mapping for Flex/ActionScript classes and web services

Flex has built it support for working with XML as well as built in support for mapping XML to ActionScript classes and back to XML again if you have an XSD file. What about mapping to and from XML if you don’t have an XSD file, and what about this XML web service stuff? FlexB is a Flex/ActionScript library for more easily working with XML and web services. It consists of a combination of classes that I originally made for my own personal use while teaching myself Flex a little over 2 years ago, which I have recently decided to renovate and make public.

Where can I get the code, library, and/or participate?

http://flexb.sourceforge.net

Why don’t you just use an XSD file and the built in XML mappings?

Because I am lazy and don’t want to use an XSD file. There are also equivalents to this idea in other languages such as with XStream and Java.

Why map XML to ActionScript classes with all of the direct component XML support?

Components such as the DataGrid allow you to directly reference XML in order to do useful things with your data.  The following example shows how to populate a DataGrid using XML:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="
http://www.adobe.com/2006/mxml">
    <mx:XML xmlns="" id="CompoundFoo">
        <CompoundFoo my_name="wii">
            <SimpleFoo><name>Moo Cow</name><foo>Bar</foo></SimpleFoo>
            <SimpleFoo><name>Meow Cat</name><foo>BarBar</foo></SimpleFoo>
        </CompoundFoo>
    </mx:XML>
    <mx:DataGrid dataProvider="{CompoundFoo.SimpleFoo}">
        <mx:columns>
            <mx:DataGridColumn headerText="Name" dataField="name" />
            <mx:DataGridColumn headerText="Foo" dataField="foo" />
        </mx:columns>
    </mx:DataGrid>
</mx:Application>

image

Using XML directly in a DataGrid is one of the fastest ways possible to display data in a Flex application, but what about maintainability? The XML in the DataGrid is a popular Flex example, but it is unrealistic. In a real world application that XML or data will most likely come from some external source, an external source which is subject to change. It is also likely that the data will be used in the application in more than once place, and its usage is likely to involve other customizations. It is also common for data to need to be converted to other types, such as with a String to a Date. Mapping XML to ActionScript classes and then using those classes makes change easier, especially if those mapped classes are used in several places throughout the application.

How is mapping XML to ActionScript better for change then mapping XML directly to several components like the DataGrid?

If you were to change any part of the XML from the first example, you would then have to change all of these references to it; the dataProvider, dataFields, and any other references. Use that XML in several places and you have several changes to make. These direct references to the XML are mapping, which can be spread out throughout components. If you use an ActionScript class to do the mappings, then all of your mappings are in the same place so it is easier to find and change. Since references are then to ActionScript classes you also gain the advantage of those references being type and name safe.

For example a reference to SimpleFoo.foo as XML would always compile whether it existed or not. If there were a class called SimpleFoo then access to the property foo would cause a compile error if it didn’t exist.

The following example shows the same DataGrid and XML, but this time the DataGrid is populated using an ActionScript class:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application
    xmlns:valentino="valentino.*"
    xmlns:mx="
http://www.adobe.com/2006/mxml">
    <mx:XML xmlns="" id="xml">
        <CompoundFoo my_name="wii">
            <SimpleFoo><name>Moo Cow</name><foo>Bar</foo></SimpleFoo>
            <SimpleFoo><name>Meow Cat</name><foo>BarBar</foo></SimpleFoo>
        </CompoundFoo>
    </mx:XML>
   
<valentino:CompoundFoo id="compoundFoo" xml="{xml}" />
    <mx:DataGrid dataProvider="{compoundFoo.foos}">
        <mx:columns>
            <mx:DataGridColumn headerText="Name" dataField="{valentino.SimpleFoo.NAME}" />
            <mx:DataGridColumn headerText="Foo" dataField="{valentino.SimpleFoo.FOO}" />
        </mx:columns>
    </mx:DataGrid>
</mx:Application>

This example uses the ActionScript classes CompoundFoo and SimpleFoo to contain all the instructions for XML Mapping. In the event that the XML were to change the only places in the application that would have to change would be those mappings. All current references to that data throughout the application could remain the same.

The difference is where you do the mapping. FlexB places the mappings in the ActionScript classes so that those are the only places that have change.

How does the XML mapping work?

My preference would be to eventually have a factory class look at annotations of plain old ActionScript classes and do the mappings, but for right now I am providing a class to extend that gives this mapping functionality. Using inheritance also provides the ability to easily override all of the mapping behavior.

The following ActionScript class defines the node, attribute, and class mappings from a “CompoundFoo” XML document:

package valentino
{
    import net.sourceforge.flexb.core.IMap;
    import net.sourceforge.flexb.mapper.XmlMappedObject;

    [Bindable]
    public class CompoundFoo extends XmlMappedObject
    {
        public var myName:String;
        [ArrayElementType("valentino.SimpleFoo")]
        public var foos:Array = new Array();
        override protected function mapNodeToFunction(nodeToFunctionMap:IMap):void
        {
            nodeToFunctionMap.put("SimpleFoo", "foos");
        }
        override protected function mapAttributeNodeToFunction(attributeNodeToFunctionMap:IMap):void
        {
            attributeNodeToFunctionMap.put("my_name", "myName");
        }
        override protected function mapNodeToClass(nodeToClassMap:IMap):void
        {
            nodeToClassMap.put("SimpleFoo", SimpleFoo);
            nodeToClassMap.put("CompoundFoo", CompoundFoo);
        }
    }
}

The node to function mappings specify the name of each node and the set/get functions of the class to map to. For example when the node “SimpleFoo” is encountered during mapping its value is associated with the class property of “"foos.”

The node to attribute mappings specify the name of each attribute and the set/get functions of the class to map to. For example when the attribute “my_name” is encountered during the mapping its value is associated with the class property of “myName.”

The node to class mappings specify the name of each node and the associated ActionScript class. For example when the node “SimpleFoo” is encountered during the mapping its value is mapped as the “SimpleFoo” ActionScript class.

Behavior such as needed for the conversion of data can also be handled by using set/get functions instead of providing publically accessible class variables.

“SimpleFoo” is also a reference to another ActionScript class:

package valentino
{
    import net.sourceforge.flexb.core.IMap;
    import net.sourceforge.flexb.mapper.XmlMappedObject;

    public class SimpleFoo extends XmlMappedObject
    {
        public var name:String;
        public var foo:String;   
        public static const FOO:String = "foo";
        public static const NAME:String = "name";


        override protected function mapNodeToFunction(nodeToFunctionMap:IMap):void
        {
            nodeToFunctionMap.put(FOO, "foo");
            nodeToFunctionMap.put(NAME, "name");
        }
        override protected function mapNodeToClass(nodeToClassMap:IMap):void
        {
            nodeToClassMap.put("SimpleFoo", SimpleFoo);
        }
    }
}

What about web service mapping?

It is actually an HTTPService mapping, and it is just an easy way to get XML data from an external source and map it to an ActionScript class. In FlexB it is called XmlService.

There are 3 ways to use it:

1. Just like an HTTPService where you handle the ResultEvent and call the mapping functions.

<service:XmlService
        id="xmlServiceResultID"
        result="xmlServiceResult(event)"
        url="../testdata/CompoundFoo.xml" />

private function xmlServiceResult(event:ResultEvent):void
{

        var compoundFoo:CompoundFoo = new CompoundFoo();
        compoundFoo.map(XML(event.result));
}

2. Specify to handle an XmlResultEvent which does the mapping for you.

<service:XmlService
        id="xmlResultService"
        resultClass="{CompoundFoo}"
        xmlResult="onXmlResult(event)"
        url="../testdata/CompoundFoo.xml" />

private function onXmlResult(event:XmlResultEvent):void
{
        var compoundFoo:CompoundFoo = event.xmlMappedObject as CompoundFoo;
}

3. Specify a function to call with the mapped instance of the object.

<service:XmlService
        id="xmlFunctionService"
        resultClass="{CompoundFoo}"
        resultFunction="{xmlServiceXmlResult}"
        url="../testdata/CompoundFoo.xml" />

public function xmlServiceXmlResult(compoundFoo:CompoundFoo):void
{

}

1 comment:

Rod Coffin said...

Great post! Keep 'em coming.