Adobe Flex 3 Help

Hierarchical data objects

You use hierarchical data objects with the controls that display a nested hierarchy of nodes and subnodes, such as tree branches and leaves, as well as Menu submenus and items. The following controls use hierarchical data objects:

The hierarchical components all use the same mechanism to work with the data provider. The following examples use the Tree control, but the examples apply to the other components.

About hierarchical data objects

The Flex framework, by default, supports two types of hierarchical data objects.

XML 

can be any of the following: Strings containing well-formed XML; or XML, XMLList, or XMLListCollection objects, including objects generated by the <mx:XML> and <mx:XMLList> compile-time tags. (These tags support data binding, which you cannot do directly in ActionScript.) Flex can automatically structure a Tree or menu-based control to reflect the nesting hierarchy of well-formed XML.



Objects 

can be any set of nested Objects or Object subclasses (including Arrays or ArrayCollection objects) that have a structure where the children of a node are in a children field. For more information, see Creating a custom data descriptor. You can also use the <mx:Model> compile-time tag to create nested objects that support data binding, but you must follow the structure defined in Using the <mx:Model> tag with Tree and menu-based controls.



You can add support for other hierarchical data provider structures, such as nested Objects where the children might be in fields with varying names.

Data descriptors and hierarchical data structure

Hierarchical data used in Tree and menu-based controls must be in a form that can be parsed and manipulated by using a data descriptor class. A data descriptor is a class that provides an interface between the hierarchical control and the data provider object. It implements a set of control-specific methods to determine the data provider contents and structure; to get, add, and remove data; and to change control-specific data properties.

Flex defines two data descriptor interfaces for hierarchical controls:

ITreeDataDescriptor Methods used by Tree controls

IMenuDataDescriptor Methods for Menu, MenuBar, and PopUpMenuButton controls

The Flex framework provides a DefaultDataDescriptor class that implements both interfaces. You can use the dataDescriptor property to specify a custom data descriptor class that handles data models that do not conform to the default descriptor structure.

Data descriptor methods and source requirements

The following table describes the methods of both interfaces, and the behavior of the DefaultDataDescriptor class. The first line of each interface/method entry indicates whether the method belongs to the ITreeDataDescriptor interface, the IMenuDataDescriptor interface, or both interfaces, and therefore indicates whether the method is used for trees, menus, or both.

Method

Returns

DefaultDataDescriptor behavior

hasChildren(node, [model])

A Boolean value indicating whether the node is a branch with children.

For XML, returns true if the node has at least one child element.

For other objects, returns true if the node has a nonempty children field.

getChildren(node, [collection])

A node's children.

For XML, returns an XMLListCollection with the child elements.

For other Objects, returns the contents of the node's children field.

isBranch(node, [collection])

Whether a node is a branch.

For XML, returns true if the node has at least one child, or if it has an isBranch attribute.

For other Objects, returns true if the node has an isBranch field.

getData(node, [collection])

The node data.

Returns the node.

addChildAt(node, child, index, [model])

A Boolean value indicating whether the operation succeeded.

For all cases, inserts the node as a child object before the node currently in the index location.

removeChildAt
(node, index, [model])

A Boolean value indicating whether the operation succeeded.

For all cases, removes the child of the node in the index location.

getType(node)

(IMenuDataDescriptor only)

A String with the menu node type. Meaningful values are check, radio, and separator.

For XML, returns the value of the type attribute of the node.

For other Objects, returns the contents of the node's type field.

isEnabled(node)

(IMenuDataDescriptor only)

A Boolean value indicating whether a menu node is enabled.

For XML, returns the value of the enabled attribute of the node.

For other Objects, returns the contents of the node's enabled field.

setEnabled(node, value)

(IMenuDataDescriptor only)

 

For XML, sets the value of the enabled attribute of the node to true or false.

For other Objects, sets the contents of the node's enabled field.

isToggled(node)

(IMenuDataDescriptor only)

A Boolean value indicating whether a menu node is selected

Returns the value of the node's toggled attribute.

