Hi, I'm Ben. This is my blog. I write about Flex,
Flash, software development, and other miscellany.

Search Results

Efficient, reusable (and centered) CheckBox renderers for DataGrids

As it turns out, truly reusable CheckBox renderers are much simpler to create than I previously reported. This post will provide and discuss what I think are the last CheckBox renderers you will ever need.

Did you know that CheckBox has a built-in click handler? I didn't until recently, despite its not-so-sneaky name of clickHandler. It is a protected function, so its only accessible if you subclass CheckBox, but that's OK since our renderers will do just that. clickHandler is key to creating these reusable renderers and keeping them super simple.

Before I start in on my inevitably wordy and detailed description lets take a look at our end result. CheckBoxRenderers in action and the source.

Edit: I initially forgot to mention that these renderers are heavily based on and influenced by the information provided by Flex Jedi Alex Harui on his blog. Thanks to Alex for illustrating "the right way" to do things.

This time around we will start with the item renderer since it is the simpler of the two. (Both of these classes center the CheckBox as that is the most common request, but if you don't need them centered simply remove the updateDisplayList() functions.)

CenteredCheckBoxItemRenderer.as:

package com.returnundefined.view.renderers
{
    import flash.display.DisplayObject;
    import flash.events.MouseEvent;
    import flash.text.TextField;

    import mx.controls.CheckBox;
    import mx.controls.dataGridClasses.DataGridListData;

    public class CenteredCheckBoxItemRenderer extends CheckBox
    {
        // update data item on click
        override protected function clickHandler(event:MouseEvent):void
        {
            super.clickHandler(event);
            data[DataGridListData(listData).dataField] = selected;
        }

        // center the checkbox icon
        override protected function updateDisplayList(w:Number, h:Number):void
        {
            super.updateDisplayList(w, h);

            var n:int = numChildren;
            for (var i:int = 0; i <n; i++)
            {
                var c:DisplayObject = getChildAt(i);
                // CheckBox component is made up of box skin and label TextField
                if (!(c is TextField))
                {
                    c.x = (w - c.width) / 2;
                    c.y = (h - c.height) / 2;
                }
            }
        }
    }
}

There are 2 major differences between this class and our previous version. First, it doesn't implement ClassFactory, which means we can simply type its fully qualified class name into the itemRenderer attribute of a DataGrid and it will work (just like with mx.controls.CheckBox), requiring one less bindable variable in our file. The reason we can do this is that CheckBox, and by extension CenteredCheckBoxItemRenderer implement IDropInListItemRenderer. I won't go into the full details (if anyone would like me to dedicate a post to describing this more fully let me know) but in the case of CheckBox, that basically means that it knows how to set its selected state based on a piece of data that is passed to it. The second major difference is closely related to this fact, which is that we no longer have to override the data setter- we get that functionality for free.

The most important line in the item renderer class is essentially the other half of the equation. Whereas we get data retrieval and rendering for free, we have to provide the data setting functionality. That is really the only thing that separates it from a regular CheckBox other than the centering code, and its accomplished with this single line of code:

data[DataGridListData(listData).dataField] = selected;

This line uses the listData property (defined by IDropInListItemRenderer) to locate and update the specific piece of data this CheckBox is rendering in our DataGrid.

The CenteredCheckBoxHeaderRender class is pretty similar to my previous version in that it still implements IFactory and has to be implemented as a ClassFactory instance. For details on using ClassFactory see the previous post referenced above. The main differences this time around are that the class is defined in ActionScript rather than MXML and streamlines things by using the built-in clickHandler function. Below is the portion of the class we will discuss further.

// these vars are used to reference the external property that stores our selected state
public var stateHost:Object;
public var stateProperty:String;

// set selected state based on external property
override public function set data(value:Object):void
{
    selected = stateHost[stateProperty];
}

// toggle external property on click
override protected function clickHandler(event:MouseEvent):void
{
    super.clickHandler(event);
    stateHost[stateProperty] = selected;
}

As discussed in the previous post, headerRenderers get initialized repeatedly during their existence. This means that we are unable to store any kind of state for them inside themselves because it will get reset over and over again. To work around this we define the stateHost and stateProperty variables which will be assigned via the properties property of ClassFactory. (Again, see previous post for a more thorough explanation of ClassFactory.) In our example application we point these to a selectAllFlag variable (the stateProperty) defined in the main application (the stateHost property). The overridden data setter shown above then uses this external value to set the selected state of our component. Correspondingly, clickHandler sets this value when our CheckBox is clicked, saving our state out to a safe, external location.

Two minor things to mention about our application are a couple of attributes set on the DataGridColumn that our renderers live in. First, notice that we set a dataField attribute like usual. This is required for our new itemRenderer class to work properly, whereas it was not needed when using ClassFactory for both the headerRenderer and itemRenderer. The second important point is that we have set the column's sortable attribute to false. This is necessary in order to ensure clicks in the header are processed as CheckBox clicks and not sorting actions. Also note that we process the header click and set the item values in the main file. While it would be possible to do this inside the header renderer class leaving it outside makes the renderer that much more flexible.

Hopefully this all makes sense, is not too long winded and provides renderers that others can and will use in future projects. Please post questions, complaints and comments in the... well... comments.

Creating truly reusable renderers with ClassFactory

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!

Item Renderers in DataGrids – A Primer for Predictable Behavior

ItemRenderers within DataGrids in Flex seem to be a topic that is often discussed and often struggled with. This is my attempt at a short and sweet explanation/example to get newcomers up-to-speed. This will also address the issues many people have with inconsistent rendering during scrolling and sorting of their DataGrid. Check out this example (right-click for source) and do as much clicking, scrolling and sorting as you want. Things should stay in the correct state regardless of how hard you bang on it.

There are three basic parts involved in successfully implementing IRs in DGs. The first is dispatching an event when the user interacts with the IR. This is a pretty straightforward step, but there do seem to be some differing opinions on exactly what and how information should be attached to the event. Let's look at my IR component, SimpleItemRenderer.mxml.

<?xml version="1.0" encoding="utf-8"?>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="100%" height="100%" horizontalAlign="center">
    <mx:Script>
        <![CDATA[
            // 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;
                    // set itemRenderer's state based on data value
                    cb.selected = value.isKnown;
                }
            }

            // 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 onto the event so that it can be manipulated in the handler
                // wherever that handler may be
                var evt:SimpleClickEvent = new SimpleClickEvent(super.data);
                dispatchEvent(evt);
            }
        ]]>
    </mx:Script>
    <mx:CheckBox id="cb" click="onClick(event)" width="15"/>
