Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

Working with the Flex DataGrid and Nested Data Structures

by
Gift

Want a free year on Tuts+ (worth $180)? Start an InMotion Hosting plan for $3.49/mo.

It often happens that the data which needs to be viewed/presented in a Flex DataGrid comes from an XML file or JSON with more than one level of nesting. Unfortunately, by default, the Flex DataGrid allows you to display only single level nested object arrays.

This tutorial shows how you can extend the Flex DataGrid class to accomodate more complicated data structures. It will also show you how to make all the columns sortable, even when using nested data structures.

Introduction

This tutorial assumes that you know the basics of Flex, how to use Flex Builder, and how to write MXML files. You should have a copy of Flex Builder installed on your system.

Step 1: Setup the Flex Project

The first step is to setup the project in Flex Builder. Create a new project in Flex Builder with Project Name as "NestedDataGrid" and Application Type as "Web application (runs in Flash Player)". Leave all other options at their default values and click Finish.

Creating the new project

Step 2: Import Sample Data

The data that we're going to show in the DataGrid is obtained from an XML file. Create a folder in your 'src' folder called 'assets' and put the data shown below in a file called 'meetings.xml'. Alternatively, you can download the XML file from here and put it in the 'assets' folder.

<?xml version="1.0" encoding="UTF-8"?>
<meetings>
  <meeting>
    <priority>high</priority>
    <presenter>
      <name>Lisa Green</name>
      <email>jdoe@company.com</email>
      <phone>+330-7593</phone>
    </presenter>
    <date>12th July 2009</date>
    <time>6:00 pm</time>
    <place>Room 405</place>
  </meeting>
  <meeting>
    <priority>medium</priority>
    <presenter>
      <name>Christopher Martin</name>
      <email>cmartin@company.com</email>
      <phone>+330-7553</phone>
    </presenter>
    <date>14th July 2009</date>
    <time>11:00 am</time>
    <place>Room 405</place>
  </meeting>
  <meeting>
    <priority>high</priority>
    <presenter>
      <name>George Rodriguez</name>
      <email>grodriguez@company.com</email>
      <phone>+330-7502</phone>
    </presenter>
    <date>18th July 2009</date>
    <time>10:00 am</time>
    <place>Room 771</place>
  </meeting>
  <meeting>
    <priority>high</priority>
    <presenter>
      <name>Jennifer Parker</name>
      <email>jparker@company.com</email>
      <phone>+330-5380</phone>
    </presenter>
    <date>20th August 2009</date>
    <time>2:00 pm</time>
    <place>Room 562</place>
  </meeting>
</meetings>

Step 3: Make the Interface

Here's a quick breakdown of building the interface to display the data and the appropriate ID values required for the code in this tutorial:

  1. Open the NestedDataGrid.mxml file, and go to the design view
  2. Drag and drop a 'Panel' from the Components view. Set its ID to "meetingsPanel" and Title to "Meetings"
  3. Set the height and width of the Panel to 500 and set the X and Y values to 0
  4. Drag and drop a 'DataGrid' onto the panel
  5. Set the X and Y values to 10
  6. Set the width to {meetingsPanel.width-40} and height to 45%
  7. Go to the source view, and in the 'mx:Appication' tag, add the attribute layout="vertical"

Your interface should look similar to that shown in the image below:

The interface

The MXML in the source view should look like this:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical">
  <mx:Panel x="0" y="0" width="500" height="500" layout="absolute" id="meetingsPanel" title="Meetings">
    <mx:DataGrid x="10" y="10" width="{meetingsPanel.width-40}" height="45%">
      <mx:columns>
        <mx:DataGridColumn headerText="Column 1" dataField="col1"/>
        <mx:DataGridColumn headerText="Column 2" dataField="col2"/>
        <mx:DataGridColumn headerText="Column 3" dataField="col3"/>
      </mx:columns>
    </mx:DataGrid>
  </mx:Panel>
</mx:Application>

Reading in the XML File

In the next three steps, we create an HTTPService component, read the data from the XML file and store it in a local variable. This is done in three stages:

Step 4: Create the HTTPService Component

Switch to the Source view of the MXML file and add the following code right below the mx:Application tag:

    <mx:HTTPService id="readXML"
      url="assets/meetings.xml" resultFormat="object"
      result="httpResultHandler(event)" fault="httpFaultHandler(event)" />

