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;