Pages

June 27, 2009

Item Renderers in Practice

Posted by Jeremy Mitchell
List-based controls (DataGrid, List, TileList & HorizontalList) render the data assigned to their dataProvider property.

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
    layout="vertical">

    <mx:ArrayCollection id="ac">
        <mx:Array>
            <mx:Object name="tom" age="12" hair="brown"/>
            <mx:Object name="sally" age="21" hair="green"/>
            <mx:Object name="kate" age="16" hair="yellow"/>
            <mx:Object name="bill" age="22" hair="brown"/>
            <mx:Object name="chriss" age="23" hair="green"/>
            <mx:Object name="yarb" age="34" hair="yellow"/>
        </mx:Array>
    </mx:ArrayCollection>
    
    <mx:List id="myList"
        dataProvider="{ac}"
        labelField="name"/>

</mx:Application>
With the help of an Item Renderer, list-based controls can customize the appearance of the data provider's data.

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
    layout="vertical">
    
    <mx:ArrayCollection id="ac">
        <mx:Array>
            <mx:Object name="tom" age="12" hair="brown"/>
            <mx:Object name="sally" age="21" hair="green"/>
            <mx:Object name="kate" age="16" hair="yellow"/>
            <mx:Object name="bill" age="22" hair="brown"/>
            <mx:Object name="chriss" age="23" hair="green"/>
            <mx:Object name="yarb" age="34" hair="yellow"/>
        </mx:Array>
    </mx:ArrayCollection>
    
    <!-- Using an Item Renderer to control the display of data -->
    <mx:List width="100%"
        dataProvider="{ac}"
        itemRenderer="com.flexdevelopers.ListIR"/>

</mx:Application>