setToggled(node, value)

(IMenuDataDescriptor only)

 

For XML, sets the value of the selected attribute of the node to true or false.

For other Objects, sets the contents of the node's enabled field.

getGroupName(node)

(IMenuDataDescriptor only)

The name of the radio button group to which the node belongs.

For XML, returns the value of the groupName attribute of the node.

For other Objects, returns the contents of the node's groupName field.

The following example Object follows the default data provider structure for a Tree control, and is correctly handled by the DefaultDataDescriptor class:

[Bindable]
public var fileSystemStructure:Object = 
    {label:"mx", children: [
        {label:"Containers", children: [
            {label:"Accordian", children:[]},
            {label:"DividedBox", children: [
                {label:"BoxDivider.as", data:"BoxDivider.as"}, 
                {label:"BoxUniter.as", data:"BoxUniter.as"}]},
            {label: "Grid", children:[]}]},
        {label: "Controls", children: [
            {label: "Alert", data: "Alert.as"},
            {label: "Styles", children: [
                {label: "AlertForm.as", data:"AlertForm.as"}]},
            {label: "Tree", data: "Tree.as"},
            {label: "Button", data: "Button.as"}]},
        {label: "Core", children:[]}
    ]};

For objects, the root is the Object instance, so there must always be a single root (as with XML). You could also use an Array containing nested Arrays as the data provider. In this case the provider has no root; each element in the top level array appears at the top level of the control.

The DefaultDataDescriptor can properly handle well-formed XML nodes. The isBranch() method, however, returns true only if the parameter node has child nodes or if the node has an isBranch attribute with the value true. Therefore, if your XML object uses any technique other than a true isBranch attribute to indicate empty branches, you must create a custom data descriptor.

The DefaultDataDescriptor handles collections properly. For example, if a node's children property is an ICollectionView instance, the getChildren() method returns the children as an ICollectionView object.

Using the <mx:Model> tag with Tree and menu-based controls

The <mx:Model> tag lets you define a data provider structure in MXML. The Flex compiler converts the contents of the tag into a hierarchical graph of ActionScript Objects. The <mx:Model> tag has two advantages over defining an Object data provider in ActionScript:

  • You can define the structure by using an easily read, XML-like format.
  • You can bind structure entries to ActionScript variables, so that you can use <mx:Model> to create an object-based data provider that gets its data from multiple dynamic sources.

To use an <mx:Model> tag with a control that uses a data descriptor, the object generated by the compiler must conform to the data descriptor requirements, as discussed in Data descriptors and hierarchical data structure. Also, as with an XML object, the tag must have a single root element.

In most situations, you should consider using an <mx:XML> or <mx:XMLList> tag, as described in XML-based data objects, instead of using an <mx:Model> tag. The XML-based tags support data binding to elements, and the DefaultDataDescriptor class supports all well-structured XML. Therefore you can use a more natural structure, where node names can represent their function, and you do not have to artificially name nodes "children."

To use an <mx:Model> tag as the data provider for a control that uses the DefaultDataDescriptor class, all child nodes must be named "children." This requirement differs from the structure that you use with an Object, where the array that contains the child objects is named "children".

The following example shows the use of an <mx:Model> tag with data binding as a data provider for a menu, and shows how you can change the menu structure dynamically:

