Tom Sugden: April 2010 Archives

« February 2010 | Main

April 16, 2010

Optimizing the Flex DataGrid for Frequent Updates

The default behaviour of a Flex DataGrid is to redraw itself entirely when an item in its data provider changes. This makes good sense in some cases, since the item renderers might need to grow or shrink in response, but at other times it can be problematic. If subsets of the data provider change frequently, like in a real-time price grid, excessive redrawing and increased CPU can occur. This blog post explains a simple technique for overriding the default behaviour to support frequent updates with reduced CPU.

DefaultAndOptimizedDG.png

Figure 1: Redraw regions of default (left) and optimized (right) data grids

You can download an example Flash Builder project to accompany this blog post:

Thanks to Christophe Coenraets for first showing this DataGrid optimization technique to me.

Understanding the Default Behavior

Let's say you're rendering a data provider full of simple, bindable value objects. Each time one of the properties of an item changes, the auto-generated binding code dispatches a propertyChange event. This is heard by the enclosing collection, triggering a collectionChange event of the update kind. Then deep within the DataGrid and its baseclass, ListBase, an invalidation occurs:

protected function collectionChangeHandler(event:Event):void
{
    ...
    itemsSizeChanged = true;

    invalidateDisplayList();
}

So the DataGrid will be redrawn during the next update. In the worst case scenario, one or more items of data might change during each frame interval, causing the grid to invalidate itself and be redrawn on every frame. This would increase CPU.

Overriding the Default Behavior

The default behaviour of the DataGrid can be overridden so that item renderers invalidate themselves independently and only when changes to their individual data items occur. This optimization usually involves two steps:

  1. Extend DataGrid to override collectionChangeHandler() to ignore collection change events that arise from updates to items in the data provider.
  2. Write a custom item renderer that handles its own invalidation when it detects changes in its data item.

Extend DataGrid

Shown below is a simple extension of DataGrid that overrides the collectionChangeHandler() function to ignore collectionChange events with the update kind.

public class FastDataGrid extends DataGrid
{
    override protected function collectionChangeHandler(event:Event):void
    {
        if (event is CollectionEvent &&
            CollectionEvent(event).kind != CollectionEventKind.UPDATE)
        {
            super.collectionChangeHandler(event);
        }
    }
}

This change stops the DataGrid redrawing excessively when frequent changes occur, but it needs to be complemented with a modified item renderer that knows how to invalidate itself.

Note that this customization is only necessary if the value objects in your data provider are dispatching propertyChange events. If you're using custom events instead, then the collection will ignore these and so the invalidation of the DataGrid will not be triggered.

Self-Invalidating Item Renderer

Shown below is a custom ActionScript item renderer based on UIComponent containing a single UITextField. The item renderer listens to its data item for matching propertyChange events and invalidates itself when they occur.

public class PropertyChangeRenderer
    extends UIComponent 
    implements IDropInListItemRenderer, IListItemRenderer 
{
    private var textField:UITextField;
    private var column:DataGridColumn;
    private var updateText:Boolean;

    //-------------------------------
    //  listData
    //-------------------------------

    private var _listData:DataGridListData;

    [Bindable("dataChange")]
    public function get listData():BaseListData
    {
        return _listData;
    }

    public function set listData(value:BaseListData):void
    {
        _listData = value as DataGridListData;
        column = _listData 
            ? DataGrid(_listData.owner).columns[_listData.columnIndex] as DataGridColumn 
            : null;
    }

    //-------------------------------
    //  data
    //-------------------------------

    private var _data : Object;

    public function get data() : Object
    {
        return _data;
    }

    public function set data(value:Object):void
    {
        if (_data == value) return;

        if (_data && _data is IEventDispatcher)
        {
            IEventDispatcher(_data).removeEventListener(
                PropertyChangeEvent.PROPERTY_CHANGE,
                propertyChangeHandler);
        }

        _data = value;
        updateText = true;

        if (_data && _data is IEventDispatcher)
        {
            IEventDispatcher(_data).addEventListener(
                PropertyChangeEvent.PROPERTY_CHANGE,
                propertyChangeHandler,
                false, 0, true);
        }

        dispatchEvent(new FlexEvent(FlexEvent.DATA_CHANGE));
    }

    private function propertyChangeHandler(event:PropertyChangeEvent):void
    {
        if (event.property == _listData.dataField)
        {
            updateText = true;
            invalidateProperties();
        }
    }

    //---------------------------------------------------------------------
    //
    //  Overrides : UIComponent
    //
    //---------------------------------------------------------------------

    override protected function createChildren():void
    {
        super.createChildren();

        textField = new UITextField();

        // hardcoded size and position to confine the redraw region, 
        // since it never changes in this example
        textField.width = 130;
        textField.height = 16;
        textField.x = 3;

        addChild(textField);
    }

    override protected function commitProperties():void
    {
        super.commitProperties();

        if (updateText)
        {
            updateText = false;
            textField.text = column.itemToLabel(data);
        }
    }
}

Some compromises are made in this item renderer. It's not as general-purpose as the DataGridItemRenderer and its creation-time is going to be higher, since it uses a UIComponent to contain a UITextField, whereas the DataGridItemRenderer simply extends UITextField. However, at runtime the CPU will be reduced, since only a region of 130x16 will be redrawn for each property change instead of the whole grid.

Sample Application

The FastDataGrid.zip project contains an application, FastDataGridExample.mxml, that demonstrates the default and optimized data grids. You can use this as a starting point to refine the grid and item renderer and perhaps make further optimizations or better generalizations. The project contains two implementations of the item renderer with comments describing the differences.

Conclusion

If your application contains data grids that change frequently, use the "Show Redraw Regions" feature of Flash Debug Player to make sure that excessive redrawing is not taking place. If so, consider optimizing the data grid and your item renderers to minimize redrawing and reduce CPU. The same technique described in this blog post also applies to the AdvancedDataGrid.

Posted by tsugden at 10:16 AM | Comments (2)