Home > Flex, tutorial > How to create a dynamic filter component

How to create a dynamic filter component

February 3rd, 2009

Most all web applications that I have worked on really break down to same types of components. There is usually a grid, perhaps a chart, maybe tab navigator at the top and, of course, a filtering mechanism. Lots of times, you will use the filtering down to the sql level to get data. However, its not always necessary to do that. The DataGrid component within flex does have a filterFunction property and there is no reason we shouldn’t take advantage of that. In some cases, depending on our object model, its tough to do that a “framework level”.

Now when I am coding up framework level components, I do try to keep application specific logic separate from just core components. (common sense. otherwise its not a framework level component). But I feel that a large amount people feel the filtering logic is all application specific and don’t necessarily take the time to write something in a more clean, reusable way. I have come up with a more usuable method of creating and adding filters dynamically at run time.

Before we jump into the code, lets think about what we want to accomplish. We have an application with several fields in our data model that we might want to filter on. We also don’t want to have a filterFunction with “if (filter.type == “blah”){ do this…” everywhere. Its a pain to manage, its brittle, and if you want to use that logic somewhere else in the program, it will turn into cut/paste code, which means duplicate code, which means more things that are a pain to manage. The end goal would be to create filter classes that we can dynamically apply at runtime. With this, we can create as many of these filter classes as we want and we can just give our application which filter types we want to use.

To start, lets create our base interface class called IFilter.as*

public interface IFilter{
	function filter(item:Object):Boolean;
	function get dataField():String;
	function set dataField(value:String):void;
	function get propertyField():String;
	function set propertyField(value:String):void;
}

All it is going to have is a function called filter that takes an object and a getter/setter for the dataField. The dataField will be the property on our item coming in and propertyField will be property on our data model that our filters will look at. Next we are going to implement our AbstractFilter class. (Note: Actionscript does not support an actual abstract class but we cheat and make it seem like it is hehe).

public class AbstractFilter implements IFilter
{
	private var _dataField:String;
	private var _propertyField:String;
	public function AbstractFilter(dataField:String, propertyField:String){
		_dataField = dataField;
		_propertyField= propertyField;
	}
	public function filter(item:Object):Boolean{
		throw IllegalOperationError("YOU MUST IMPLEMENT THIS FUNCTION!!!");
	}
 
	public function get dataField():String{
		return _dataField;
	}
 
	public function set dataField(value:String):void{
		_dataField = value;
	}
 
	public function set propertyField(value:String):void{
		_propertyField = value;
	}
	public function get propertyField():String{
		return _propertyField;
	}
}

All we are doing is setting the dataField/propertyField and then for our filter function, we are forcing whatever class that extends this that it must be overridden. Next we need to build our data model. This is more of an application specific thing, but its important we know what we are looking at.

public class FilterModel
{
	private static var _instance:FilterModel = new FilterModel();
	public var startDate:Date;
	public var endDate:Date;
	public var ageText:int;
	public var name:String;
	public var filterList:ArrayCollection;			
	public function FilterModel(){
		if (_instance != null)
			throw IllegalOperationError("you may only access this through getInstance()");
	}
	public static function getInstance():FilterModel{
		return _instance;
	}
}

We have a few properties on here relating to our application. Note that this is also a singleton class, we are guaranteed to only have one instance of this class and we will call it via its getInstance() function. The important part is the filterList property. This is our container for all of our IFilter’s that will be applied to our Object.

At this point, we are now ready to create our filter objects. We will create a filter that will work to filter our numbers that do not equal the value in our filter criteria.

public class IsEqualFilter extends AbstractFilter
{
	public function IsEqualFilter(dataField:String, propertyField:String){
		super(dataField,propertyField);
	}
	override public function filter(item:Object):Boolean{
		var model:FilterModel = FilterModel.getInstance();
		if (model.hasOwnProperty(propertyField)){
			var value1:int = item[dataField] as int;
			var value2:int = FilterModel.getInstance()[propertyField] as Number;	
			if (value1 == value2)
				return true;
			else
				return false;
		}else{
			//if the model does not have the specified property, ignore it.
			return true;
		}
	}
}

As you can see, it is simple. It takes in the item Object (which is from our datagrid), then we get the age from our FilterModel class and then compare it. One thing you may have noticed is that there is nothing application specific about this filter. It grabs the model data, then item data and that is it. We are able to apply this to whichever property on the grid we want.

dynamicfilter

So how does this all tie into our application? How do we tell our application which filters to use and which ones not to use. Look at the screen shot at the top. See those check boxes next to the fields? That is what is going to be our filter’s on/off switch.

public class FilterSelector extends CheckBox
{
	private var _filter:IFilter;
	public function FilterSelector(){
		super();
	}
	public function get filter():IFilter{
		return _filter;
	}
	public function set filter(value:IFilter):void{
		_filter = value;
	}
}

It is a class that extends a CheckBox and then puts the additional property of the IFilter onto it. So whenever we use it, we just give it a filter to associate itself with, like this. Whenever it is checked or unchecked, we either add or remove it from the models filterList.

Alright! We have our filters. We have our selectors. We have our model. Now lets check out the actual application.

Within our init() function, we call set our filterFunction. You probably noticed that our filterFunction is pretty simplistic. All it does it iterates through the list on our model and applies each one to our item. If any of them return false, then it is filtered out. The updateFilterList is the event handled by the check boxes. Then the applyFilters sets our model’s data and we then call refresh on our dataprovider (which in turn calls the filterFunction).

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="init()" xmlns:components="com.codeofdoom.components.*">
	<mx:Script>
		<![CDATA[
			import com.codeofdoom.components.FilterSelector;
			import com.codeofdoom.filters.DateFilter;
			import com.codeofdoom.filters.IFilter;
			import com.codeofdoom.filters.IsEqualFilter;
			import com.codeofdoom.model.FilterModel;
			import com.codeofdoom.filters.StartsWithFilter;
			import mx.collections.ArrayCollection;
 
			[Bindable]
			private var _dataProvider:ArrayCollection = new ArrayCollection([{name:"bill",age:27,date:new Date(2009,0,1)},
																			 {name:"rob",age:29,date:new Date(2009,0,12)},
																			 {name:"foo",age:28,date:new Date(2009,0,5)},
																			 {name:"bar",age:28,date:new Date(2009,0,19)},
																			 {name:"baz",age:29,date:new Date(2009,0,25)}]);
			private function init():void{
				FilterModel.getInstance().filterList = new ArrayCollection();
				_dataProvider.filterFunction = filterFunction;
			}
 
			private function updateFilters(e:MouseEvent):void{
				//Setting our model data
				var fm:FilterModel = FilterModel.getInstance();
				fm.ageText = ageStepper.value as int;
				fm.name = nameText.text;
				fm.startDate = startDate.selectedDate;
				fm.endDate= endDate.selectedDate;				 
				_dataProvider.refresh();
			}
 
			private function updateFilterList(e:MouseEvent):void{
				//checks to see if the selector has been checked. adds it if i has, removes it if it hasnt 
				var fm:FilterModel = FilterModel.getInstance();
				if ((e.currentTarget as FilterSelector).selected)
					fm.filterList.addItem((e.currentTarget as FilterSelector).filter);
				else{
					var i:int = fm.filterList.getItemIndex((e.currentTarget as FilterSelector).filter);
					fm.filterList.removeItemAt(i);
				}
			}
			public function filterFunction(item:Object):Boolean{
				for each (var f:IFilter in FilterModel.getInstance().filterList){
					if (!f.filter(item))
						return false;
				}
				return true;
			}
 
		]]>
	</mx:Script>
	<mx:VBox width="100%" paddingLeft="2" >
		<mx:HBox>
			<components:FilterSelector click="updateFilterList(event)" filter="{new StartsWithFilter('name','name')}"/>
			<mx:Label text="Starts With..."/>
			<mx:TextInput text="" id="nameText" />
		</mx:HBox>
		<mx:HBox>
			<components:FilterSelector click="updateFilterList(event)" filter="{new DateFilter('date')}"/>
			<mx:DateChooser selectedDate="{new Date(2009,0,1)}" id="startDate"/>
			<mx:Spacer width="10"/>
			<mx:DateChooser selectedDate="{new Date(2009,0,30)}" id="endDate"/>
		</mx:HBox>
		<mx:HBox>
			<components:FilterSelector click="updateFilterList(event)" filter="{new IsEqualFilter('age','ageText')}"/>
			<mx:Label text="Age..."/>
			<mx:NumericStepper id="ageStepper" value="27" maximum="30" minimum="20" />
		</mx:HBox>
 
		<mx:Button label="Update Filter" id="submitButton" click="updateFilters(event)"/>
		<mx:DataGrid id="grid" width="100%"  dataProvider="{_dataProvider}">
			<mx:columns>
				<mx:DataGridColumn dataField="name"/>
				<mx:DataGridColumn dataField="age"/>
				<mx:DataGridColumn dataField="date"/>
			</mx:columns>
		</mx:DataGrid>
	</mx:VBox>
</mx:Application>

One of the things I want to stress is how important it is to create reusable, managable code. We could actually take this one step further and place some of this logic into a FilterManager or something like that, but I didn’t (sorry). The focus of this though is to have a good PRACTICAL example of how to create a dynamic filter class that can be used in any application. I hope it was helpful for you and if you have any questions (or criticisms hehe), leave a comment.

Thanks!

Here is the source

*Any other flex/as programmers put an “I” before interfaces? It’s not exactly common within java but just wondering if anyone else does this.

Flex, tutorial , , ,

  1. Anonymous
    February 4th, 2009 at 08:06 | #1

    Putting an I before an interface is a part of the official adobe flex naming scheme — for good or bad. When in rome ….

  2. Jonathan
    June 23rd, 2009 at 13:33 | #2

    I have seen the decorator pattern used for filtering datasets in other places but not implemented like this. Nice and elegant. Thanks!

  3. EJ
    June 24th, 2009 at 10:49 | #3

    Thanks for sharing.

  4. jurguen
    March 10th, 2010 at 19:49 | #4

    very nice and neat.

    many thanks

  1. No trackbacks yet.