ArrayCollection: filterFuntion with multiple filter functions using decorator pattern

I was looking for an elegant solution to implement multiple filter functions on an ArrayCollection (which provides a method filterFunction() that can be assigned dynamically in order to filter data inside the collection). The first and only valid solution I found was that one from Cristian Rotundu, he has extended ArrayCollection class providing a filterFunctions properties which accepts an Array of functions which will be executed in sequence (into a loop) once filterFunction is triggered. Anyway I didn’t want to subclass ArrayCollection and by using the decorator pattern I implemented my own multiple filters version.

So, this is what I done:

  1. I defined an interface IFilter, which declares only one method: apply()
  2. I defined a dummy Filter class (which implements IFilter in a basic way just to agree to the interface), that is used as a basic empty filter to decorate. It also has a constant called ALL_VALUES which is used as a wildcard for filters (it’s a simple string with value “*”, which means “all values are allowed”)
  3. I defined an abstract AbstractFilterDecorator class
  4. I created as many subclass of AbstractFilterDecorator as the filters I need (yes I treat filters as classes not mere functions)

The implementation code is the following:

var data:ArrayCollection = ArrayCollection(grid.dataProvider);
var filter:IFilter = new Filter();
				
filter = new LevelFilter(filter, levelValue);
filter = new CategoryFilter(filter, categoryValue);
filter = new DateFilter(filter, dateValue, "DD/MM/YY");
				
data.filterFunction = filter.apply;
data.refresh();

The filter object is wrapped by decorators which all accepts 2 common arguments: an IFilter reference and an Object representing a value. The DateFilter has a third parameter which is used to configure an internal DateFormatter (from mx.formatters package).
I’m pretty satisfied :)
Uh… and if you are worried about performance, it is fast enough because filter object are created once and then the function assigned to filterFunction is a reference to the resulting decorated filter.

UPDATE:
I realized a very simple example application which can be downloaded here.
This following is the content of the mxml file:

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

	<mx:Script>
		<![CDATA[
		
			import mx.collections.ArrayCollection;
			import com.daveoncode.filters.*;
			
			private function applyFilters():void {
				
				var data:ArrayCollection  = ArrayCollection(userGrid.dataProvider);
				var filter:IFilter = new Filter();
				
				filter = new AgeFilter(filter, ageFilterValue.text);
				filter = new DateFilter(filter, dateFilterValue.selectedDate);
				filter = new SexFilter(filter, sexFilterValue.value);
				
				data.filterFunction = filter.apply;
				data.refresh();
				
			}
			
		]]>
	</mx:Script>
	
	<mx:Panel title="Filters" width="600">
		<mx:Form>
			<mx:FormItem label="Age:">
				<mx:TextInput id="ageFilterValue" width="30" />
			</mx:FormItem>
			<mx:FormItem label="Sex:">
				<mx:ComboBox id="sexFilterValue" dataProvider="{[{data: Filter.ALL_VALUES, label: 'ALL'}, {data: 'm', label: 'Male'}, {data: 'f', label: 'Female'}]}" />
			</mx:FormItem>
			<mx:FormItem label="Join date:">
				<mx:DateField id="dateFilterValue" formatString="DD/MM/YY" />
			</mx:FormItem>
			<mx:FormItem>
				<mx:Button label="Apply filters" click="{this.applyFilters()}" />
			</mx:FormItem>
		</mx:Form>
	</mx:Panel>
	
	<mx:Panel title="Users" width="600">
		<mx:DataGrid id="userGrid" width="100%">
			<mx:dataProvider>
				<mx:ArrayCollection>
					<mx:Array>
						<mx:Object age="24" sex="f" name="Susan" joinDate="{new Date(2007, 5, 15)}" />
						<mx:Object age="36" sex="f" name="Ashley" joinDate="{new Date(1998, 7, 20)}" />
						<mx:Object age="24" sex="f" name="Jennifer" joinDate="{new Date(2001, 3, 24)}" />
						<mx:Object age="19" sex="f" name="Emma" joinDate="{new Date(2002, 3, 24)}" />
						<mx:Object age="44" sex="f" name="Carol" joinDate="{new Date(1999, 9, 16)}" />
						<mx:Object age="28" sex="m" name="Peter" joinDate="{new Date(2005, 3, 12)}" />
						<mx:Object age="35" sex="m" name="Mike" joinDate="{new Date(2008, 10, 10)}" />
						<mx:Object age="26" sex="m" name="Dave" joinDate="{new Date(2008, 10, 10)}" />
						<mx:Object age="44" sex="m" name="William" joinDate="{new Date(2004, 9, 16)}" />
						<mx:Object age="24" sex="m" name="Sean" joinDate="{new Date(2006, 3, 24)}" />
					</mx:Array>
				</mx:ArrayCollection>
			</mx:dataProvider>
		</mx:DataGrid>
	</mx:Panel>
	
