This post describes how to filter data for use with any component that displays hierarchical data, such as the Tree, AdvancedDataGrid, or your own custom component. For the purpose of the post I’ll just consider the Tree, but the same technique applies to the others.

Background

Suppose I have constructed a “Library” of Folder and Leaf nodes and I want to display that in a Tree. Each Folder node has an ArrayCollection called “children” which contains any child Folders and/or Leaf nodes.

So our classes may resemble something like this (I’ve kept things extremely simple for illustration purposes):


public class FolderItem 
{
  public function get name():String { ... }
  public function get type():String { ... }
  public function get parentFolder():Folder { ... }
}

public class Folder extends FolderItem
{
  public function get children():ArrayCollection { ... }
}

public class Leaf extends FolderItem
{
  public function get someLeafProp():String {...}
}

In this case the only major difference between a Folder and a Leaf is that the Leaf class has no children, but you may have a lot more code in your actual objects, this doesn’t really matter. All we care about is that we have some data to filter on.

The Tree component will happily take the children ArrayCollection of the root Folder as its dataProvider, and it will display any sub-folders and leaf nodes without any trouble, all the way down to the very final Leaf nodes. However we haven’t yet told it exactly how to display them, so it may look like garbage, but you can solve that by using the labelFunction property on the Tree.

Filtering

Now suppose I want to display only the Folders that have their type property set to “imageFolder”. Well, many blog posts you may read will say just set the filterFunction built into the ArrayCollection class and call refresh(). But this has a couple of problems…

So why can’t we just use ArrayCollection’s filterFunction?

The problem with the ArrayCollection’s filterFunction is two-fold:

1. It only works on the first “level” of objects. It will not filter the children of its children. So this is no good for hierarchical data filtering and Trees.

2. You can technically get around that limitation by doing your own recursion, going into every child and setting the filterFunction on each children ArrayCollection of each child. I won’t go into how to write a recursive function as it’s pretty straight forward, but there you encounter another problem, described below.

Recursively filtering ArrayCollections

So you’ve written a function that sets the filterFunction on a top level node, and recurses down through its children setting their filterFunction, and their childrens’ filterFunctions and so on. And voila! Your Tree displays the right nodes. Job done? Afraid not. What you’ve in-inadvertently done here is modified your data so that when something else wants to display your “Library”, it’ll already be filtered, woops.

This is because you’ve set the filterFunction on all the children, so to display the Library elsewhere, you’d have to recursively switch off all those filterFunctions. This is not just in-efficient, it relies on the programmer knowing they need to do that before they can view the original data. It also means you cannot display two views of the same Library using two different filters at the same time*.

*Imagine you have two trees, one un-filtered, one filtered. As the user expands/contracts nodes in the first supposedly unfiltered Tree, they’ll see the effects of the filterFunction which was applied to the data when you set up the second tree because both Tree components are reading the same hierarchical data.

The solution, implement ITreeDataDescriptor and ITreeDataDescriptor2

So how do we filter hierarchical data without permanently affecting the underlying data?

The Tree component has a property called dataDescriptor. This takes an instance of ITreeDataDescriptor. It uses this object to obtain and interpret how the data is structured so that it can display it. By default a Tree component will use the DefaultDataDescriptor implementation which will not filter anything.

There is a one method it calls on ITreeDataDescriptor that we can utilise to perform our filtering, that method is getChildren(node:Object, model:Object=null);

In the current Flex 3 and 4 SDKs the Tree component also supports an extension to ITreeDataDescriptor called ITreeDataDescriptor2 which has three extra methods, but we won’t need to use that here. (I believe these extra methods patch some un-desirable behaviour in the AdvancedDataGrid so please be sure to extend that if required. See comments for more on this.)

So let’s create a new class and extend ITreeDataDescriptor. I’ve pasted a sample below:


import com.domain.app.model.Folder;
import com.domain.app.model.FolderItem;

import mx.collections.ArrayCollection;
import mx.collections.ICollectionView;
import mx.collections.IViewCursor;
import mx.controls.treeClasses.ITreeDataDescriptor;

public class LibraryTreeFilteredDataDescriptor implements ITreeDataDescriptor
{
	private var filter:FolderFilter;
	
	public function LibraryTreeFilteredDataDescriptor(filter:FolderFilter)
	{
		this.filter = filter;
	}
	
