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.
<mx:Application xmlns:mx=”http://www.adobe.com/2006/mxml”
<mx:Script>
import vo.Person;
import mx.collections.ArrayCollection;
[Bindable]
private var people:ArrayCollection = new ArrayCollection([
new Person("Sammy"),
new Person("Alan")
])),
new Person(“Tiffany“, new ArrayCollection([
new Person("Adam"),
new Person("Graham"),
new Person("Vennesa")
])),
new Person(“Michael“, new ArrayCollection([
new Person("Alan", new ArrayCollection([
new Person("Jane")
]))
])),
new Person(“Peter“),
new Person("David"),
new Person("Joseph"),
new Person("Cameron"),
new Person("Debra"),
new Person("Polly")
]))
]))
]);
private function refreshData():void{
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{
for each(var _person:Person in person.children.source){
}
//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{
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
return true;
else if(item.children != null && item.children.length > 0)
return false;
}
]]>
</mx:Script>
<mx:VBox width=”100%” height=”100%“
paddingBottom=”10“
paddingLeft=”5“
paddingRight=”5“>
labelField=”name“
width=”100%“
height=”100%” />
<mx:HBox>
<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: ArrayCollection, Filtering, Recursion, Tree
Thank you very much… I am a newbie to flex.. it is very helpful to me. i have been searching for this solution. thnkx
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?
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
I’m interested in that fix as well, if you find anything!
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.
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.
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
}
have you try to do the same thing with a xmlCollection ?
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.
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);
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.
One more approach is here : http://www.davidarno.org/2009/04/01/how-to-filter-all-nodes-of-a-flex-tree-component/...
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.
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.
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
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!
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.
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.
Actually, I just realized that I was not setting the filter on the main Array collection. so that did it. I am all set
It’s Really hepled a lot
Umm .. please publish the rest of the code to make this work. it would be wonderful …. for example .. the vo.person object …
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.
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);
}
}
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
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.
Sure, send your code to kalen[dot]gibbons[at]gmail[dot]com and I’ll take a look at it.
I sent the code in your gmail account. You should get an email from rafiq16@yahoo.com
Thanks.
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;
}
}
}
}
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.
Hi Kalen,
This solves the issue. Thank you very very much! This is an owesome site and I will be a frequent user….
Mitu
Thank you sir. The example you provided here has proven a great help for me.
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
Did you ever figure out that bug from the 2nd response?
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.
greate works!
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.
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.
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
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.
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 !
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?