Filtering a Flex Tree using an ArrayCollection

Flex Trees can be difficult to filter because of their hierarchical nature. One possible way to do it is to use an ArraryCollection as your dataProvider, with each node having a “children” property that is also an ArrayCollection.

The tricky part in filtering the Tree is making sure the children are filtered in addition to the root nodes.  So, in the example below, I have created a function that will loop through each node and its children recursively.

It is important to note that the filtering takes place from the bottom up, meaning that a node’s children are always filtered before the node itself.  This is important, because normally a filter removes everything that does not match a certain criteria.  In our case, to remain in the collection, a node must match a certain criteria OR have children that do.

<?xml version=”1.0″ encoding=”utf-8″?>
<mx:Application xmlns:mx=”http://www.adobe.com/2006/mxml

layout=”absolute>


<mx:Script>

<![CDATA[
import vo.Person;
import mx.collections.ArrayCollection;

[Bindable]
private var people:ArrayCollection = new ArrayCollection([

new Person("Grandma Susan", new ArrayCollection([

new Person("John", new ArrayCollection([

new Person("Timmy"),
new Person("Sammy"),
new Person("Alan")

])),
new Person(“Tiffany“, new ArrayCollection([

new Person("Billy"),
new Person("Adam"),
new Person("Graham"),
new Person("Vennesa")

])),
new Person(“Michael“, new ArrayCollection([

new Person("Jannette"),
new Person("Alan", new ArrayCollection([

new Person("Alice"),
new Person("Jane")

]))

])),
new Person(“Peter“),

new Person(“Cindy“, new ArrayCollection([

new Person("Paul"),
new Person("David"),
new Person("Joseph"),
new Person("Cameron"),
new Person("Debra"),
new Person("Polly")

]))

]))

]);

private function refreshData():void{

//reset the root node to its original unfiltered data
people[0].children = new ArrayCollection(people[0].children.source);
//start the recursion at the root node
refreshRecursiveChildren(people.source[0]);
//update the Tree after the data has been filtered
personsTree.invalidateList();

}

private function refreshRecursiveChildren(person:Person):void{

if(person.children){

//loop through each child and filter its children
for each(var _person:Person in person.children.source){

refreshRecursiveChildren(_person);

}
//reset each “children” ArrayCollection to its original unfiltered data
person.children = new ArrayCollection(person.children.source);
//set the filterfunction for the newly updated node
person.children.filterFunction = filterData;
//run the fitlerFunction
person.children.refresh();

}

}

public function filterData(item:Object):Boolean{

//get the string to filter the nodes by
var searchString:String = iNameFilter.text;
//if string is found in node return true.
//since the recursive filtering takes place from bottom up, if
//a collection still has children after filtering, also return true

if(searchString.length == 0
|| item.name.toLowerCase().indexOf(searchString.toLowerCase()) >= 0)
return true;

else if(item.children != null && item.children.length > 0)

return true;

return false;

}
]]>

</mx:Script>

<mx:VBox width=”100%” height=”100%

paddingTop=”10
paddingBottom=”10
paddingLeft=”5
paddingRight=”5>
<mx:Tree id=”personsTree

dataProvider=”{people}
labelField=”name
width=”100%
height=”100%/>

<mx:HBox>

<mx:Label text=”Filter the Tree:/>
<mx:TextInput id=”iNameFilter” change=”refreshData()/>

</mx:HBox>

</mx:VBox>

</mx:Application>

You may right-click and select “View Source”
to view the full source code for the following example.


Tags: , , ,