The httpResultHandler() function will be called when the data has been fetched. If there's an error in fetching the data, the httpFaultHandler() function is called. Note that this only creates the HTTPService object, the data has to be fetched by an explicit function call (See sub-step 3)

Step 5: httpResultHandler() and httpFaultHandler()

Add an mx:Script tag just below the mx:Application tag. Inside that, define the variable that will hold the incoming data and the functions to handle the events from the HTTPService component. The code to do that looks like this:

      import mx.rpc.events.FaultEvent;
      import mx.rpc.events.ResultEvent;
      import mx.collections.ArrayCollection;
      import mx.controls.Alert;
      
      [Bindable]
      public var dataForGrid:ArrayCollection;
      
      private function httpResultHandler(event:ResultEvent):void{
        dataForGrid = event.result.meetings.meeting;
      }
      
      private function httpFaultHandler(event:FaultEvent):void{
        Alert.show("Error occurred in getting string");
      }

The variable 'dataForGrid' holds the data that we are going to read in. The '[Bindable]' tag makes sure that whenever the data changes (when it is read in), the DataGrid is updated accordingly. The XML is read in as an object which is passed throught the 'ResultEvent' event, and 'event.result.meetings.meeting' accesses the ArrayCollection of 'meeting' objects.

Step 6: Get the Data From the XML File

In this step, the actual function call to get the XML data is done. An intialize function is assigned to the event that is triggered everytime the application loads - the 'creationComplete' event. Add the attribute creationComplete="getData()" to the 'mx:Application' tag and define the function 'getData()' as below (to be added after the 'httpFaultHandler' function):

      private function getData():void{
        readXML.send();
      }

This makes the HTTPService object get the data from the file. Once the data is retrieved, the 'result' event is triggered which calls the 'httpResultHandler()' function. If there's a problem getting the data, the 'fault' event is triggered, which calls the httpFaultHandler() function.

Step 7: Milestone

At this point your NestedDataGrid.mxml should look like this:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="getData()">
<mx:Script>
  <![CDATA[
      import mx.rpc.events.FaultEvent;
      import mx.rpc.events.ResultEvent;
      import mx.collections.ArrayCollection;
      import mx.controls.Alert;
        
      [Bindable]
      public var dataForGrid:ArrayCollection;

      private function httpResultHandler(event:ResultEvent):void{
        dataForGrid = new ArrayCollection(event.result.meetings.meeting);
      }
      
      private function httpFaultHandler(event:FaultEvent):void{
        Alert.show("Error occurred in getting string");
      }
      
      private function getData():void{
        readXML.send();
      }

  ]]>
</mx:Script>
    <mx:HTTPService id="readXML"
      url="assets/meetings.xml" resultFormat="object"
      result="httpResultHandler(event)" fault="httpFaultHandler(event)" />
  <mx:Panel width="500" height="500" layout="vertical" id="meetingsPanel" title="Meetings">
    <mx:DataGrid width="{meetingsPanel.width-40}" height="45%">
      <mx:columns>
        <mx:DataGridColumn headerText="Column 1" dataField="col1"/>
        <mx:DataGridColumn headerText="Column 2" dataField="col2"/>
        <mx:DataGridColumn headerText="Column 3" dataField="col3"/>
      </mx:columns>
    </mx:DataGrid>
  </mx:Panel>
</mx:Application>

Step 8: DataGrid with Non-Nested Data

I'll just briefly point out why nested data poses problems in display, by first demonstrating how you show non-nested data. Say, from the XML file above, you only wanted to show the date, place and priority of the meetings (and not the presenter information). The below code will be able to display it without any problems (contents of 'mx:Panel' shown here. All other code is the same):

    <mx:DataGrid width="{meetingsPanel.width-40}" height="45%" dataProvider="{dataForGrid}">
      <mx:columns>
        <mx:DataGridColumn headerText="Priority" dataField="priority"/>
        <mx:DataGridColumn headerText="Date" dataField="date"/>
        <mx:DataGridColumn headerText="Time" dataField="time"/>
        <mx:DataGridColumn headerText="Place" dataField="place"/>
      </mx:columns>
    </mx:DataGrid>

The result of the this would be the following application:

