Wednesday, July 20, 2011

The Truth About ArrayList, DataGrid, and Christmas Trees


The "High Performance 'Christmas Tree' DataGrids" entry describes how to efficiently update individual cells in a DataGrid so that it twinkles like a Christmas tree when dataProvider items are updated randomly and at a high rate.  The key to getting this done efficiently is using the DataGrid invalidateCell() method to inform the grid about the cells whose contents have changed.

If you create a DataGrid with an ArrayCollection or an ArrayList as its dataProvider, and the grid's columns just display properties of each dataProvider item, and those properties dispatch PropertyChangeEvents when they're set (as bindable properties do by default), then you'll find that it's not necessary to call invalidateCell(), and that the DataGrid is updated automatically.  You'll also find that performance isn't particularly good when the number of columns is large or the item update rate is high. This special case works "automatically" because ArrayList, or the internal ArrayList that's automatically created by ArrayCollection, adds PropertyChange listeners to all of its items and dispatches an "update" CollectionEvent in response to each one.

DataGrid responds to "update" CollectionEvents by redisplaying all of the visible cells whose renderer data is the same as the item that was updated.  This means that one or more rows of renderers will be redisplayed.

The efficiency of applications whose data model matches the special case outlined above can be improved by making a small extension to ArrayList.  The protected itemUpdated() ArrayList method is responsible for dispatching "update" CollectionEvents in response to item PropertyChangeEvents.  What we want it to do instead is to call invalidateCell() for the column (or columns) whose renderers depend on the changed item property and then only if the corresponding item is curently visible.  This can't be done within the ArrayList implementation, since the computation depends on the DataGrid and can be application-specific.  A simple way to deal with that is to have the ArrayList itemUpdated() method delegate to a Function:

public class DataList extends ArrayList
{
    private var itemUpdate:Function = null;
    
    public function DataList(source:Array=null, itemUpdate:Function=null)
    {
        super(source);
        this.itemUpdate = itemUpdate;
    }
    
    override protected function
        itemUpdateHandler(event:PropertyChangeEvent):void
    {
        if (itemUpdate != null)
            itemUpdate(event);
    }    
}

The Application provides the itemUpdate function which is responsible for calling invalidateCell() based on the item and property that changed.  The source code for DataList is here, and the source code for this example application is here.

The example below is similar to the one described in the previous entry on this topic, except that each column just depends directly on one item "colorN" property.


In this example, each dataProvider item is a simple object with one colorN property per column and each column's dataField specifies the colorN the column (GridColumn) depends on.  This makes it easy to map from a PropertyChangeEvent to the row,column location of the cell that needs to be invalidated.   If the PropertyChangeEvent maps to a visible cell, then invalidateCell() is called.

private function handleItemUpdate(event:PropertyChangeEvent):void
{
    const item:Object = event.target;
    const property:String = event.property as String;


    // Find the dataProvider index of item, if currently visible
    var itemIndex:int = -1;
    for each (var rowIndex:int in dataGrid.grid.getVisibleRowIndices())
    {
        if (dataGrid.dataProvider.getItemAt(rowIndex) == item)
        {
            itemIndex = rowIndex;
            break;
        }
    }
    if (itemIndex == -1)
        return;  // item wasn't visible


    // Find column index for event.property, if currently visible
    var propertyIndex:int = -1;
    for each (var colIndex:int in dataGrid.grid.getVisibleColumnIndices())
    {
        if (dataGrid.columns.getItemAt(colIndex).dataField == property)
        {
            propertyIndex = colIndex;
            break;
        }
    }
    if (propertyIndex == -1)
        return;  // column displaying event.property not visible
    
    dataGrid.invalidateCell(itemIndex, propertyIndex);
}


2 comments:

  1. When you said, "The protected itemUpdated() ArrayList method is responsible for dispatching 'update' CollectionEvents",

    did you mean "The protected itemUpdateHandler() ArrayList method is responsible for dispatching 'update' CollectionEvents" ??

    Great/useful post by the by

    ReplyDelete
  2. Sorry, hadn't got as far as the code when I added the previous comment, where you clearly use itemUpdatehandler!!!

    ReplyDelete