	public function getChildren(node:Object, model:Object=null):ICollectionView
	{
		var children:ArrayCollection = new ArrayCollection([]);
		
		if(filter == null) 
		{
			// no filter being used, just return the children for Folder nodes
			var folderItem:FolderItem = node as FolderItem;
			if(folderItem is Folder) return (folderItem as Folder).children;
			else return null;
		}
		else if(node is Folder)
		{
			// filter the Folder's children
			var folder:Folder = node as Folder;
			
			for(var i:uint=0; i < folder.children.length; i++)
			{
				var child:FolderItem = folder.children.getItemAt(i) as FolderItem;
				if( filter.filterFunction(child) ) 
					children.addItem(child);
			}
		}
  
           return children;
	}
	
	public function hasChildren(node:Object, model:Object=null):Boolean
	{
		var folderItem:FolderItem = node as FolderItem;
		if(folderItem is Folder) return (folderItem as Folder).children.length > 0;
		else return false;
	}
	
	public function isBranch(node:Object, model:Object=null):Boolean
	{
		return hasChildren(node, model);
	}
	
	public function getData(node:Object, model:Object=null):Object
	{
		return node;
	}
	
	public function addChildAt(parent:Object, newChild:Object, index:int, model:Object=null):Boolean
	{
		// not impl
		return false;
	}
	
	public function removeChildAt(parent:Object, child:Object, index:int, model:Object=null):Boolean
	{
		// not impl
        return false;
	}	
}

I mentioned we can use the getChildren() method to filter our data, and we kind of are. But you’ll notice I’ve not put the filtering in directly here, instead I’m passing a FolderFilter instance into this ITreeDataDescriptor implementation and calling the filter’s filterFunction inside of getChildren() instead.

Why externalise the filterFunction?

The reason I wrote the filterFunction in a separate class to the LibraryTreeDataDescriptor is because whilst the dataDescriptor will filter the children, it (rather ironically) not filter the first level of nodes in the Tree’s children.

So by externalising it into a FolderFilter class, we can re-use it to filter the first level when assigning the Tree’s dataProvider without modifying the underlying data.

Here’s how the FolderFilter looks:


import com.domain.app.model.Asset;
import com.domain.app.model.Folder;
import com.domain.app.model.FolderTypes;
import com.domain.app.model.Slide;

public class FolderFilter
{
	public var folderTypes:Array		= []; 
	public var searchString:String		= "";
	
	/**
	* Filters by Folder.type and matches searchString if given
	*/
	public function filterFunction(node:Object):Boolean
	{
		searchString = searchString.toLowerCase();
		
		var folderItem:FolderItem = node as FolderItem;
		
		// build Regex to match folder types
		var folderTypesRegex:RegExp;
		if (folderTypes != null && folderTypes.length > 0)
		{
			folderTypesRegex = new RegExp( "(\" + folderTypes.join(")|(\") + ")", "i");
		}
			
		// begin filtering
		var allowed:Boolean = false;
		
		// match Folder.type
		if( !(folderItem is Folder) 
			|| (folderItem is Folder &&  (folderItem as Folder).type.search(folderTypesRegex) != -1) )
		{
			// match searchString
			if(searchString=="")
			{ 
				allowed = true;
			}
			else if(folderItem.name.toLowerCase.indexOf(searchString) != -1) 
			{
				allowed = true;
			}
		}
		
		return allowed;
	}
}

Now this filterFunction is not the simplest, but it could also be more complex. At the simplest level you could simply use the filterFunction to match a single property such as see if a search term appears in the “name” property for an item in your Tree.

Finally here’s how you’d use it with a Tree:


var folderFilter:FolderFilter = new FolderFilter();
folderFilter.folderTypes = ["imageFolder", "videoFolder"];
folderFilter.searchString = searchString;
					
libraryTree.dataDescriptor = new LibraryTreeFilteredDataDescriptor(folderFilter);
libraryTree.showRoot = false;      // do not show a folder for the Library itself
libraryTree.dataProvider = _library;

That pretty much wraps it up. You can now display various views of the same hierarchical data whilst applying different types of simple or complex filter (perhaps also using search terms as shown in the above code).

Alternative when using an ArrayCollection as the DataProvider
In this case I’ve used a single object as the data provider, the Library, and I’ve specified showRoot = false on the tree so it does not appear as a folder. If you plan to use an Array/ArrayCollection as the dataProvider instead, you will need to filter this before assigning it as the dataDescriptor will not catch the first layer of data, also remove the showRoot = false (defaults to true) e.g.

var dataProvider:ArrayCollection = new ArrayCollection(_library.children.source);
dataProvider.filterFunction = folderFilter.filterFunction;
dataProvider.refresh();