</mx:Application>

20 thoughts on “ArrayCollection: filterFuntion with multiple filter functions using decorator pattern

  1. Davide Zanotti Post author

    Because, as I learned by studying design patterns, subclassing is not always the best way to extend a class. Furthermore the filter system of ArrayCollection as been thought as single function, that is a moot point but that is and I didn’t want to “upset” the logic behind its design.
    Another reason is that by defining each filter as a class, my code is cleaner and more flexible

  2. Davide Zanotti Post author

    Actually I’m using this technique on my own project and I don’t wanna share all the code until it’s complete :P
    …however I’m going to create and share an example for tomorrow ;)

  3. Gareth Arch

    Nice! Are you going to be posting the code for your Filter classes? Took me a few moments to see how it was actually working, but I like your implementation of the filter classes.

  4. Dennis Jaamann

    Took a look at the code and this solution is indeed far superior to extending the arraycollection class.

    The method you are using (the decorator) encapsulates variation, enforces higher cohesion and a lower coupling. All this enables even more flexibilty.

    Nice work and good thinking.

  5. David Repas

    Very cool. Any advice on how to filter multiple traits targeting the same field?

    For example – let’s say I have a color field in an arraycollection that holds values such as Red, Blue, White, Black, Brown. How can I create a filter that shows both Red and Black, etc.

    Any advice? Thanks much.

  6. Davide Zanotti Post author

    There are different approaches to accomplish your needs, first of all since each concrete filter must override apply() method (from the AbstractFilterDecorator class), you can add all the logic you need inside that method and create extra internal private methods to “extend” it. You can also decide to pass extra arguments to your “ComplexFilter” by changing the constructor’s signature to match your requirements (so instead of ComplexFilter(target:IFilter, value:Object) you can have ComplexFilter(target:IFilter, red:uint, blue:uint, black:uint)).
    This is a fast example of a “complexFilter”:

    public class ComplexFilter extends AbstractFilterDecorator {

    public function ComplexFilter(target:IFilter, value:Object) {
    super(target, value);
    }

    private function filterColors(colors:Array):Boolean {

    for (var i:uint=0; i<colors.length; i++) {
    if (colors[i] != this._value[i]) {
    return false;
    }
    }

    return true;

    }

    override public function apply(record:Object):Boolean {
    return this._target.apply(record) && this.filterColors(record.complexProperty);
    }

    }

    applicable to something like this:

    <mx:Application xmlns:mx=”http://www.adobe.com/2006/mxml” layout=”vertical”>

    <mx:Script>
    <![CDATA[

    import filters.*;
    import mx.collections.ArrayCollection;

    private function filter():void {

    var data:ArrayCollection = ArrayCollection(mygrid.dataProvider);
    var filter:IFilter = new Filter();

    filter = new ComplexFilter(filter, [251, 100, 94]);

    data.filterFunction = filter.apply;
    data.refresh();

    }

    ]]>
    </mx:Script>

    <mx:DataGrid id=”mygrid” width=”500″>
    <mx:dataProvider>
    <mx:ArrayCollection>
    <mx:Array>
    <mx:Object simpleProperty=”easy” complexProperty=”{[255, 100, 94]}” />
    <mx:Object simpleProperty=”easy” complexProperty=”{[255, 100, 94]}” />
    <mx:Object simpleProperty=”easy” complexProperty=”{[255, 100, 94]}” />
    <mx:Object simpleProperty=”easy” complexProperty=”{[251, 100, 94]}” />
    </mx:Array>
    </mx:ArrayCollection>
    </mx:dataProvider>
    </mx:DataGrid>

    <mx:Button width=”200″ height=”50″ label=”apply filters” click=”{filter()}” />

    </mx:Application>

    I hope my dummy example will help you… anyway, just play and experiment your own solution. Good luck ;-)

  7. arest

    Looks awesome :)
    Great work…

    Do you released the code in a specific license or how can we use the code?

  8. Bill

    thanks for sharing David! got this solution to work vs extending the arraycollection. For those who want to do a partial match on a string (ie text entry field) you can modify the override public function apply in your specific .as from the original “return this._target.apply (item) && (String(this._value) == String(item.WhateverColumnHere) || this._value == null)” to “return this._target.apply (item) && (key.indexof(this._value.toString().toLowerCase())!=-1 || this._value == null)” where you define key as a variable “var key:String = item.WhateverColumnHere.toLowerCase(“);

  9. swan

    HI wat if ageFilterValue.text has comma separated values( n values ) filtering is not done on the values pl need help

  10. jetspice

    Ack. Nothing on the web about why an ExtendedArrayCollection will not type-cast as an ArrayCollection. Will your technique work to add functions to the ArrayCollection class (I want to add disableEvents() and enableEvents() like ArrayList has)?

Leave a Reply