Tree Control DataProviders
Lately I've been thinking of trees. Maybe it's because it is autumn in the northern hemisphere. So it seems an appropriate time to talk about Flex Tree controls. I'm going to publish a small series on the Flex 2.0 Tree component as there is a lot of ground to cover. In this series I'll present information on Tree dataProviders, itemRenderers, and drag-and-drop.
Data Providers
The Tree uses heirarchical data. That is, data that has levels: nodes that are branches and leaves. The DataGrid, by contrast, presents non-heirarchical data as does the List control.
XMLListCollection is an excellent dataProvider for the Tree and is the most commonly used class for this purpose. You can also use ArrayCollection and I'll go into more detail on that later in this article.
XML is a good because it is, by nature, heirarchical. Consider this simple XML:
[Bindable]
var company:XML =
<node>
<node label="Finance" dept="200">
<node label="John H" />
<node label="Sam K" />
</node>
<node label="Engineering" dept="300">
<node label="Erin M" />
<node label="Ann B" />
</node>
<node label="Operations" dept="400" isBranch="true" />
</node>
You can easily see the Tree structure in that XML. But also notice something else: all of the elements in the XML are <node /> elements. Every element is the same in that it has the name "node" and it has a label; branches and leaves as well. If you need to have a node that should be a branch but has no leaves, use isBranch="true" to tell the Tree the node should be treated like a branch.
The Tree likes this very much. You can give that XML to the Tree and you will see that structure without any fuss. But is this realistic to ask that all of the nodes in the data be the same? Consider this XML:
[Bindable]
var company:XML =
<list>
<department title="Finance" code="200">
<employee name="John H" />
<employee name="Sam K" />
</department>
<department title="Engineering" code="300">
<employee name="Erin M" />
<employee name="Ann B" />
</department>
<department title="Operations" code="400" isBranch="true" />
</list>
This XML is not uniform. If you give this to a Tree component you will get the XML dumped in the control because the Tree doesn't know what to do with it without extra instructions.
This XML can be displayed in the Tree with the help of a labelFunction.
private function treeLabel( item:Object ) : String
{
var node:XML = XML(item);
if( node.localName() == "department" )
return node.@title;
else
return node.@name;
}
The labelFunction simply returns the value of the name or title attribute depending on the type of node it is.
XMLListCollection
Earlier I mentioned XMLListCollection as a good dataProvider for the Tree, but I've been using XML so far. Here is the proper way to supply the Tree with data:
[Bindable]
var companyList:XMLListCollection = new XMLListCollection( company.department );
...
<mx:Tree dataProvider="{companyList}" labelFunction="treeLabel" />
An XMLListCollection is better as a dataProvider because the Tree can manipulate it (for editing, drag-and-drop, etc.) and because changes to the XMLListCollection are reflected in the Tree. That is, if I were to change the company XML object, you would not see the change in the Tree. If change the XMLListCollection, companyList, then not only would the underlying XML be changed, but so would the Tree.
Use XMLListCollection to supply the Tree with its data; you can change the collection and both the Tree and underlying XML will get changed, too.
If you cannot supply a very uniform XML structure to the Tree, use a labelFunction (or itemRenderer, but that's coming later) to supply the label for the display.
ArrayCollection
You can also use an ArrayCollection for the Tree. You can make an ArrayCollection heirarchical by embedding one ArrayCollection inside another:
[Bindable]
private var companyData:ArrayCollection = new ArrayCollection(
[ {type:"department", title:"Finance", children:new ArrayCollection(
[ {type:"employee", name:"John H"},
{type:"employee", name:"Sam K"} ] ) },
{type:"department", title:"Engineering", children: new ArrayCollection(
[ {type:"employee", name:"Erin M"},
{type:"employee", name:"Ann B"} ] ) },
{type:"department", title:"Operations", children: new ArrayCollection()}
] );
With this structure you'll notice that whenever a node has children, the name of the field is children - the Tree looks for this to identify branches from leaves.
You will also need a labelFunction with this data, too, so the Tree knows what to display on each node:
private function treeLabel( item:Object ) : String
{
if( item.type == "department" )
return item.title;
else
return item.name;
}
Adding Nodes
You make changes to the Tree through the dataProvider, not through the Tree control itself. When you want to add a node you add it to the dataProvider and the Tree will be changed automatically.
To add a node to the XMLListCollection you need to have a handle on the parent node. For example, to add a new department which is a top-level node, you can do that like this:
var newNode:XML = <department title="Administration" code="500" >
<employee name="Mark C" />
</department>;
companyList.addItem(newNode);
Here is how to add a new employee to the existing Operations department:
var newNode:XML = <employee name="Beth T" />;
var dept:XMLList =company.department.(@title == "Operations");
if( dept.length() > 0 ) {
dept[0].appendChild(newNode);
}
Once you identify a specific node and have its XML equivalent, you can use the appendChild() method.
To add a node to the ArrayCollection you just append it to whatever part of the structure requires the node. Here's how to add a new department (top-level) node:
var newNode:Object = {type:"department",title:"Administration"};
var newEmployee:Object = {type:"employee",name:"Mark C"};
newNode.children = new ArrayCollection( [newEmployee] );
companyData.addItem(newNode);
Here is how to add a new employee to the existing Operations department:
var newNode:Object = {type:"employee", name:"Beth T"};
for(var i:Number=0; i < companyData.length; i++) {
var item:Object = companyData.getItemAt(i);
if( item.title == "Operations" ) {
var children:ArrayCollection = item.children;
children.addItem(newNode);
companyData.itemUpdated(item);
empName.text = "";
break;
}
}
As you can see, using an ArrayCollection to add a node is a bit more complicated than using XML.
Removing Nodes
If you know you are removing a top-level node you can do that through the XMLListCollection's removeItemAt() method - but you have to know the index of the item. In the following example, all you know is the name, "Operations", so you have to loop through the nodes and when a match is found, remove the item.
var deptTitle:String = "Operations";
for(var i:Number=0; i < companyData.length; i++) {
var item:XML = XML(companyData.getItemAt(i));
if( item.@title == deptTitle ) {
companyData.removeItemAt(i);
break;
}
}
Removing the selected top-level node little easier:
var index:Number = tree.selectedIndex;
companyData.removeItemAt(index);
Here is how to remove a leaf node:
var node:XML = XML(tree.selectedItem);
if( node == null ) return;
if( node.localName() != "employee" ) return;
var children:XMLList = XMLList(node.parent()).children();
for(var i:Number=0; i < children.length(); i++) {
if( children[i].@name == node.@name ) {
delete children[i];
}
}
The same technique applies for ArrayCollection in that you have to search for the item, but once you find it you can use the ArrayCollection removeItemAt() method since the index is always valid for an ArrayCollection.
Here's how to remove a leaf node if the Tree's dataProvider is an ArrayCollection:
var node:Object = tree.selectedItem;
if( node == null ) return;
if( node.type != "employee" ) return;
var children:ArrayCollection = node.parent().children() as ArrayCollection;
for(var i:Number=0; i < children.length; i++) {
if( children[i].name == node.name ) {
children.removeItemAt(i);
break;
}
}
t is not possible to give a Collection a node of the Tree data and have the Collection remove it - you must hunt for it and delete it yourself.