<?xml version="1.0"?>
<!-- dpcontrols\ModelWithMenu.mxml -->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="*">
    <mx:Script>
        <![CDATA[

            import mx.controls.Menu;
            public var productMenu:Menu;

            public function initMenu(): void
            {
                productMenu = Menu.createMenu(null, Products.Department);
                productMenu.setStyle("disabledColor", 0xCC3366);
                productMenu.show(10,10);
            }

        ]]>
    </mx:Script>

    <mx:Model id="Products">
        <Root>
            <Department label="Toys">
                <children label="Teddy Bears"/>
                <children label="Action Figures"/>
                <children label="Building Blocks"/>
            </Department>
            <Department label="Kitchen">
                <children label="Electronics">
                    <children label="Crock Pot"/>
                    <children label="Panini Grill"/>
                </children>
                <children label="Cookware">
                    <children label="Grill Pan"/>
                    <children label="Iron Skillet" enabled="false"/>
                </children>
            </Department>
            <!-- The items in this entry are bound to the form data -->
            <Department label="{menuName.text}">
                <children label="{item1.text}"/>
                <children label="{item2.text}"/>
                <children label="{item3.text}"/>
            </Department>
        </Root>
    </mx:Model>

    <mx:Button label="Show Products" click="initMenu()"/>
    <!-- If you change the contents of the form, the next time you 
        display the Menu, it will show the updated data in the last 
        main menu item. -->
    <mx:Form>
        <mx:FormItem label="Third Submenu title">
            <mx:TextInput id="menuName" text="Clothing"/>
        </mx:FormItem>
        <mx:FormItem label="Item 1">
            <mx:TextInput id="item1" text="Sweaters"/>
        </mx:FormItem>
        <mx:FormItem label="Item 2">
            <mx:TextInput id="item2" text="Shoes"/>
        </mx:FormItem>
        <mx:FormItem label="Item 3">
            <mx:TextInput id="item3" text="Jackets"/>
        </mx:FormItem>
    </mx:Form>
</mx:Application>

The executing SWF file for the previous example is shown below:

Creating a custom data descriptor

If your hierarchical data does not fit the formats supported by the DefaultDataDescriptor class--for example, if your data is in an object that does not use a children field--you can write a custom data descriptor and specify it in your Tree control's dataDescriptor property. The custom data descriptor must implement all methods of the ITreeDataDescriptor interface.

The following example shows how you can create a custom data descriptor--in this case, for use with a Tree control. This data descriptor correctly handles a data provider that consists of nested ArrayCollection objects.

The following code shows the MyCustomTreeDataDescriptor class, which implements only the ITreeDataDescriptor interface, so it supports Tree controls but not menu-based controls. The custom class supports tree nodes whose children field is either an ArrayCollection or an Object. When getting a node's children, if the children object is an ArrayCollection, it returns the object; otherwise, it wraps the children object in an ArrayCollection before returning it. When adding a node, it uses a different method to add the node, depending on the children field type.