Note that the dataProvider attribute of the DataGrid can be directly assigned to the the ArrayCollection 'dataForGrid', and each DataGridColumn inside is given a dataField attribute that directly corresponds to the property name. However, suppose you want to access the presenter's name information, it can be accessed as "presenter.name". If you try giving this value to the 'dataField' you'll get an error. This is because, Flex doesn't support nested objects by default. Read on to learn how to solve this problem by extending the DataGridColumn class and writing your own code to handle this case.

Step 9: Creating the NestedDataGridColumn Class

We redefine some functions in the DataGrid column class to circumvent the problem outlined above. First create a folder in the 'src' directory called 'classes'. Create a new 'ActionScript Class' in that folder, named "NestedDataGridColumn". In the 'Superclass' field, click 'Browse...' and select 'DataGridColumn' from the list that pops up. Leave everything else at the default values, and click 'Finish'. A new file should have been created and populated with the following code:

package classes
{
  import mx.controls.dataGridClasses.DataGridColumn;

  public class NestedDataGridColumn extends DataGridColumn
  {
    public function NestedDataGridColumn(columnName:String=null)
    {
      super(columnName);
    }
    
  }
}

Step 10: Declaring the 'nestedDataField' Property

In the NestedDataGridColumn class, add a public bindable variable called 'nestedDataField'. We'll use this instead of the default 'dataField' property to pass the field name. This is vital, because if the default 'dataField' property is used, an error saying Error: Find criteria must contain at least one sort field value. will occur when we try to sort the DataGrid after having defined the custom sort function later on.

Step 11: Redefining the 'itemToLabel' Funtion

As you can see, the new class we created has already been populated with a constructor. Leave the constructor as it is and below that add the following function:

override public function itemToLabel(data:Object):String
    {
      var fields:Array;
      var label:String;
        
      var dataFieldSplit:String = nestedDataField;
      var currentData:Object = data;
      
      //check if the nestedDataField value contains a '.' (i.e. is accessing a nested value)
      if(nestedDataField.indexOf(".") != -1)
      {  
        //get all the fields that need to be accessed
        fields = dataFieldSplit.split("."); 
      
        for each(var f:String in fields)
        //loop through the fields one by one and get the final value, going one field deep every iteration
          currentData = currentData[f]; 
       
        if(currentData is String)
        //return the final value
          return String(currentData);
      }
      
      //if there is no nesting involved
      else 
      {
        if(dataFieldSplit != "")
            currentData = currentData[dataFieldSplit];
      }
      
      //if our method hasn't worked as expected, resort to calling the default function
      try
      {
        label = currentData.toString();
      }
      catch(e:Error)
      {
        label = super.itemToLabel(data);
      }
        
      //return the result
      return label;
    }

Redefining the 'itemToLabel' function is the key to being able to access nested data in your DataGrid. The itemToLabel function controls what is shown in the DataGrid rows. So we use it here to ask Flex to show the nested data in the manner that we have specified.

As you can see, the function definition starts with the 'override' keyword, which means the default pre-defined function of the same name is being overridden in favour of the function you have defined. Each statement is explained in the comments. In brief, what the function does is check if nested data fields is being accessed (if a '.' is present). If it is, get each data field name, and iterate through the dataProvider, going deeper each step by accessing the child field.

The 'itemToLabel' function is called by Flex with an argument which contains the ArrayCollection that was specified as the dataProvider to the dataGrid. All attributes of the NestedDataGridColumn (when it is used in the mxml file) are directly accessible, and any public properties defined in this class can be assigned a value in the MXML. In our NestedDataGrid.mxml file, we replace the 'mx:DataGridColumn' components with 'classes:NestedDataGridColumn' component, and assign the specific elements that we want to show in the columns to the 'nestedDataField' attribute (which was declared in the 'NestedDataGridColumn.as' file). The DataGridColumn now should look like this:

    <mx:DataGrid x="10" y="10" width="{meetingsPanel.width-40}" height="45%" dataProvider="{dataForGrid}">
      <mx:columns>
        <classes:NestedDataGridColumn headerText="Priority" nestedDataField="priority" width="60"/>
        <classes:NestedDataGridColumn headerText="Presenter Name" nestedDataField="presenter.name"  sortable="false"/>
        <classes:NestedDataGridColumn headerText="Presenter Phone" nestedDataField="presenter.phone" width="90"  sortable="false"/>
        <classes:NestedDataGridColumn headerText="Date" nestedDataField="date" width="110"/>
        <classes:NestedDataGridColumn headerText="Time" nestedDataField="time" width="70"/>
        <classes:NestedDataGridColumn headerText="Place" nestedDataField="place" width="70"/>
      </mx:columns>
    </mx:DataGrid>