</mx:HBox>

As you can see, it is simply a CheckBox inside of an HBox. Using an HBox allows me to center the CheckBox and take advantage of other layout conveniences if I need to. I've declared a click handler for the CheckBox but its purpose is really just to package whatever data we need and pass it up the chain via dispatchEvent. You'll notice a reference to super.data, which refers to the item from your dataProvider that was passed to this row of the DataGrid. If we weren't worried about things like loose coupling we could simply update our data from here and be done with it. However, to promote modularity we will simply attach this data to our SimpleClickEvent and let it bubble up to a component that is better suited to handle the user interaction and any associated logic.

That better suited component is usually the container that is holding your DataGrid. Here is the event handler that will ultimately be fired when a user clicks on our IR's CheckBox:

// this is the function that will catch the click event
// that will bubble up from the Checkbox in our DataGrid's itemRenderer
private function onSimpleClickEvent(e:SimpleClickEvent):void
{
    // create refs to the items we care about
    var ir:SimpleItemRenderer = e.target as SimpleItemRenderer;
    var cb:CheckBox = ir.cb as CheckBox;
    var personVO:PersonVO = e.vo as PersonVO;

    // update the appropriate data property based on the CheckBox's selected state
    if(cb.selected)
    {
        personVO.isKnown = true;
    }
    else
    {
        personVO.isKnown = false;
    }
}

The important things to note here are that we are finally checking the type of our event's data payload, which is PersonVO. In our SimpleItemRenderer and SimpleClickEvent we avoided depending on any specific type so that they can be reused in other contexts. We then update the appropriate value in our dataProvider based on the state of our CheckBox. This is an essential step, as it records what the on-record value of this item should be for future reference.

The third and final step in our (very) basic implementation will be one such future reference and is necessary to maintain predictable behavior in the ItemRenderer, particularly if the DataGrid scrolls. This is because of the way DataGrids actually work behind-the-scenes. If your DataGrid's dataProvider has 100 items, but it is only big enough to display 10 at any given time, the DataGrid only actually draws 10 items, in order to maximize performance. When you scroll the DataGrid there aren't any new items being drawn, it is simply swapping the data properties between the already drawn items. So scrolling down one row means that the piece of data for the second item is switched to be the data for the first item, the second item receives the third item's data, and so on and so on. During these reassignments, things like whether or not the CheckBox should be checked get all messed up. Some get checked, some get unchecked... its completely unpredictable. To handle this problem, we must override the data's setter method and use our own logic to determine whether or not the CheckBox should be checked. Here is the relevant piece from SimpleItemRenderer (also shown above).

// 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;
        // set itemRenderer's state based on data value
        cb.selected = value.isKnown;
    }
}

Yes, this is somewhat tightly coupled to this particular usage since we are assuming value has an isKnown property. I haven't figured out the best way to get around this dependency yet but am leaning towards creating an interface or a generic superclass and having all of my ItemRenderers implement/extend it. To see the scrolling wackiness in action just comment out this method.

OK, so I lied. This definitely wasn't a short explanation, but hopefully it will prove thorough and clear enough to help someone else get their head around using ItemRenderers in Flex.