making mx.rpc.xml.XMLDecoder play nice with xsi:type
August 23rd, 2009In previous posts I have outlined how to use the inbuilt (and well hidden) XML Schema classes that are available in the mx.rpc.xml package in Flex. Utilising SchemaManager and SchemaTypeRegistry one can quickly register specific ActionScript classes with specific types defined within an xml schema (XSD). Once set up it is simply a matter of using XMLDecoder and XMLEncoder to automatically serialise between XML and ActionScript and voila! the work is done.
That is unless you happen to have elements with your xml file that use an xsi:type attribute to indicate that their contents is actually that of a derivative element (such as an extension, restriction, etc). In these cases Flex just ignores the xsi:type and decodes against the original element type – which aint much help. What’s suprisising is that the Flex classes have been built with xsi:type in mind (storing the type in custom classes implementing IXMLSchemaInstance), but just seems to have been missed when implementing XMLDecoder – always saves the element type in the xsitype property of IXMLSchemaInstance
Whether you love it, hate it or are just indifferent, xsi:type is part of the XSD standard and all it takes is a few tweaks for Flex to handle it correctly.
I’ve included a CustomXMLDecoder class below, but firstly:
A quick primer on xsi:type
If you are unfamiliar with xsi:type here is a quick example of it in action:
In an XSD file we have a complex type called user:
<xs:complexType name="User">
<xs:sequence>
<xs:element name="username" type="xs:string" />
<xs:element name="password" type="xs:string" />
</xs:sequence>
</xs:complexType>
And we have another complex type that is an extension of User called InternalUser :
<xs:complexType name="InternalUser">
<xs:complexContent>
<xs:extension base="User" >
<xs:sequence>
<xs:element name="staffId" type="xs:string" />
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
In an XML file implementing this schema we have a set of User elements. As you can see below one of which includes contains an xsi:type attribute that indicates that the contents is actually that of an InternalUser.
<user >
<username>joeCitizen</username>
<password>1234</password>
</user>
<user xsi:type="InternalUser" >
<username>jamesBond</username>
<password>5678</password>
<staffId>007</staffId>
</user>
CustomXMLDecoder
After hours of trawling through mx.rpc.xml.XMLDecoder, it was surprisingly straight forward to fix the shortcoming by overriding a couple of methods within XMLDecoder.
As an added tweak i’ve included a property to the class that determines the behaviour when attemptying to decode an xsi:type that hasn’t been registered with the SchemaTypeRegistry.
ignoreXSITypeIfNotRegistered:Boolean = false;
If this property is set to true then the decoder will ignore an xsi:type that is not registered against an ActionScript class, and try to decode it against the original element type instead (esentially replicating the functionality of XMLDecoder). This ensures that unexpected xsi:type extensions to a element are decoded as the element type rather than generic object proxies.
If the xsi:type is registered then it will always use it.
Here is the class:
package
{
import mx.rpc.xml.*;
public class CustomXMLDecoder extends XMLDecoder
{
public function CustomXMLDecoder()
{
super();
}
/**
* Set this flag if you want unregistered xsi types to be ignored (and objects to be decoded against the
* base element type instread).
* If the xsi type is registered then it will always be used over the base element type
*/
public var ignoreXSITypeIfNotRegistered:Boolean = false;
/**
* Decodes a element based on the xsi:type. If the type isn't registered then it
* checks the ignoreXSITypeIfNotRegistered property of the class.
*
* If true - decodes against the element type (and ignores the xsi:type)
* If false - uses object proxy.
*/
public override function decodeElementTopLevel(definition:XML, elementQName:QName, value:*):*
{
var content:*;
if(value is XML)
{
var xsiType:QName = getXSIType(value);
var isXSITypeRegistered:Boolean = typeRegistry.getClass(xsiType) != null
if(xsiType != null && (isXSITypeRegistered || ignoreXSITypeIfNotRegistered == false))
{
content = createContent(xsiType);
decodeType(xsiType, content, elementQName, value);
return content;
}
}
return super.decodeElementTopLevel(definition, elementQName, value);
}
/**
* Decodes agains the xsi:type (if available) rather than the defined element type
*/
public override function decode(xml:*, name:QName=null, type:QName=null, definition:XML=null):*
{
if(xml is XML)
{
var xsiType:QName = getXSIType(xml);
if(xsiType != null)
type = xsiType;
}
return super.decode(xml, name, type, definition);
}
}
Hope that helps other people out there – as this was previously the only real show stopper for using the Schema classes in some of our projects.
———
One last thing,
Can anyone point out to how override the decoding of nested arrays of elements?
Ususally XML will be structured so that an array of items is wrapped within a parent element
e.g the child ‘item’ elements are located within an <items/> element
<item> <name>Parent</name> <items> <item > <name>Child A</name> </item> <item > <name>Child B</name> </item> </items> </item>
In Flex this data may be registered to a class such as this one:
package
{
[Bindable]
public class ItemVO
{
public var name:String;
public var items:ArrayCollection;
public function ItemVO():void
{
}
}
}
Unfortunately the decoder always includes the extra level when decoding – resulting in an ArrayCollection containing a single item (an ArrayCollection) that contains the array of items.
Does anyone know of a way to map this relationship better so that the items.item in the xml maps directly to the items ArrayCollection in the Class?
Ta,
Dom