package myComponents
// myComponents/MyCustomTreeDataDescriptor.as
{
import mx.collections.ArrayCollection;
import mx.collections.CursorBookmark;
import mx.collections.ICollectionView;
import mx.collections.IViewCursor;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import mx.controls.treeClasses.*;

public class MyCustomTreeDataDescriptor implements ITreeDataDescriptor
{

    // The getChildren method requires the node to be an Object
    // with a children field.
    // If the field contains an ArrayCollection, it returns the field
    // Otherwise, it wraps the field in an ArrayCollection.
    public function getChildren(node:Object,
        model:Object=null):ICollectionView
    {
        try
        {
            if (node is Object) {
                if(node.children is ArrayCollection){
                    return node.children;
                }else{
                    return new ArrayCollection(node.children);
                }
            }
        }
        catch (e:Error) {
            trace("[Descriptor] exception checking for getChildren");
        }
        return null;
    }

    // The isBranch method simply returns true if the node is an
    // Object with a children field.
    // It does not support empty branches, but does support null children
    // fields.
    public function isBranch(node:Object, model:Object=null):Boolean {
        try {
            if (node is Object) {
                if (node.children != null)  {
                    return true;
                }
            }
        }
        catch (e:Error) {
            trace("[Descriptor] exception checking for isBranch");
        }
        return false;
    }

    // The hasChildren method Returns true if the
    // node actually has children. 
    public function hasChildren(node:Object, model:Object=null):Boolean {
        if (node == null) 
            return false;
        var children:ICollectionView = getChildren(node, model);
        try {
            if (children.length > 0)
                return true;
        }
        catch (e:Error) {
        }
        return false;
    }
    // The getData method simply returns the node as an Object.
    public function getData(node:Object, model:Object=null):Object {
        try {
            return node;
        }
        catch (e:Error) {
        }
        return null;
    }

    // The addChildAt method does the following:
    // If the parent parameter is null or undefined, inserts
    // the child parameter as the first child of the model parameter.
    // If the parent parameter is an Object and has a children field,
    // adds the child parameter to it at the index parameter location.
    // It does not add a child to a terminal node if it does not have
    // a children field.
    public function addChildAt(parent:Object, child:Object, index:int, 
            model:Object=null):Boolean {
        var event:CollectionEvent = new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
        event.kind = CollectionEventKind.ADD;
        event.items = [child];
        event.location = index;
        if (!parent) {
            var iterator:IViewCursor = model.createCursor();
            iterator.seek(CursorBookmark.FIRST, index);
            iterator.insert(child);
        }
        else if (parent is Object) {
            if (parent.children != null) {
                if(parent.children is ArrayCollection) {
                    parent.children.addItemAt(child, index);
                    if (model){
                        model.dispatchEvent(event);
                        model.itemUpdated(parent);
                    }
                    return true;
                }
                else {
                    parent.children.splice(index, 0, child);
                    if (model)
                        model.dispatchEvent(event);
                    return true;
                }
            }
        }
        return false;
    }

    // The removeChildAt method does the following:
    // If the parent parameter is null or undefined,
    // removes the child at the specified index
    // in the model.
    // If the parent parameter is an Object and has a children field,
    // removes the child at the index parameter location in the parent.
    public function removeChildAt(parent:Object, child:Object, index:int, model:Object=null):Boolean
    {
        var event:CollectionEvent = new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
        event.kind = CollectionEventKind.REMOVE;
        event.items = [child];
        event.location = index;

        //handle top level where there is no parent
        if (!parent)
        {
            var iterator:IViewCursor = model.createCursor();
            iterator.seek(CursorBookmark.FIRST, index);
            iterator.remove();
            if (model)
                model.dispatchEvent(event);
            return true;
        }
        else if (parent is Object)
        {
            if (parent.children != undefined)
            {
                parent.children.splice(index, 1);
                if (model) 
                    model.dispatchEvent(event);
                return true;
            }
        }
        return false;
    }

}
}

The following example uses the MyCustomTreeDataDescriptor to handle hierarchical nested ArrayCollections and objects. When you click the button, it adds a node to the tree by calling the data descriptor's addChildAt() method. Notice that you would not normally use the addChildAt() method directly. Instead, you would use the methods of a Tree or menu-based control, which in turn use the data descriptor methods to modify the data provider.

<?xml version="1.0" encoding="iso-8859-1"?>
<!-- dpcontrols\CustDataDescriptor.mxml -->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns="*" creationComplete="initCollections()">

    <mx:Script>
        <![CDATA[
            import mx.collections.*;
            import mx.controls.treeClasses.*;
            import myComponents.*;
    
            /* Variables used to construct the ArrayCollection data provider
               First top-level node and its children. */
            public var nestArray1:Array = [
                {label:"item1", children: [
                    {label:"item1 child", children:     [
                        {label:"item 1 child child", data:"child data"}
                    ]}
                ]}
            ]; 
            /* Second top-level node and its children. */
            public var nestArray2:Array = [
                {label:"item2", children: [
                    {label:"item2 child", children: [
                        {label:"item 2 child child", data:"child data"}
                    ]}
                ]}
            ];
            /* Second top-level node and its children. */
            public var nestArray3:Array = [
                {label:"item3", children: [
                    {label:"item3 child", children: [
                        {label:"item 3 child child", data:"child data"}
                    ]}
                ]}
            ]; 
            /* Variable for the tree array. */
            public var treeArray:Array
            /* Variables for the three Array collections that correspond to the 
               top-level nodes. */
            public var col1:ArrayCollection;
            public var col2:ArrayCollection;
            public var col3:ArrayCollection;
            
            /* Variable for the ArrayCollection used as the Tree data provider. */
            [Bindable]
            public var ac:ArrayCollection;
            
            /* Build the ac ArrayCollection from its parts. */
            public function initCollections():void{
                /* Wrap each top-level node in an ArrayCollection. */
                col1 = new ArrayCollection(nestArray1);
                col2 = new ArrayCollection(nestArray2);
                col3 = new ArrayCollection(nestArray3);

                /* Put the three top-level node
                   ArrayCollections in the treeArray. */
                treeArray = [
                    {label:"first thing", children: col1},
                    {label:"second thing", children: col2},
                    {label:"third thing", children: col3},
                ]; 

                /* Wrap the treeArray in an ArrayCollection. */
                ac = new ArrayCollection(treeArray);
            }

            /* Adds a child node as the first child of the selected node,
               if any. The default selectedItem is null, which causes the
               data descriptor addChild method to add it as the first child
               of the ac ArrayCollection. */
            public function clickAddChildren():void {
                var newChild:Object = new Object();
                newChild.label = "New Child";
                newChild.children = new ArrayCollection();
                tree.dataDescriptor.addChildAt(tree.selectedItem, newChild, 0, ac);
            }

        ]]>
    </mx:Script>

    <mx:Tree width="200" id="tree" dataProvider="{ac}" 
        dataDescriptor="{new MyCustomTreeDataDescriptor()}"/>    
    <mx:Button label="Add Child" click="clickAddChildren()"/>
