return undefined;


Creating truly reusable renderers with ClassFactory

Posted in AS3, Flex by Ben on the November 14th, 2006

Update: I have come up with better solutions alternate approaches since writing this post. For a more compact and self-contained approach check out this post as well.

In my last post, I outlined some ways to ensure that your item renderers behave predictably. In that article, however, I admitted that I had yet to figure out the best way to create renderers that were completely decoupled from the data they represented and therefore wholly reusable across projects. As of yesterday afternoon, I believe that has changed. I have extended my previous example to demonstrate, and the new version can be seen here. Right-click for source.

My current project requires a DataGrid with the commonly seen select/unselect all functionality. The design depicts this simply as a column that is a centered CheckBox as both the itemRenderer and headerRenderer. When you click the CheckBox in the header, it toggles the state of all the items to match its own. I first addressed the item renderers. I managed to create a component that was completely decoupled (hooray!), predictable, and easy to use. It implemented the IDropInListItemRenderer interface, which allowed it to be tied to the appropriate property in its data item through the familiar dataField attribute of DataGridColumn. When I moved on to implement the headerRenderer, however, I ran into a bit of a problem. This turned out to be a blessing in disguise though, as it led me to the real star of the show here: ClassFactory. ClassFactory essentially just allows you to specify a class (that implements IFactory), specify some properties for it, and then assign it as the itemRenderer or headerRenderer for your column.

The headerRenderer is probably the simpler of the two, so I'll go into more detail on that first. The problems I initially had with my headerRenderer were related to the fact that headerRenderers seem to be recreated, or at least reinitialized, every time there is an event in the DataGrid in which they live. (If anyone can more accurately explain how they behave please post in the comments.) As a result, you must bind their state to a value that is held outside of said DataGrid. In my case, I called this variable allSelected. You also need to set up the ClassFactory variable that you will assign as the headerRenderer. Here is what that code looks like:

[Bindable] private var checkBoxHeaderRenderer:ClassFactory;
...
checkBoxHeaderRenderer = new ClassFactory(GenericCheckBoxHeaderRenderer);
checkBoxHeaderRenderer.properties = {externalObject: this, externalPropertyName: "allSelected"};

This tells Flex to use the GenericCheckBoxHeaderRenderer class as the renderer, and to set 2 properties on each instance it creates. It should be apparent that I am giving each instance a reference to the allSelected property mentioned above. Here is what our GenericCheckBoxHeaderRenderer class looks like.

<?xml version="1.0" encoding="utf-8"?>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="100%" horizontalAlign="center" preinitialize="init()" implements="mx.core.IFactory">
    <mx:Script>
        <![CDATA[
            import mx.binding.utils.BindingUtils;
                       
            // will be set by "properties" property of ClassFactory
            // allows us to bind to an external value while remaining decoupled
            public var externalObject:*;
            public var externalPropertyName:String;
           
            // local property that will be bound to external value held in externalObject[externalPropertyName]
            // CheckBox's selected property in turn bound to this
            [Bindable] public var isSelected:Boolean;
           
            // method required by IFactory
            public function newInstance():*
            {
                return new GenericCheckBoxHeaderRenderer();
            }
           
            // set up binding of local property to value specified by ClassFactory::properties
            private function init():void
            {
                BindingUtils.bindProperty(this, "isSelected", externalObject, externalPropertyName);
            }
                       
            // local click handler that dispatches the event
            // so it can be handled in a  more appropriate place (such as the document holding the DataGrid)
            private function onClick(event:MouseEvent):void
            {
                var evt:SimpleHeaderClickEvent = new SimpleHeaderClickEvent(cb);
                dispatchEvent(evt);
            }
        ]]>
    </mx:Script>
    <mx:CheckBox id="cb" click="onClick(event)" selected="{isSelected}" width="15"/>
</mx:HBox>

One thing to note in that class is that I have created a local variable that is bound to the external value (allSelected) that is passed in via the properties property, and my CheckBox then binds to that local value.

The itemRenderer version of this code is very similar, the main difference being that it is tied to a specific property on the data object that is passed to it in the list rather than a single, external value. As a result, we only need to pass it one value, that being the name of the property to which we want to tie the renderer. The ClassFactory variable is set up in the same fashion:

[Bindable] private var checkBoxItemRenderer:ClassFactory;
...
checkBoxItemRenderer = new ClassFactory(GenericCheckBoxItemRenderer);
checkBoxItemRenderer.properties = {dataField: "isKnown"};

and the GenericCheckBoxItemRenderer class is also similar:

<?xml version="1.0" encoding="utf-8"?>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="100%" horizontalAlign="center" implements="mx.core.IFactory">
    <mx:Script>
        <![CDATA[
            import mx.controls.dataGridClasses.DataGridColumn;
           
            // name of property on our VO we're concerned with
            public var dataField:String;
           
            // method required by IFactory
            public function newInstance():*
            {
                return new GenericCheckBoxItemRenderer();
            }
           
            // this override is essential for preventing random (un)checking when your DataGrid is scrolled
            override public function set data(value:Object):void
            {
                // not sure of the details on when or why, but sometimes this method is passed a null value
                if(value != null)
                {
                    super.data = value;
                    // the parent DataGridColumn is passed as value before the real data arrives
                    if(!(value is DataGridColumn))
                    {
                        // set itemRenderer's state based on the value held in the property specified by _dataField
                        cb.selected = value[dataField];
                    }
                }
            }
           
            // local click handler that dispatches the event
            // so it can be handled in a  more appropriate place (such as the document holding the DataGrid)
            private function onClick(e:MouseEvent):void
            {
                // attach our VO and _dataField onto the event so that it can be manipulated in the handler
                // wherever that handler may be
                var evt:SimpleItemClickEvent = new SimpleItemClickEvent(super.data, dataField);
                dispatchEvent(evt);
            }
        ]]>
    </mx:Script>
    <mx:CheckBox id="cb" click="onClick(event)" width="15"/>
</mx:HBox>

Here you can see that the override method no longer relies on any specific property (and therefore datatype), instead using the property specified by dataField on whatever type of object is passed to the itemRenderer by the dataProvider.

And that's my system. If you look at the source code you can see that we still handle the events in the parent file of our DataGrid, but note that these event handlers are the only place where we are tightly coupled to specific properties and/or data types, which is exactly what we want. We have moved all type references out of our renderers, making them completely reusable across varying parts of a project or in different projects altogether. It would likely be very easy to genericize the renderers even further to allow specifying what type of control should be used (CheckBox, ComboBox, etc), which property of said control you're interested in and on and on all through the properties property, but I decided not to go that far. There is something to be said for retaining semantic meaning in your class/component names and code and not extrapolating to oblivion.

Sorry these posts are so long. As it turns out, conciseness is not a trait I possess. Hopefully someone will find this information useful though, and feel free to post any questions or other thoughts in the comments. Enjoy!