libraryTree.dataProvider = dataProvider;

  • Rich

    Posted: May 11, 2009


    I mention ITreeDataDescriptor2 in the post, here's how I'd implement those extra 3 methods it defines:


    public function getParent(node:Object, collection:ICollectionView, model:Object = null):Object
    {
    return (node as FolderItem).parentFolder;
    }

    public function getNodeDepth(node:Object, iterator:IViewCursor, model:Object = null):int
    {
    var folderItem:FolderItem = node as FolderItem;

    var depth:int=0;
    var parentFolder:Folder = folderItem.parentFolder;

    while(parentFolder != null)
    {
    depth++;
    parentFolder = parentFolder.parentFolder;
    }

    return depth;
    }

    public function getHierarchicalCollectionAdaptor(hierarchicalData:ICollectionView,
    uidFunction:Function,
    openItems:Object,
    model:Object = null):ICollectionView
    {
    return new HierarchicalCollectionView(hierarchicalData,
    this,
    uidFunction,
    openItems);
    }


  • Brian

    Posted: May 22, 2009


    Can you review your ITreeDataDescriptor2 implementation (specifically getHierarchicalCollectionAdaptor())? The constructor for HierarchicalCollectionView takes only two parameters. IHierarchicalData is the first parameter, and ICollectionView does not extend IHierarchicalData.


  • Rich

    Posted: May 22, 2009


    Hi Brian, I have mx.controls.treeClasses.HierarchicalCollectionView open in a text editor, I can see the constructor takes 4 parameters. You may be referring to the "other" HierarchicalCollectionView in the mx.collections package, yep, it's that crazy but it can be explained...

    As to which to use, the Tree class imports the (bundled) one that takes 4 parameters (you can find it in the mx.controls.treeClasses package.The other one in mx.collections.* does take 2 params but it is not part of the standard Flex SDK, it is found in Flex Builder Professional's "Data Visualisation Components" SWC, and I would guess that one is used when you are defining a DataDescriptor for the AdvancedDataGrid, not the Tree.

    You can see the notice about it being for the Data Visualization Components only at the top of the livedocs page for it:

    http://livedocs.adobe.com/flex/gumbo/langref/mx/collections/HierarchicalCollectionView.html

    It gets more confusing to the developer though because the "standard"/treeClasses HierarhicalCollectionView has an [ExcludeClass] meta-tag which I believe prevents FlexBuilder from "auto-importing" it, and also from it appearing in the Livedocs, even though you can import and use it manually. Presumably this is because you don't need to use it yourself, the Tree is the only thing that actually needs to use it.


  • Brian

    Posted: May 22, 2009


    Disregard previous comment; I did not realize that there are two HierarchicalCollectionView(s):

    mx.collections.HierarchicalCollectionView
    mx.controls.treeClasses.HierarchicalCollectionView

    I'm sure there is a good reason for naming a "collection" a "view", but that's a different question for a different day.

    Thanks.


  • Stu

    Posted: July 31, 2009


    Hey Richard,

    I have rewritten some of your code for use with an XMLList/XMLListCollection datasource.

    Below are the rewritten filterFunction, getChildren and hasChildren functions:


    public function filterFunction(node:XML):Boolean
    {
    //Set searchString
    searchString = searchString.toLowerCase();

    //Determine if node or a descendant node contains searchString
    if(node.@name.toString().toLowerCase().indexOf(searchString) != -1 ||
    node.*.(@name.toString().toLowerCase().indexOf(searchString) != -1).length() > 0) return true;

    //Walk up the tree to see if predecessors contain the search string
    var pre:XML = node.parent();
    while(pre != null){
    if(pre.@name.toString().toLowerCase().indexOf(searchString) != -1){
    return true;
    }
    pre = pre.parent();
    }
    return false;
    }

    public function getChildren(node:Object, model:Object=null):ICollectionView
    {
    var children:XMLListCollection = new XMLListCollection();
    if(filter == null || filter.searchString == null || filter.searchString == "")
    {
    var root:XML = node as XML;
    if(root.children().length() > 0) return new XMLListCollection(root.children());
    else return null;
    }
    else if(node is XML)
    {
    //Filter node
    var item:XML = node as XML;
    for each(var child:XML in item.children())
    {
    if(filter.filterFunction(child))
    children.addItem(child);
    }
    }
    return children;
    }

    public function hasChildren(node:Object, model:Object=null):Boolean
    {
    return XML(node).children().length() > 0;
    }


    When performing a search I'm using the following code:


    MyDescriptor(tree.dataDescriptor).filter.searchString=txtSearch.text;
    tree.invalidateList();
    expandAll(); //expands all tree nodes


    This seems to work fine when the search results contain only leaf nodes. If however a folder is included in the search results I get a stack overflow error at line 147 of XMLListAdapter.as which returns the source of an XMLList.

    I can only assume that something is screwed with respect to the HierarchicalCollectionView or ICollectionView.

    Have you got any idea what is going on here?

    Cheers,
    Stu.