Note that I'm directly specifying the 'nestedDataField' property as the "presenter.name" and "presenter.phone" here. Also, I've added widths to the columns and set the width of the 'mx:Panel' component to 600px for better display. I've added the 'sortable' property to be false to the two new columns. If you remove this (or set it to true) and run the program the column won't sort. We'll solve this in the next step by defining our own sort function. For now the application should look like this:

Step 12: Writing the Custom Sort Function

The only thing left now is to define the custom sorting function so that sorting will also be enabled in all the fields (say, you want to sort the presenter names alphabetically). Add the function called 'mySortCompareFunction' below the 'itemToLabel' function as given:

    private function mySortCompareFunction(obj1:Object, obj2:Object):int{
      var fields:Array;
      var dataFieldSplit:String = nestedDataField;
      var currentData1:Object = obj1;
      var currentData2:Object = obj2;
      
      if(nestedDataField.indexOf(".") != -1)
      { 
        fields = dataFieldSplit.split(".");
      
        for each(var f:String in fields){
          currentData1 = currentData1[f];
          currentData2 = currentData2[f];
        }

      }
      else
      {
        if(dataFieldSplit != ""){
            currentData1 = currentData1[dataFieldSplit];
            currentData2 = currentData2[dataFieldSplit];
         }
      }
      
      if(currentData1 is int && currentData2 is int){
        var int1:int = int(currentData1);
        var int2:int = int(currentData2);
        var result:int = (int1>int2)?-1:1; 
        return result;
      }
      if(currentData1 is String && currentData2 is String){
        currentData1 = String(currentData1);
        currentData2 = String(currentData2);
        return (currentData1>currentData2)?-1:1;
      }
      if(currentData1 is Date && currentData2 is Date){
        var date1:Date = currentData1 as Date;
        var date2:Date = currentData2 as Date;
        var date1Timestamp:Number = currentData1.getTime();
        var date2Timestamp:Number = currentData2.getTime();
        return (date1Timestamp>date2Timestamp)?-1:1;
      }
      
      return 0;
    }

This function will be called by Flex with two objects, and it's expected to return either -1.0 or 1 depending on whether the first object is greater than, equal to, or less than, respectively, the second object. Flex takes care of the actual sorting.

This function uses the same logic as the 'itemToLabel' function to get the appropriate nested value. Then depending upon the type of the value (whether it be int, String, or Date) it compares it appropriately, and returns -1 if 'currentData1' is greater than 'currentData2', 0 if they are equal, and 1 if 'currentData2' is greater than 'currentData1'.

Step 13: Hooking up the Custom Sort Function

If you noticed, 'customSortCompareFunction' is not a predefined function in the class DataGridColumn which we override. This function has to be assigned as the sorting function in a different way. We have to assign to the pre-defined variable 'sortCompareFunction' the name of the sorting function, which is 'customSortCompareFunction' in our case. This should be done inside the constructor. The constructor now looks like this:

public function NestedDataGridColumn(columnName:String=null)
    {
      //the custom sort function being assigned to the pre-defined variable
      sortCompareFunction = mySortCompareFunction;
      super(columnName);
    }

Once this is done, you're all set. You now have a custom class that can handle arbitrarily nested data to show in a DataGrid. And you can sort the grid as you want.

Conclusion

Today you learnt how to circumvent a limitation of the FlexDataGrid class to get arbitrarily nested data, and show it in a DataGrid. You also learned how to define your own sorting function so that the grid remains sortable. You can now use this NestedDataGridColumn in all your applications without any overhead.

You can further extend the 'itemToLabel' function to include other arbitrary format of access - say, accessing arrays inside the child objects, or accessing xml attributes. You can also further extend the sort function to sort other types of data. You might also be able to add other features like colouring the rows based on the meeting priority and showing more details about the presenter by clicking a row.

Thanks for reading :)

Advertisement