</mx:Application>

The executing SWF file for the previous example is shown below:

XML-based data objects

The data for a tree is often retrieved from a server in the form of XML, but it can also be well-formed XML defined within the <mx:Tree> tag. The DefaultDataDescriptor class can handle well-formed XML data structures.

You can use an <mx:XML> or <mx:XMLList> tag to define an XML or XMLList object in MXML. Unlike the XML and XMLList classes in ActionScript, these tags let you use MXML binding expressions in the XML text to extract node contents from variable data. For example, you can bind a node's name attribute to a text input value, as in the following example:

<mx:XMLList id="myXMLList">
    <child name="{textInput1.text}"/>
    <child name="{textInput2.text}"/>
</mx:XMLList>

You can use an XML object directly as a data provider to a hierarchical data control. However, if the object changes dynamically, you should do the following:

  1. Convert the XML or XMLList object to an XMLListCollection object.
  2. Make all updates to the data by modifying the XMLListCollection object.

Doing this ensures that the component represents the dynamic data. The XMLListCollection class supports the use of all IList and ICollectionView interface methods, and adds many of the most commonly used XMLList class methods. For more information on using XMLListCollections, see XMLListCollection objects.

The following code example defines two Tree controls. The first uses an XML object directly, and the second uses an XMLListCollection object as the data source:

<?xml version="1.0"?>
<!-- dpcontrols\UseXMLDP.mxml -->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:XML id="capitals">
        <root>
            <Capitals label="U.S. State Capitals">
                <capital label="AL" value="Montgomery"/>
                <capital label="AK" value="Juneau"/>
                <capital label="AR" value="Little Rock"/>
                <capital label="AZ" value="Phoenix"/>       
            </Capitals>
            <Capitals label="Canadian Province Capitals">
                <capital label="AB" value="Edmonton"/>
                <capital label="BC" value="Victoria"/>
                <capital label="MB" value="Winnipeg"/>
                <capital label="NB" value="Fredericton"/>
            </Capitals>
        </root>
    </mx:XML>

    <!-- Create an XMLListCollection representing the Tree nodes.
         capitals.Capitals is an XMLList with both Capitals elements. -->
    <mx:XMLListCollection id="capitalColl" source="{capitals.Capitals}"/>
    
    <mx:Label text="These two Tree controls appear identical, although their data sources are different."/>

    <mx:HBox>
        <!-- When you use an XML-based data provider with a tree
             you must specify the label field, even if it
             is "label". The XML object includes the root,
             so you must set showRoot="false". Remember that
             the Tree will not, by default, reflect dynamic changes
             to the XML object. -->
        <mx:Tree id="Tree1" dataProvider="{capitals}" labelField="@label"
            showRoot="false" width="300"/> 
        
        <!-- The XMLListCollection does not include the XML root. -->
        <mx:Tree id="Tree2" dataProvider="{capitalColl}" labelField="@label" 
            width="300"/>       
    </mx:HBox>
