Did a little bit of vanilla Flex work recently, and I needed a Tree component to display an object hierarchy. Everyone, by now, hopefully knows that mx:DataGrid
and mx:Tree
are two of the crappiest, bug ridden, worst performing components from Flex 3. And, everyone by now, has left the buggy world of Flex 3 behind and entered the world Flex 4 and the vastly improved Spark components. With the arrival of Flex 4.5 this summer, Adobe finally gave us a rewritten Spark-based DataGrid
. Alas, no updated Tree
yet, so I had to write my own. So once again, I turned to the trusty combo of List
plus custom ItemRenderer
to make pure-Spark custom TreeList
component that doesn’t suck.
Alex Harui is the guru of turning a Flex 4 Spark List
into a look-alike for the old Flex 3 component using skins and a custom ItemRenderer
. Alex has used List
+ ItemRenderer
to make a DataGrid, DateField, ColorPicker, Menu + MenuBar, and even an XML-based TreeList. Unfortunately, Alex’s TreeList
assumes incoming XML, and I needed a TreeList
that could display a simple object hierarchy (root node with children, and those children have children, etc.). Since I couldn’t find exactly what I wanted, I decided to build it myself.
Flattener
The key step to getting a hierarchy of objects to display as a list is: flatten the list, duh! Or at least flatten the part of the tree you wish to display. So, I built a simple adapter class that turns an object hierarchy into an ArrayList
that can be given directly to a List
‘s dataProvider
.
Here’s the actual flattener, but with just the comments not the code:
public class MyObjFlattenedList extends ArrayList { //the root object private var _root:MyObj; //list of open nodes private var _openList:Array; public function MyObjFlattenedList(root:MyObj) { _root = root; _openList = []; reset(); ... } public function reset(openList:Array = null):void { //init the flattened list, starting at root _openList = (openList == null ? [] : openList); var a:Array = []; addItemToList(_root, a); this.source = a; } private function addItemToList(obj:MyObj, a:Array):void { //recursively walk obj and all of its "open" children to build //a flattened list that is returned in array a } public function isItemOpen(obj:MyObj):Boolean { //true if obj has children and is "open" } public function openItem(obj:MyObj):void { //add all of obj's children (if any) to the list } public function closeItem(obj:MyObj):void { //remove all of obj's children (if any) from the list } ... }
There’s really not much to it. When instantiated with a root object, the object hierarchy is walked recursively to build a flattened list of all the open nodes. Once the initial list is built, openItem()
can be called to open a node, and add all its children to the list. Alternately, closeItem()
can be called to close a node, and remove all its children from the list.
Design
I used some basic styling and skinning, but the ItemRenderer
does the majority of the work. Here’s the abbreviated version of MyObjRenderer.mxml
with all the boring stuff edited out:
<?xml version="1.0" encoding="utf-8"?> <s:ItemRenderer xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" width="100%" autoDrawBackground="false"> <fx:Script> <![CDATA[ [Bindable] private var _obj:MyObj; [Bindable] private var _hasChildren:Boolean = false; private var _list:MyObjFlattenedList; override public function set data(val:Object):void { super.data = val; if (val != null) { _obj = val as MyObj; var ownerList:List = owner as List; _list = ownerList.dataProvider as MyObjFlattenedList; btn.selected = _list.isItemOpen(_obj); _hasChildren = (_obj.children != null && _obj.children.length > 0); } } private function toggleHandler():void { if (btn.selected) { _list.openItem(_obj); } else { _list.closeItem(_obj); } } ]]> </fx:Script> <s:states> <s:State name="normal" /> <s:State name="hovered" /> <s:State name="selected" /> </s:states> <s:Rect ...> <s:HGroup ...> <s:ToggleButton id="btn" click="toggleHandler()" visible="{_hasChildren}" ... /> <s:Group id="dot" visible="{!_hasChildren}" ... /> <s:Label ... /> </s:HGroup> </s:ItemRenderer>
Each rendered item has a background Rect
and a Label
. But most importantly, each row has either a ToggleButton (if the object has children) or some non-interactive visuals (if the object doesn’t have children it just gets a dot). The toggle button is the key interactive element used to open or close the node, everything else is part of the visual gravy added to make the list look good.
Focusing on the functional stuff, if you look in the script block, you’ll see two functions: a data()
setter and a toggleHandler()
to handle toggle button clicks. As expected, clicking the toggle button calls openItem()
or closeItem()
on the flattener adapter which adds or removes children from the flattened list, respectively. The setter mostly sets up the local variables, but it notably computes if the object has children or not, and also sets the initial state of the toggle button.
Conclusion
With just a little effort, we can have a nice usable Spark TreeList component that looks decent. More importantly, we have total control and can make the TreeList look like anything our designer can throw at us. As is always the case, the combo of List
+ ItemRenderer
proves to be awesome. I tried to cover all the interesting pieces, but for the details, you’ll need to check out the source code.
Here’s the finished product (view source enabled):
Use it and enjoy.