53 Responses to “Filtering a Flex Tree using an ArrayCollection”

  1. Abdul Jaleel Says:

    Thank you very much… I am a newbie to flex.. it is very helpful to me. i have been searching for this solution. thnkx

  2. Manoj K Says:

    This is a nice approach to filtering a tree, however it seems to be slightly buggy…
    steps to reproduce:
    1. Expand “Grandma Susan” node
    2. Expand the “John” node
    3. Search for “alan”
    4. Collapse the “John” node

    TypeError: Error #1010: A term is undefined and has no properties.
    at mx.controls::Tree/expandItem()
    at mx.controls::Tree/http://www.adobe.com/2006/flex/mx/internal::expandItemHandler()
    at flash.events::EventDispatcher/dispatchEventFunction()
    at flash.events::EventDispatcher/dispatchEvent()
    at mx.core::UIComponent/dispatchEvent()
    at mx.controls::Tree/http://www.adobe.com/2006/flex/mx/internal::dispatchTreeEvent()
    at mx.controls.treeClasses::TreeItemRenderer/disclosureMouseDownHandler()

    any ideas how this can be fixed?

  3. Kalen Gibbons Says:

    Good catch Manoj,
    This appears to be a bug in the framework. When the arrayCollection is being filtered, the empty nodes are not being removed from the listItems array of the ListBase class. So when the branch tries to collapse, it’s looking for nodes that no longer exist. I haven’t yet determined a way to force an update of the listItems array but I’m actively looking for a solution. I will let you know what I find.

    Thanks again,
    Kalen Gibbons

  4. Romu Says:

    I’m interested in that fix as well, if you find anything!

  5. Sham Bhangal Says:

    Another (less efficient but has no bugs) is to recurse through children in the filterfunction… if the item gives false, then look through its immediate children (i.e to a depth of 1), and return true if filterfunction(child[n]) would return true. Return false if none do. You’re actually applying the fiilterfunction recursively that way (when you apply filterfunction to the children, that will recurse for every child.child etc)….

    filterfunction(obj:Object):Boolean {

    I did it that way and it worked when using an AdvancedDataGrid instead of a tree, but the process is the same.

  6. Sham Bhangal Says:

    Another (less efficient but has no bugs) is to recurse through children in the filterfunction… if the item gives false, then look through its immediate children (i.e to a depth of 1), and return true if filterfunction(child[n]) would return true. Return false if none do. You’re actually applying the fiilterfunction recursively that way (when you apply filterfunction to the children, that will recurse for every child.child etc)….

    filterfunction(obj:Object):Boolean {

    if (obj will return false)

    I did it that way and it worked when using an AdvancedDataGrid instead of a tree, but the process is the same.

  7. Sham Bhangal Says:

    whoops finger trouble on the last post…..

    Another (less efficient but has no bugs) is to recurse through children in the filterfunction… if the item gives false, then look through its immediate children (i.e to a depth of 1), and return true if filterfunction(child[n]) would return true. Return false if none do. You’re actually applying the fiilterfunction recursively that way (when you apply filterfunction to the children, that will recurse for every child.child etc)….

    pseudocode…

    function filterfunction(obj:Object):Boolean {
    check obj
    if (obj will return false and this is not the bottom level) {
    return checkChildren(obj);
    }
    }

    function checkChildren (obj) {
    loop (for every child of obj do filterfuncton(obj.child), if it returns true, return true)
    return false
    }

  8. loop Says:

    have you try to do the same thing with a xmlCollection ?

  9. Kalen Gibbons Says:

    I have actually been considering writing a post about using an xmlCollection to filter the Tree. I guess I’ll have to do that, but with my schedule it may take a week or two. So hang in there.

  10. icemaker Says:

    hello,

    To resolve the ‘listitems’ bug, try to put this just after calling personsTree.invalidateList() :

    personsTree.expandItem(people[0],false);
    personsTree.expandItem(people[0],true);

  11. Kalen Gibbons Says:

    Hi icemaker,

    Thanks for the suggestion. I had tried this in the past and it DOES fix the problem, however, it’s a bit of a hack. I’m trying to figure out what happens in the expandItem() method that corrects the problem (i.e. updates the listItems array).

    I’ve been slacking lately but I’ll dig back into this. I also want to write a follow up post on sorting a Tree using an xmlCollection but I’d like to get this figured out before I do so.

    Thanks again.

  12. RinKaMeAri Says:

    One more approach is here : http://www.davidarno.org/2009/04/01/how-to-filter-all-nodes-of-a-flex-tree-component/...

  13. Kaushal Says:

    I want to filter a datagrid by multiple criteria. I would like to have several types of components (checkbox, slider, textfield, etc) to filter, each one

    set to filter a particular datafield (asset_status, serial, tag….see data structure) in the arraycollection.

    The filterfunction does not work. When I click type in the textbox or check any checkboxes, it does not retreive the intended results. My link is below.

    http://s256908546.onlinehome.us/datagrid/am_post.html

    Appreciate any help.

  14. Kalen Gibbons Says:

    Hi Kaushal,
    I can see that there are some problems with the way your data filters are set up. I will take a look when I get a chance and will get back to you.

  15. Kalen Gibbons Says:

    Hi Kaushal,
    I wasn’t quite sure what you wanted to do with some of the filters but I gave it my best try anyways. Here is the new mxml file: http://kalengibbons.com/assets/pages/am_post.mxml

    I hope it helps.

    Thanks,
    Kalen Gibobns

  16. Kaushal Says:

    It works! Thank you so much. I looked at your code. I get all the filter logic except for one thing. In the following line, why do we need ‘item.Disconnected_Asset’ in addition to ‘tem.Disconnected_Asset == “Always Connected”‘?

    (chkAmount2.selected && (item.Disconnected_Asset && item.Disconnected_Asset == “Always Connected”))

    Thanks again!

  17. Kalen Gibbons Says:

    You don’t really need it, I must have left it in the code by accident. Usually when comparing strings I like to use toUpperCase() or toLowerCase() to prevent case mismatches. So the line originally read (item.Disconnected_Asset && item.Disconnected_Asset.toLowerCase() == “always connected”) and the first part of the statement was necessary because if the value of Disconnected_Assets was null, it would throw an error when it got to toLowerCase().

    Good luck with your application Kaushal. :-)

  18. Mihran Says:

    Hello Kalen, thanks for the post, I am having difficulty removing parent nodes that don’t match a search criteria. e.g. for query test I would want to remove Grandma Susan Node. My filtering works properly and the filtered array collection does not contain unwanted parent nodes, yet the tree control shows them. Any advice is much appreciated.

  19. Mihran Says:

    Actually, I just realized that I was not setting the filter on the main Array collection. so that did it. I am all set

  20. Bala Says:

    It’s Really hepled a lot

  21. Zanpher Says:

    Umm .. please publish the rest of the code to make this work. it would be wonderful …. for example .. the vo.person object …

  22. Kalen Gibbons Says:

    Hi Zanpher,
    If you right-click on the sample application and select “View Source” you’ll be able to see all the code, and you’ll be able download an all-inclusive zip file as well. :-)

  23. Mitu Zaman Says:

    Hi,
    I have created a tree with a custom treeitemrenderer where I have added a textinput box next to each label. My data provider is an ArrayCollection. When I enter value in the input box, I can see the value fine as I expand or collapse the tree using the disclousure arrow. There is a strange issue when I try to expand and collapse tree from the mxml page with a button that expand and collapse tree using the following code. The problem is as I expand or collapse tree, the values I entered in the input boxes appear to be moving. For example, I have entered 15 in input box 1. When I expand and collapse tree, 15 moves to another input box, and moves to another, and then it moves back to where it was entered. I am still learning Flex. I am not sure what is happening, it is something to do with arraycollection. Any help would be greatly appreciated. Thanks.

    private function tree_expandItem(evt:MouseEvent):void {
    if (tree.selectedItem && tree.dataDescriptor.isBranch(tree.selectedItem)) {
    tree.expandItem(tree.selectedItem, checkBox.selected);
    }
    }

    private function tree_expandChildrenOf(evt:MouseEvent):void {
    if (tree.selectedItem && tree.dataDescriptor.isBranch(tree.selectedItem)) {
    tree.expandChildrenOf(tree.selectedItem, checkBox.selected);
    }
    }

  24. Kalen Gibbons Says:

    Hi Mitu,
    From the symptoms you’ve described I think your problem is due to how itemRenderers recycle their children. The following article should give you some perspective on what is happening and how to solve it. Good luck, and let me know if you still have any unresolved questions/issues.

    http://www.adobe.com/devnet/flex/articles/itemrenderers_pt1.html

  25. Mitu Zaman Says:

    Hi Kalen,
    Thanks for the article. But I still couldn’t figure out. I feel like pulling my hair out. Can I send you my code? Its a very small prog.

  26. Kalen Gibbons Says:

    Sure, send your code to kalen[dot]gibbons[at]gmail[dot]com and I’ll take a look at it.

  27. Mitu Zaman Says:

    I sent the code in your gmail account. You should get an email from rafiq16@yahoo.com
    Thanks.

  28. Mitu Zaman Says:

    here is the renderer code:

    package
    {
    import mx.collections.*;
    import mx.controls.Label;
    import mx.controls.TextInput;
    import mx.controls.listClasses.*;
    import mx.controls.treeClasses.*;

    [Bindable]
    public class TestSetupTreeItemRenderer extends TreeItemRenderer {

    [Bindable]
    public var _numQuestion:TextInput;
    private var _numOfQuestionText:Label;
    private var _listData:TreeListData;
    [Bindable]
    public var tItem:TestSetupTreeItem;

    public function TestSetupTreeItemRenderer():void {
    super();
    mouseEnabled = false;
    }

    override protected function createChildren():void {
    super.createChildren();
    _numQuestion = new TextInput();
    _numQuestion.text = “0″; //default

    _numQuestion.width = 45;
    _numQuestion.height = 20;
    _numQuestion.restrict = “0123456789″;
    addChild(_numQuestion);

    _numOfQuestionText = new Label();
    _numOfQuestionText.width = 60;
    _numOfQuestionText.height = 22;
    addChild(_numOfQuestionText);
    }

    override public function set data(value:Object):void {
    if(value == data)
    return;
    tItem= value as TestSetupTreeItem;
    super.data = value;
    validateNow();
    }

    override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
    super.updateDisplayList(unscaledWidth,unscaledHeight);
    if(super.data){
    tItem = super.data as TestSetupTreeItem;
    this.label.width = 150;
    this.label.truncateToFit(null);
    this.label.multiline = true;
    this.label.wordWrap = true;

    var tld:TreeListData = TreeListData(super.listData);

    this._numOfQuestionText.text = “of ” + tItem.totalQuestionCount;
    this._numOfQuestionText.x = 300;
    this._numOfQuestionText.y = super.label.y;

    this._numQuestion.x = 200;
    this._numQuestion.y = super.label.y;
    this._numQuestion.editable = true;

    tItem.desiredQuestionCount = this._numQuestion.text;
    }
    }
    }
    }

  29. Kalen Gibbons Says:

    Hi Mitu,
    I sent you the updated files that should now work properly. I see this problem a lot and I’ve been meaning to blog about it, but just haven’t gotten around to it yet. The problem is how Flex recycles itemRenderers. In a nutshell this means that the same instance of an item renderer will be reused when a list is scrolled (or changed) instead of creating a new instance. This dramatically improves performance but can cause display issues. If you are trying to store a local variable in an itemRenderer, that variable will be carried along with the itemRenderer when it’s reused. So what you need to do is store information specific to an itemRenderer outside the renderer itself. The most common place to do this is in the data that the renderer references.

    For the code that you sent, Mitu, I simply added an event listener for changes to the textInput and then I store that value inside your data variable.

    _numQuestion.addEventListener(KeyboardEvent.KEY_UP, changeHandler);

    private function changeHandler(event:KeyboardEvent):void {
    tItem.desiredQuestionCount = event.currentTarget.text;
    }

    and then, to make sure that the correct data is displayed in the textInput I reset that text value whenever the itemRenderer is re-renderered.

    override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {

    this._numQuestion.text = tItem.desiredQuestionCount;

    }

    Please let me know if you have any further questions.

  30. Mitu Zaman Says:

    Hi Kalen,
    This solves the issue. Thank you very very much! This is an owesome site and I will be a frequent user….

    Mitu

  31. Prashant Says:

    Thank you sir. The example you provided here has proven a great help for me.

  32. Nguyenhuutinh Says:

    Hi.
    It’s an good example.but can you help me read and external xml file by using httpservice .My Xml Structure like person above ,consist parent name,and its children. I dont know how to do .please help me .
    Thanks

  33. Jeff Says:

    Did you ever figure out that bug from the 2nd response?

  34. Kalen Gibbons Says:

    Jeff,
    I haven’t figured out a solution to the bug mentioned, although I haven’t tested this code with any of the recent SDK updates. There is a comment above by Icemaker that provides a work-around for this issue. Until a better solution is found, that should do the trick.

  35. yuege Says:

    greate works!

  36. Pappu Says:

    Hi,

    I am trying the same example above by pulling data of the Person class from the server side using a Remote Object but it doesnt work.

    Can some one help me how to solve this problem.

  37. Kalen Gibbons Says:

    Pappu,
    It’s hard to say what the problem is without more information. I’m guessing that the problem is the format of your data. Make sure that each level of your hierarchy is an ArrayCollection so that you can filter it; arrays or anything else won’t work.

  38. prudvi Says:

    i need some regarding tree component.i want to display the hard drive folders in the explorer form .can any body give me the answer

  39. Kalen Gibbons Says:

    Prudvi,

    I need more information to help. Is this an AIR project? Where is your data coming from and in what format? If you could post it here that would help.

  40. Khaled Says:

    thanks for the example, very useful, I’m trying though to get the root node to disappear with no luck , i tried to change the following

    private function refreshData():void{

    people[0].children = new ArrayCollection(people[0].children.source);

    var root:Person = new Persion();
    root.children = new ArrayCollection();
    root.children.addItem(people[0]);

    refreshRecursiveChildren(root);

    personsTree.invalidateList();

    }

    I can see that Grandma Susan is passed now to filterData and return ‘false’ still it shows in the tree !

  41. Kalen Gibbons Says:

    How is the new root node being applied to the Tree? Are you reassigning the dataProvider on the tree or adding the root into the existing “people” ArrayCollection?

  42. Rene Roberto Ramirez Szabo Says:

    Thank you, very useful post.

  43. Simon Says:

    Nice approach, thanks for sharing! However, I would not recommend to use it for complex projects, because it chages the original data when filtering, so if you want to display it in another view component, you’ll have a problem. There’s also a problem, if you need EventListeners on your “children”-ArrayCollection, because they will be gone after calling “refreshRecursiveChildren”, because the function resets each “children” ArrayCollection to its original unfiltered data by creating a new ArrayCollection instance.

    If you ran into the same problem, here’s another really nice article on “Filtering Hierachical Data in Flex”:
    http://www.richardleggett.co.uk/blog/index.php/2009/05/11/filtering-hierachical-data-in-flex-itree

  44. Basil Says:

    Good morning and happy holidays,
    Have u ever had the chance to convert this example for a xmlCollection?
    I’ve been trying to make it work with data loaded from an XML but with limited success.
    Thanks

  45. Kalen Gibbons Says:

    Hi Basil,
    I’m sorry, but no, I never got around to creating an example using an xmlCollection. The code shown in this post was more of a proof-of-concept than anything and several commenter have pointed out flaws with this approach. I would recommend looking into using the ITreeDataDescriptor class for doing this type of filtering instead.

    Simon previously posted a comment with this link (http://www.richardleggett.co.uk/blog/index.php/2009/05/11/filtering-hierachical-data-in-flex-itree) that explorers the topic more thoroughly than I have done here. I recommend you check it out.

  46. Umer Says:

    Very nice, ONE Question

    how would I find some particular person in tree, in my case on root elements come from server, one i click it ll fetch children, then if you click on children it ll fetch its respective children , hence tree can be opened, ITS WORKING but
    I want to locate an object lets say person xyz, which is on 6th level, how would I open tree step by step to reach that level , please help me on that, thanks
    Umer

  47. Kalen Gibbons Says:

    Hi Umer,
    It sounds like you’re “lazy loading” the branches of your tree. If you wanted to do a search you would need to query your server for that person and, if found, have the server return the path to that person. For example 2-4-3-5 could mean that the user is 5th node on the 3rd branch on the 4th branch of the 2nd branch of the root node. Then you would traverse your tree from top to bottom either opening or loading branches until you get to the requested person node.

  48. Joe Says:

    I think this example works incorrectly because in 100% of cases I envision, the children of a filtered item should be visible. For instance, if I’m trying to filter store locations by state and I type “ca”, I want to see a list of stores in California. In your example, if I type “t”, Tiffany node is visible, but its four child nodes disappear. I fixed it by changing filter data to unfilter child node if a node returns true:

    if (searchString.length == 0
    || item.name.toLowerCase().indexOf(searchString.toLowerCase()) >= 0) {
    if (item.children) {
    item.children.filterFunction = null;
    item.children.refresh();
    }
    return true;
    }

  49. Kalen Gibbons Says:

    Hi Joe, it all depends on your use case. Thanks for posting your code, it may come in very handy for someone looking for the same result.

  50. Arthur Says:

    I am developing a similar example and had a problem with scrollbar’s parent container. When I expand the tree, it creates the verticalscroll, but if I filter the tree to have a few nodes to disappear scrollbar, the scrollbar remains the same size, and if I scrolling, happens some issue in the layout tree ..

    I tried to call invalidateDisplayList in parant container but don’t it fixed the problem.

    any suggestions?

    Thanks

  51. Benydo Says:

    I don’t know if you found a solution to the problem mentioned at 2, but maybe dispatching a CollectionEvent each time after the refresh call with CollectionEventKind.RESET will fix this problem. To be honest i also dispatch one after the invalidateList, but this is maybe unnecessary.

    Arthur this is also solves your problem.

  52. Jody Flack Says:

    hey, great blog! love it :)

  53. ashish Says:

    when i try to search “peter” tree gets properly filtered and shows me only ‘peter’ under ‘Grandma Susan’ but when i click on trangel to collapse it same ” error 1010″ is found. (this error is specially occur when data contain only one entry)
    any one can try this on the same site. after showing me error again try to open the tree, it ll not be worked until u refresh the browser.

    plz if any one has soln for the above problem post ur comment
    thanks in advance.

Leave a Reply