Reading nested XML in Sencha Touch with a hasMany association

In one of our recent mobility projects we were using Sencha Touch to communicate with web services that only delivered XML.  We found the Ext.data.reader.Xml to have some limitations when reading XML and specifying a hasMany association.  We found a way to overcome one of the limitations, and wanted to share our method in case it helps you on your project.

XML Example


Let's look at some simple XML:

<?xml version="1.0" encoding="utf-8" ?>
<Calendar month="September" year="2014">
     <EventItem id="3" date="09/01/2014" />
     <EventItem id="4" date="09/02/2014" />
     <EventItem id="5" date="09/03/2014" />
</Calendar>


This XML is showing a list of events on a calendar.  In Sencha Touch, we've defined models for Calendar and EventItem.  Our first thought was to add the following hasMany association to the Calendar model to populate both models when read from the XML.

hasMany: {
     model 'EventItem',
     name: 'EventItems'
}

This won't work because EventItem doesn't have a root node besides Calendar.  What Sencha Touch expects is XML that looks like this:

<?xml version="1.0" encoding="utf-8" ?>
<Calendar month="September" year="2014">
     <EventItems>
          <EventItem id="3" date="09/01/2014" />
          <EventItem id="4" date="09/02/2014" />
          <EventItem id="5" date="09/03/2014" />
     </EventItems>
</Calendar>


Fixing the Issue


So what options do we have to deal with this problem?  We can always parse the data ourselves, but that could introduce more defects into our code unless we can thoroughly test every scenario.  We opted to extend the Ext.data.reader.Xml class and allow us an option to inject the node we need.

If we read the source code for Ext.data.reader.Reader we can see readAssociated() calls a function getAssociatedDataRoot() that will return what Sencha Touch thinks should be the root of the hasMany relationship.  In our case, we needed to return EventItems here.

To return what we wanted, we added a property to the hasMany object to take what the associated root name should be.  We modified our calendar model to look like this:

hasMany: {
     model 'EventItem',
     name: 'EventItems',
     associatedName: 'EventItems' // will add an EventItems node
}

Then, we defined our own XML reader, and overrode the functions we needed to use the new associatedName property.

Ext.define('Sample.data.reader.Xml', {

     extend: 'Ext.data.reader.Xml',

     /**
      * Most of this is taken from Ext.data.reader.Reader.
      */
     readAssociated: function (record, data) {
          var associations = this.getModel().associations.items,
              length = associations.length,
              i = 0,
              association, 
              associationData, 
              associatedName
              associationKey;

          for (; i < length; i++) {
              association = associations[i];
              associatedName = association.getAssociatedName();
              associationKey = association.getAssociationKey();
              associationData = this.getAssociatedDataRoot(
                   data, associatedName, associationKey);

              if (associationData) {
                   record[associationKey] = associationData;
              }
          }
     },

     /**
      * We add the root node before calling 
      * getAssociatedDataRoot() of Ext.data.reader.Xml.
      */
     getAssociatedDataRoot: function (
          data, 
          associatedName
          associationKey) {

          $(data).find(associatedName)
               .wrapAll('<' + associationKey + '>')';
          return this.callParent([data, associationKey]);
     }
}

You'll noticed we're using jQuery here.  That's because we used it in another part of the project so it was available to use.  Adding the node could be done a number of ways.

What Just Happened?


In our case, we needed an additional node to exist before Sencha Touch would cleanly turn this XML into populated models.  This happens in the reader, so we extended the Sencha Touch XML reader to allow us to have a property to specify what this additional node should be called when it doesn't exist.  We overrode functions from Ext.data.reader.Xml to add that additional node and used the hasMany property in our model to specify what we want to name the new node.

Summary


Our approach is one of many that could work.  We're also familiar with the data we're using so we know how this affects the rest of the readers.  In other applications, we may want to only use this reader in some circumstances.  If you've run into this problem, we hope our solution can provide some guidance on one of the ways this can be solved.

Labels: , , ,