<!-- Item Renderer (ListIR.mxml-->

<mx:Label xmlns:mx="http://www.adobe.com/2006/mxml"
    text="{data.name + ' (' + data.age + ')'}"
    color="0xFF0000"/>
In addition, the custom appearance can be controlled by conditional logic processed by an Item Renderer. We'll look at this a bit later.

A single instance of your Item Renderer class (ListIR) is created for each visible item of the list-based control.



As the user scrolls through the items of a list-based control, Item Renderer instances are recycled rather than creating new instances. This is a concept known as virtualization.

Example: When Data Item #1 is removed from the list-based control's display due to a scrolling action, the Item Renderer instance previously used to render Data Item #1 is recycled and used to display the data for Data Item #6.



Item Renderers must directly or indirectly (through class inheritance) implement the IDataRenderer interface which enforces the implementation of 2 very important methods: get data() & set data(). With an implicit getter and setter in place for the data property, the Item Renderer instance can receive a data object passed from the host component (the list-based control) which represents the data for the item it is responsible to render (i.e. Data Item #6).

Example: If instance #1 of the Item Renderer is rendering the data for Data Item #6, the data for Data Item #6 is passed to instance #1 of the Item Renderer via the Item Renderer's set data setter method.
When an instance of an Item Renderer is recycled due to scrolling, the set data method is called and the applicable data object is passed. This is the point in which conditional logic can be applied to control the appearance of an Item Renderer.

<!-- Item Renderer (ListIR.mxmlwith conditional logic -->

<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml">
    <mx:Script>
        <![CDATA[
            private var minAge:Number = 21;
                        
            override public function set data(value:Object):void {
                super.data = value; // set the Item Renderer's data object
                // inject additional logic
                if (data.age < minAge) {
                    listLabel.setStyle("color",0xFF0000);
                else {
                    listLabel.setStyle("color",0x000000)// default value
                }
            }
        ]]>
    </mx:Script>
    <mx:Label id="listLabel"
        text="{data.name + ' (' + data.age + ')'}"/>
</mx:HBox>
Note: It is important to remember that Item Renderers are recycled and as such, their property values may reflect a condition met by the previous "user" of the Item Renderer instance (i.e. the Data Item). Therefore, it is important to reset properties to the default value when conditional logic fails. In the previous example, the reset is accomplished by setting the label's font color back to black (0x000000) when data.age >= 21.
In the previous example, we utilized a property of the data object (data.age) and a hard-coded value (21) to dictate the item's appearance. However, certain scenarios may require access to property values outside of an Item Renderer's scope to determine the appearance.

Example: The value of an HSlider in conjunction with the age property of the data object (data.age) should dictate the font color of each item's label.
This scenario requires the following:

1. Create a new public property for the list-based control exposed via a getter and setter

By extending an existing class and adding a new public property, our list-based control will have a place to store the value of an external property (i.e. the value of the HSlider's value property).

// MyList.as extends List

package com.flexdevelopers
{
    import mx.controls.List;

    public class MyList extends List
    {
        private var _minAge:Number; // new property
        
        public function MyList()
        {
            super();
        }

        // new property getter and setter methods
       
        public function get minAge():Number {
            return _minAge;
        }
        
        public function set minAge(value:Number):void {
            _minAge = value;
        }
        
    }
}

2. Bind the new list-based control's public property (MyList.minAge) to the value of the desired external property utilizing Flex's data-binding utility

Data-binding will provide real-time synchronization of the new public property (minAge) with the external property's value (i.e. the HSlider's value property)

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:comps="com.flexdevelopers.*"
    layout="vertical">
    
    <mx:Script>
        <![CDATA[
            import com.flexdevelopers.MyList;
        ]]>
    </mx:Script>

    <mx:ArrayCollection id="ac">
        <mx:Array>
            <mx:Object name="tom" age="12" hair="brown"/>
            <mx:Object name="sally" age="21" hair="green"/>
            <mx:Object name="kate" age="16" hair="yellow"/>
            <mx:Object name="bill" age="22" hair="brown"/>
            <mx:Object name="chriss" age="23" hair="green"/>
            <mx:Object name="yarb" age="34" hair="yellow"/>
        </mx:Array>
    </mx:ArrayCollection>
    
    <comps:MyList width="100%"
        dataProvider="{ac}"
        itemRenderer="com.flexdevelopers.ListIR"
        minAge="{hslider.value}"/> <!-- minAge is bound to hslider.value -->
    
    <mx:HBox>

        <mx:HSlider id="hslider"
            minimum="10" maximum="50"
            value="20"
            snapInterval="1"
            liveDragging="true"/>
            
        <mx:Label text="Minimum Drinking Age: {hslider.value}"/>
        
    </mx:HBox>

</mx:Application>

3. Provide the Item Renderer with access to a ListData object

To access the value of our new public property from within an Item Renderer instance, our Item Renderer must implement the IDropInListItemRenderer interface. This ensures a reference to the Item Renderer's owner (the list-based control) is available to the Item Renderer, and consequently, so is our new public property.

<!-- ListIR.mxml -->

<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml"
       implements="mx.controls.listClasses.IDropInListItemRenderer">
    <mx:Script>
        <![CDATA[
            import mx.controls.listClasses.BaseListData;
            import mx.controls.listClasses.ListData;
            import com.flexdevelopers.MyList;
            
            // property required of IDropInListItemRenderer exposed thru a public getter & setter
            private var _listData:BaseListData;
            
            // getter & setter methods
            public function get listData():BaseListData {
                return _listData;
            }
            
            public function set listData(value:BaseListData):void {
                _listData = value;
            }
            
            override public function set data(value:Object):void {
                super.data = value;
                // retrieve the value of the list-based control's new public property
                var minAge:Number = (listData.owner as MyList).minAge;
                if (data.age < minAge) {
                    listLabel.setStyle("color",0xFF0000);
                else {
                    listLabel.setStyle("color",0x000000);
                }
            }
        ]]>
    </mx:Script>
    
    <mx:Label id="listLabel"
        text="{data.name + ' (' + data.age + ')'}"/>
    
</mx:HBox>

4. Make a call to the invalidateList method when the source of our new, bounded public property is changed (i.e. the HSlider value property changes)

By calling MyList.invalidateList() each time the HSlider value changes, we ensure that the conditional logic of the Item Renderer is re-evaluated. InvalidateList forces each Item Renderer instance of a list-based control to call the data setter method.

// update the new property's setter method to include invalidateList()
// IMPORTANT: Without calling invalidateList, a change to the HSlider will not impact 
// the appearance of the currently visible list items

public function set minAge(value:Number):void {
   _minAge = value;
   invalidateList();
}

See it in action (view source is enabled):