I needed the tree sort for an AIR application. I built a sort function for XML objects and plugged an XML object into the Tree’s data provider. It works. I decided this function would be a good post by itself so I put together a Flex 2 example with only one mxml file which is provided at the bottom of the post.
Tree node labels are specified with the property “labelField” and normally you would want to sort on these labels.
Actually, you might want to first sort on the node type and then the labels, but I haven’t gotten far enough in my application to need that yet. I know I’ll need it in the future, so stay tuned.
I set the Tree’s “labelField” property to one of the attribute names of the elements in the XML object. The example uses the “name” attribute preceded by the “@” character which is the E4X designator for attributes.
<mx:Tree dataProvider="{mvXml}" labelField="@name"/>The data provider variable “mvXml” is defined as:
var mvXml:XML = <Country name= "USA"> <State name="Texas"> <City name="Corpus Christi"/> <City name="austin"/> <City name="San Antonio"/> </State> <State name="Michigan"> <City name="Ypsilanti"/> <City name="lansing"/> <City name="Ann Arbor"/> <City name="Kalamazoo"/> <City name="Allen Park"/> </State> </Country>Now the fine print
It is not unusual to have alphanumeric, numeric, currency and date nodes in a single tree. Of course all of the tree node labels can be sorted as simple text, but if you have numbers in some of the labels then 10 would come before 2. It is important to understand we are actually sorting the Tree’s data provider, an XML object, instead of the Tree nodes directly. My function sorts on only one attribute and it must be in every element of the XML object.
Lower expectations and raise false hope
Still a lot of trees will look fine if the labels are all sorted the same way. Also, I put the labels in an Array object to sort and allow you to pass in Array sorting arguments. So you may do descending, numeric, case insensitive and custom sorts.
If you are really desperate to sort on multiple types of labels, you could write a custom sort that would analyze the field and determine if it were alphanumeric, numeric, currency or a date before sorting.
There is another way to handle unusual situations. You could add an XML attribute to be used for sorting and another attribute for the label. The sorting attribute would never be seen, but would be used by my sorting function. This way you can sort numbers as alphanumeric by right aligning them and dates by first converting them to numbers.
Or you could wait because I know I will have to deal with this soon.
The example...
I have buttons for 5 different test sorts for the tree and the button click events are as follows:
Button "Sort Ascending" Click Event:
sortXml( mvXml, "name" ); expandAllTreeNodes( Tree1 );Button "Sort Case Insensitive" Click Event:
sortXml( mvXml, "name", Array.CASEINSENSITIVE ); expandAllTreeNodes( Tree1 );Button "Sort Descending" Click Event:
sortXml( mvXml, "name", Array.CASEINSENSITIVE | Array.DESCENDING ); expandAllTreeNodes( Tree1 );Button "Sort Reverse" Click Event:
sortXml( mvXml, "name", sortReverseText ); expandAllTreeNodes( Tree1 );Button "Sort Reverse Descending" Click Event:
sortXml( mvXml, "name", sortReverseText, Array.DESCENDING ); expandAllTreeNodes( Tree1 );You can pass the Array sort options in the optional 3rd and 4th function arguments. The last 2 click events use the custom sort function, "sortReverseText", which can be seen in the complete listing at the bottom of the post.
Note that it is necessary to expand the trees after each sort. Of course, this only applies to the example. In a real application you would want to restore the expanded states to their original settings. This presents a whole different problem.
The "name" argument refers to the name of the attribute to be sorted in each element. This attribute must be in every element. Otherwise the element, and descendents, will be lost after a sort.
Actually this could be a feature. Our product not only sorts, it automatically permanently filters out elements that don't have the required attribute.
The XML Sort Function
Here is a breakdown of the sort function, "sortXml", which is a recursive static function.
1 public static function sortXml 2 ( avXml :XML, 3 avAttributeName :String, 4 avArraySortArgument0 :* = 0, 5 avArraySortArgument1 :* = 0 ) 6 :void 7 { 8 var lvChildrenCount:int = avXml.children().length(); 9 10 if( lvChildrenCount == 0 ) 11 return; 12 13 if( lvChildrenCount > 1 ) 14 { 15 var lvAttributeValue:String; 16 17 var lvArray:Array = new Array(); 18 avXml.children(). 19 ( 20 lvAttributeValue = attribute( avAttributeName ), 21 lvArray.indexOf( lvAttributeValue ) == -1 22 ? lvArray.push( lvAttributeValue ) : null 23 ); 24 25 lvArray.sort 26 ( 27 avArraySortArgument0, 28 avArraySortArgument1 29 ); 30 31 var lvXmlList:XMLList = new XMLList(); 32 for each( lvAttributeValue in lvArray ) 33 { 34 lvXmlList += avXml.children(). 35 ( 36 attribute( avAttributeName ) 37 == 38 lvAttributeValue 39 ); 40 } // for each 41 42 avXml.setChildren( lvXmlList ); 43 44 } // if 45 46 for each( var lvXmlChild:XML in avXml.children() ) 47 { 48 sortXml 49 ( 50 lvXmlChild, 51 avAttributeName, 52 avArraySortArgument0, 53 avArraySortArgument1 54 ); 55 } // for each 56 57 } // sortXmlLines 17 - 29
I know you won't believe me, but I did not think I would ever want to use the exact code I used in the last example of my preceding post. The purpose is to get all unique attribute values into an Array and sort them.
Lines 31 - 40
The purpose of the code is to build a new XMLList object to contain all of the child elements in the sorted order. This code loops through each of the unique attribute values in the sorted Array, but the lines 34 thru 39 are a little more complicated. These lines are one statement using E4X syntax. Lines 36 thru 38 act a filter. It returns all child elements where the attribute value is equal to the loop's current sorted attribute value. It can return more than one element in the case where more than one element has the same attribute value. The resulting XmlList variable, "lvXmlList", ends up containing the same child elements of the XML input argument "avXml".
Line 42
We are fortunate the XML class has the "setChildren" function. With this function, the code replaces the existing children with the same, but sorted, elements in the XMLList object.
Lines 46 - 55
This is just the standard recursive stuff that calls this same function on all of the child elements so that their children, and so on, are sorted. I tried to build an E4X statement to do the same thing. Maybe a fresher mind, mine or yours can come up the code.
Complete Source Code for Example:
TreeSortExample.mxml
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="onCreationComplete()" > <mx:VBox height="100%" width="100%" x="50" y="5" > <mx:Panel width="90%" height="50%" horizontalAlign="center" title="Jim Freer's E4X Sorting for Trees" > <mx:HBox height="100%" width="100%" > <mx:VBox> <mx:Button label="Sort Ascending" width="200" click="onClickButtonSortTree1Ascending()" /> <mx:Button label="Sort Case Insensitive" width="200" click="onClickButtonSortTree1CaseInsensitive()" /> <mx:Button label="Sort Descending" width="200" click="onClickButtonSortTree1Descending()" /> <mx:Button label="Sort Reverse" width="200" click="onClickButtonSortTree1Reverse()" /> <mx:Button label="Sort Reverse Descending" width="200" click="onClickButtonSortTree1ReverseDescending()" /> </mx:VBox> <mx:Tree id="Tree1" height="100%" width="100%" dataProvider="{mvXml}" labelField="@name" /> </mx:HBox> </mx:Panel> <mx:Label text="*State capitals are in lowercase :) to demonstrate case insensitive sorting." /> </mx:VBox> <mx:Script> <![CDATA[ // --------------------------------------------------------------------- // Private Member Variables // --------------------------------------------------------------------- [Bindable] private var mvXml:XML = <Country name= "USA"> <State name="Texas"> <City name="Corpus Christi"/> <City name="austin"/> <City name="San Antonio"/> </State> <State name="Michigan"> <City name="Ypsilanti"/> <City name="lansing"/> <City name="Ann Arbor"/> <City name="Kalamazoo"/> <City name="Allen Park"/> </State> </Country>; // --------------------------------------------------------------------- // expandAllTreeNodes // --------------------------------------------------------------------- private function expandAllTreeNodes ( avTree :Tree ) :void { avTree.selectedIndex = 0; avTree.expandChildrenOf( avTree.selectedItem, true ); } // expandAllTreeNodes // --------------------------------------------------------------------- // onClickButtonSortTree1Ascending // --------------------------------------------------------------------- private function onClickButtonSortTree1Ascending () :void { sortXml( mvXml, "name" ); expandAllTreeNodes( Tree1 ); } // onClickButtonSortTree1Ascending // --------------------------------------------------------------------- // onClickButtonSortTree1Descending // --------------------------------------------------------------------- private function onClickButtonSortTree1Descending () :void { sortXml ( mvXml, "name", Array.CASEINSENSITIVE | Array.DESCENDING ); expandAllTreeNodes( Tree1 ); } // onClickButtonSortTree1Descending // --------------------------------------------------------------------- // onClickButtonSortTree1CaseInsensitive // --------------------------------------------------------------------- private function onClickButtonSortTree1CaseInsensitive () :void { sortXml( mvXml, "name", Array.CASEINSENSITIVE ); expandAllTreeNodes( Tree1 ); } // onClickButtonSortTree1CaseInsensitive // --------------------------------------------------------------------- // onClickButtonSortTree1Reverse // --------------------------------------------------------------------- private function onClickButtonSortTree1Reverse () :void { sortXml( mvXml, "name", sortReverseText ); expandAllTreeNodes( Tree1 ); } // onClickButtonSortTree1Reverse // --------------------------------------------------------------------- // onClickButtonSortTree1ReverseDescending // --------------------------------------------------------------------- private function onClickButtonSortTree1ReverseDescending () :void { sortXml( mvXml, "name", sortReverseText, Array.DESCENDING ); expandAllTreeNodes( Tree1 ); } // onClickButtonSortTree1ReverseDescending // --------------------------------------------------------------------- // onCreationComplete // --------------------------------------------------------------------- private function onCreationComplete () :void { expandAllTreeNodes( Tree1 ); } // onCreationComplete // --------------------------------------------------------------------- // sortReverseText // // Reverses the strings before comparing. // For example, Kalamazoo is oozamalaK. // A lame example, but shows how to use a custom sort. // --------------------------------------------------------------------- private function sortReverseText ( avStringA :String, avStringB :String ) :Number { var lvArray:Array; lvArray = avStringA.split( "" ); lvArray = lvArray.reverse() var lvStringA:String = lvArray.join( "" ); lvArray = avStringB.split( "" ); lvArray = lvArray.reverse() var lvStringB:String = lvArray.join( "" ); if( lvStringA > lvStringB ) { return 1; } // if else if( lvStringA < lvStringB ) { return -1; } // else if else { return 0; } // else } // sortReverseText // --------------------------------------------------------------------- // sortXml // // Warning: All elements in avXml must have an attribute with the name // in avAttributeName or the element will be deleted along with any // descendants. // --------------------------------------------------------------------- public static function sortXml ( avXml :XML, avAttributeName :String, avArraySortArgument0 :* = 0, avArraySortArgument1 :* = 0 ) :void { var lvChildrenCount:int = avXml.children().length(); if( lvChildrenCount == 0 ) return; if( lvChildrenCount > 1 ) { var lvAttributeValue:String; var lvArray:Array = new Array(); avXml.children(). ( lvAttributeValue = attribute( avAttributeName ), lvArray.indexOf( lvAttributeValue ) == -1 ? lvArray.push( lvAttributeValue ) : null ); lvArray.sort( avArraySortArgument0, avArraySortArgument1 ) var lvXmlList:XMLList = new XMLList(); for each( lvAttributeValue in lvArray ) { lvXmlList += avXml.children(). ( attribute( avAttributeName ) == lvAttributeValue ); } // for each avXml.setChildren( lvXmlList ); } // if for each( var lvXmlChild:XML in avXml.children() ) { sortXml ( lvXmlChild, avAttributeName, avArraySortArgument0, avArraySortArgument1 ); } // for each } // sortXml ]]> </mx:Script> </mx:Application>
One more thing, I am looking for work (this time for money). Any advice would be appreciated.
1 comment:
Thanks for the example. I found it very helpful. The E4x lines are still confusing to me however.
Post a Comment