Building a Dynamic Tree in Flex

I have been playing around with Flex on and off based on my project requirements. Recently I ran into a problem; that is to build a Dynamic Tree which does lazy loading for performance. I looked around the internet to see how others have done it. I found number of examples that showed how to build a Tree in flex using a XML file. It works great if we have a static XML. But in my case I wanted to build a tree that dynamically builds itself based on a dynamic XML it gets from the server.

So, Here is how approached it.

Solution in Brief:

1. Servlet: I created a servlet that returned an XML file with the Nodes for requested branch.

2. Flex Component: Flex would call the servlet to get the XML data and render a tree  node based on that XML. As user would click on nodes Flex would  make calls to the server to get the XML data and append it to the Node as children.

Solution defined:

Here is a quick look at our finished Tree:

1. Servlet: Since I am focusing on the Flex stuff here, I am going to describe the servlet part briefly.

Here if we hit the servlet for the first time it should give us a XML that represents the First Nodes. In this case something like:

  <?xml version="1.0" encoding="UTF-8" ?> 
  <TREEDATA>
     <NODE ID="cars" LABEL="Cars" HASCHILDREN="true" /> 
  </TREEDATA>

As we hit the server with the id we get the children node xml for instance, here if we request the server with id=”cars” we get xml like:

  <?xml version="1.0" encoding="UTF-8" ?> 
  <TREEDATA>
     <NODE ID="acura" LABEL="Acura" HASCHILDREN="true" /> 
     <NODE ID="lexus" LABEL="Lexus" HASCHILDREN="true" /> 
     <NODE ID="saturn" LABEL="Saturn" HASCHILDREN="false" /> 
  </TREEDATA>
2. Flex Component: In Flex we will just have two components, 1. MX:Tree and 2. MX:HTTPService
Here is how Flex is set up:
<mx:HTTPService id="getCarInfo"
		url = "http://localhost:8080/Demo/getTreeData"
		result="treeDataResultHandler(event)"
		resultFormat="e4x" showBusyCursor = "true"/>
		
	<mx:Tree id="carTree" 
			dataProvider="{treeData}"
			dataDescriptor="{new MyCustomDescriptor()}"  
			labelField="@LABEL"
			itemOpening="loadChildren(event)"
			 width="434" x="10" y="10" height="266"/>


Here we set up a HTTPService to a servlet, we are getting a XML response so I am using e4x. Once the result is received it calls the function “treeDataResultHandler”.
private function treeDataResultHandler(event: ResultEvent):void{
				//Get the XML Data from the server
				var xmlResult:XML = new XML(event.result);
				var clickedNode:XML = nodeMap[event.token.message.messageId];

				//if we find that result was for a clicked node then we append the resulting node as children
				//else we know its first call then we just add all at the main level.
				if (clickedNode != null){
				  clickedNode.appendChild(xmlResult.NODE);
				  treeData.itemUpdated(clickedNode);
				}
				else {
					 for each(var nodeElement:XML in xmlResult.NODE)
				  	  	treeData.addItem(nodeElement);
				}
			}

I hope the comments in the code should explains itself.

One of the interesting thing to look at is the mx:Tree component. We define the dataDescriptor=”{new MyCustomDescriptor()}”
The Code is pasted below. We define the label for the node in the tree by doing: labelField=”@LABEL”. This makes the LABEL attribute form the XML to show up as a node Label of the tree.
The MAGIC happens in this code itemOpening=”loadChildren(event)” . When flex tries to open a children, this function will be called, lets see what it does below:

 

private function loadChildren(event:TreeEvent):void{
				var node:XML = XML(event.item);
				var params: Object = new Object();
				
				params.id = node.@ID;
				var a:AsyncToken = getCarInfo.send(params);
				nodeMap[a.message.messageId] = node;
			}
 

In this function, we basically get the ID attribute from the selected node then pass that as a parameter to the servlet using the “getCarInfo” service.
We add the messageId to the nodeMap so when the result handler is invoked later we know where to append the children nodes to.

Now to the custom Descriptor that extends the DefaultDataDescriptor

package {
	import mx.collections.ICollectionView;
	import mx.collections.XMLListCollection;
	import mx.controls.treeClasses.DefaultDataDescriptor;
	
	public class MyCustomDescriptor extends DefaultDataDescriptor
	{
		//We will do a lazy load here
		override public function hasChildren(node:Object, model:Object=null):Boolean
		{
			var rc:Boolean = false;
			if (node.@HASCHILDREN == "true")
			{
				rc = true;
			}
			return rc;
		}
		
		override public function isBranch(node:Object, model:Object=null):Boolean
		{
			var rc:Boolean = false;
			if (node.@HASCHILDREN == "true")
			{
				rc = true;
			}
			return rc;
		}
		
		override public function getChildren(node:Object, model:Object=null):ICollectionView
		{
		//We only render the NODE as node for the Tree so we will filter the XML to only give the NODE items	
		return new XMLListCollection(node.NODE);
		}
	}
}

Tree Flex component uses an object of the above class to determine how the tree is rendered. we override the functions that makes the tree to show the > indicator if it has any children or if it is a leaf node.
I also override the “getChildren” function and I filter only “NODE” xml element to as children this is because we can have other xml element that may not represent a tree node, in other implementation, i have node, that holds more data about the node, I didn’t want it to be rendered as a node.

So here is the simple implementation of a lazy loading dynamic Flex tree. Below is the complete Flex mxml Content.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" initialize="init()">
<mx:Script>
	<![CDATA[
		
		import mx.rpc.events.ResultEvent;
		import mx.rpc.AsyncToken;
		import mx.events.TreeEvent;
		import mx.collections.ArrayCollection;

		[Bindable]
		private var treeData:ArrayCollection = new ArrayCollection(); 
			
		private var nodeMap:Dictionary = new Dictionary();
		
		private function init():void{
			getCarInfo.send();
		}
				
		private function treeDataResultHandler(event: ResultEvent):void{
				//Get the XML Data from the server
				var xmlResult:XML = new XML(event.result);
				var clickedNode:XML = nodeMap[event.token.message.messageId];
				  	  
				//if we find that result was for a clicked node then we append the resulting node as children
				//else we know its first call then we just add all at the main level.
				if (clickedNode != null){
				  clickedNode.appendChild(xmlResult.NODE);
				  treeData.itemUpdated(clickedNode);
				}
				else {
					 for each(var nodeElement:XML in xmlResult.NODE)
				  	  	treeData.addItem(nodeElement);
				}
			}
		
		private function loadChildren(event:TreeEvent):void{
				var node:XML = XML(event.item);
				var params: Object = new Object();
				
				params.id = node.@ID;
				var a:AsyncToken = getCarInfo.send(params);
				nodeMap[a.message.messageId] = node;
			}
	]]>
</mx:Script>
	
	<mx:HTTPService id="getCarInfo"
		url = "http://localhost:8080/Demo/getTreeData"
		result="treeDataResultHandler(event)"
		resultFormat="e4x" showBusyCursor = "true"/>
		
	<mx:Tree id="carTree" 
			dataProvider="{treeData}"
			dataDescriptor="{new MyCustomDescriptor()}"  
			labelField="@LABEL"
			itemOpening="loadChildren(event)"
			 width="434" x="10" y="10" height="266"/>
</mx:Application>