Transforming ArrayCollection data using a map function to keep your view seperated in an MVC app

Last night I was working on a project which involved me having to display large amounts of data in a Flex list.  However, this data needed to be transformed before being displayed.  Let me explain with a simplified example.

The problem

Suppose we need to display a huge list of Result objects, where a Result object contains a score property (lets say it is a Number).  If we just need to display the number then we provide the ArrayCollection of Results as the dataProvider to the list and set the dataField to ‘score’ and everything is nice and easy.  Now suppose that we need to do some kind of calculation on score – for example, we need to divide it by 2 before displaying it.  Again, this is no problem; we use the labelFunction in the list and add a callback function in the MXML file that divides the score by two before returning it:

   1: private function labelFunction(item:Object):String {

   2:     var result:Result = item as Result;

   3:     return result.score / 2;

   4: }

This is all fine.

Now lets suppose that we need to transform the score in a more complicated way – imagine that we are writing some kind of multiuser application and we need to divide the score by the number of users currently logged on to the application before displaying it in the list.  At this point I should point out that I tend to use PureMVC so I will explain this using PureMVC terminology, but its equally applicable to all other MVC frameworks (Mate, Swiz, Robotlegs, etc).

Lets assume that the number of users logged on is in our Model tier; we then have a couple of options on how to go about this:

Inject the number of users into the view from our mediator and continue to use labelFunction.

       1: public var userCount:Number;

       2:

       3: private function labelFunction(item:Object):String {

       4:     var result:Result = item as Result;

       5:     return results.score / userCount;

       6: }

This is an ok solution – it doesn’t break MVC encapsulation and it works fine.  However, the drawbacks are that we need to monitor when userCount changes and each time re-inject it into the view (and tell the list to update itself).  But the main problem with this is that it gives our view knowledge of something that it shouldn’t know about.

Extend Result to do the calculation itself

We can add a getter to Result that does the calculation which means we can get rid of labelFunction in the view and just set dataField to the getter (e.g. dataField=”transformedScore”).  But this just moves the problem elsewhere; now the Result object will need to know the number of users; we’d either have to inject it in via a static variable, or give the Result object knowledge of the MVC framework.  This solution is in fact much worse than our first effort.

Pre-build the ArrayCollection with transformed scores

In this method we iterate through the Results, doing our calculation on score each time and building up a new dataprovider.  This is fine from an MVC perspective; the view gets its data fully formed in the format it requires, and since the mediator is building the list there is no problem with it querying the model tier.  However, this is a bad solution for other reasons; say that you have 5000 Result objects; the mediator will need to iterate through the entire list before passing the new collection to the view, incurring a nasty performance hit.  Furthermore we will need to rebuild this list entirely if the score or the number of logged in users change.

The solution

Here, at last, is my solution to the problem.  Like all good solutions it is disarmingly simple 🙂

   1: package org.davekeen.collections {

   2:     import mx.collections.IList;

   3:     import mx.collections.ListCollectionView;

   4:

   5:     public class MappedListCollectionView extends ListCollectionView {

   6:

   7:         public var mapFunction:Function;

   8:

   9:         public function MappedListCollectionView(list:IList = null) {

  10:             super(list);

  11:         }

  12:

  13:         override public function getItemAt(index:int, prefetch:int = 0):Object {

  14:             if (mapFunction == null) {

  15:                 return super.getItemAt(index, prefetch);

  16:             } else {

  17:                 return mapFunction(super.getItemAt(index, prefetch));

  18:             }

  19:         }

  20:

  21:     }

  22:

  23: }

MappedListCollectionView extends the normal ListCollectionView (which is the superclass of ArrayCollection) adding a single property – mapFunction – which transforms an item of the array collection into another item.  So now we can do the following in the mediator:

   1: var mappedListCollectionView:MappedListCollectionView = new MappedListCollectionView(resultsArrayCollection);

   2:

   3: mappedListCollectionView.mapFunction = function(item:Object):String {

   4:     var userCount:Number = getUserCountFromModel();

   5:

   6:     var result:Result = item as Result;

   7:     return result.score / userCount;

   8: }

   9:

  10: myView.myList.dataProvider = mappedListCollectionView;

I think this is an excellent solution – it maintains encapsulation within the view, it follows Flex standards (in that its syntax is the same as filterFunction), it will work with Flex binding and COLLECTION_CHANGE events and best of all it is high performance as mapFunction is only called for the rows that are actually on the screen.

One thing to bear in mind when using this solution is that Flex databinding will only be triggered when elements of the array collection itself change, so if userCount changes you will need to manually dispatch a COLLECTION_CHANGE event on the array collection.

Hope you find this as useful as I do!

Using addItemAt and removeItemAt on a DataGrid dataprovider

According to the notes buried deep within the Flex documentation:

If you use the ICollectionView interface to sort or filter a collection, do not use the IList interface to manipulate the data, because the results are indeterminate.

This means that if you are using an ArrayCollection as the dataprovider of a sortable component (e.g. a DataGrid), sort the columns and then try and use addItemAt() or removeItemAt() elsewhere in your application you will be entering a world of pain; the wrong items will be added/deleted in the datagrid, rows might get duplicated in strange way, etc.

The reason for this is that sorting the datagrid is changing the ListCollectionView of the original ArrayCollection which affects subsequent index based operations.

Luckily there is a simple solution; give the component its own personal ListCollectionView:

   1: dataGrid.dataProvider = new ListCollectionView(arrayCollection);

This preserves IList integrity on the original collection which means that databinding should work as expected.

Watch and learn – Data binding in Flex

My apologies to those of you who are watching my blog for updates – I’ve been working flat out on a number of exciting projects over the last few months including a plugin-based XML editor written in Flex which will be released as open source software in the coming weeks.

In the meantime I wanted to point everyone’s attention to this brilliant lecture which explains Flex data binding at the compiler level and finally tells us what [Bindable] really does, why we need to use ArrayCollection instead of Array and all the other internals of Flex architecture which the official docs gloss over.

http://www.onflex.org/ted/2008/08/diving-in-data-binding-waters-with.php