</mx:Application>

The executing SWF file for the previous example is shown below:

This example shows two important features of using a hierarchical data provider with a Tree control:

  • ECMAScript for XML (E4X) objects must have a single root node, which might not be appropriate for displaying in the Tree. Also, trees can have multiple elements at their highest level. To prevent the tree from displaying the root node, set the showRoot property to false. (The default showRoot value for the Tree control is true.) XMLList collections, however, do not have a single root, and you typically do not need to use the showRoot property.
  • When you use an XML, XMLList, or XMLListCollection object as the tree data provider, you must specify the labelField property, even if it is "label", if the field is an XML attribute. You must do this because you must use the @ sign to signify an attribute.

XMLListCollection objects

XMLListCollection objects provide collection functionality to an XMLList object and make available some of the XML manipulation methods of the native XMLList class, such as the attributes(), children(), and elements() methods. For details of the supported methods, see XMLListCollection in the Adobe Flex Language Reference.

The following simple example uses an XMLListCollection object as the data provider for a List control. It uses XMLListCollection methods to dynamically add items to and remove them from the data provider and its representation in the List control. The example uses a Tree control to represent a selection of shopping items and a List collection to represent a shopping list.

Users add items to the List control by selecting an item in a Tree control (which uses a static XML object as its data provider) and clicking a button. When the user clicks the button, the event listener uses the XMListCollection addItem() method to add the selected XML node to the XMLListCollection. Because the data provider is a collection, the List control is updated to show the new data.

Users remove items in a similar manner, by selecting an item in the list and clicking the Remove button. The event listener uses the XMListCollection removeItemAt() method to remove the item from the data provider and its representation in the List control.

<?xml version="1.0"?>
<!-- dpcontrols\XMLListCollectionWithList.mxml -->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">    
    <mx:Script>
        <![CDATA[
            import mx.collections.XMLListCollection;
            import mx.collections.ArrayCollection;
    
            /* An XML object with categorized produce. */
            [Bindable]
            public var myData:XML=
                <catalog>
                  <category name="Meat">
                      <product name="Buffalo"/>
                      <product name="T Bone Steak"/>
                      <product name="Whole Chicken"/>
                  </category>
                  <category name="Vegetables">
                      <product name="Broccoli"/>                         
                      <product name="Vine Ripened Tomatoes"/>
                      <product name="Yellow Peppers"/>
                  </category>
                  <category name="Fruit">
                      <product name="Bananas"/>
                      <product name="Grapes"/>
                      <product name="Strawberries"/>
                  </category>
              </catalog>;

            /* An XMLListCollection representing the data
               for the shopping List. */
           [Bindable]
           public var listDP:XMLListCollection = new XMLListCollection(new XMLList());
    
           /* Add the item selected in the Tree to the List XMLList data provider. */
           private function doTreeSelect():void {
               if (prodTree.selectedItem)
               listDP.addItem(prodTree.selectedItem.copy());
           }

           /* Remove the selected in the List from the XMLList data provider. */
           private function doListRemove():void {
               if (prodList.selectedItem)
                   listDP.removeItemAt(prodList.selectedIndex);
           }
        ]]>
    </mx:Script>
    
    <mx:HBox>
        <mx:Tree id="prodTree" dataProvider="{myData}" width="200"
            showRoot="false" labelField="@name"/>           
        <mx:VBox>
            <mx:Button id="treeSelect" label="Add to List"
                click="doTreeSelect()"/>
            <mx:Button id="listRemove" label="Remove from List"
                click="doListRemove()"/>
        </mx:VBox>         
        <mx:List id="prodList" dataProvider="{listDP}" width="200"
            labelField="@name"/>        
    </mx:HBox>
            
</mx:Application>

The executing SWF file for the previous example is shown below: