by Peter Ent

Peter Ent

Table of contents

Created

22 September 2008

Requirements

Prerequisite knowledge

To benefit most from this article, it is best if you are familiar with Flex Builder and ActionScript 3.0.

 

User level

Beginning

Required products

Flex Builder 3 (Download trial)

In Part 2 of this series, I showed you how to make external itemRenderers in both MXML and ActionScript. In the examples I've been using, there is a Button which dispatches a custom event—BuyBookEvent—so the application can react to it. This article covers communication with itemRenderers in more detail.
 
There is a rule I firmly believe must never be violated: you should not get hold of an instance of an itemRenderer and change it (setting public properties) or call its public methods. This, to me, is a big no-no. The itemRenderers are hard to get at for a reason, which I talked about in Part 1: the itemRenderers are recycled. Grabbing one breaks the Flex framework.
 
With that rule in mind, here are things you can do with an itemRenderer:
 
  • An itemRenderer can dispatch events via its list owner. (You've seen bubbling; this is a better practice, which you'll see below.)
  • An itemRenderer can use static class members. This includes Application.application. If you have values stored "globally" on your application object, you can reach them that way.
  • An itemRenderer can use public members of the list that owns it. You'll see this below.
  • An itemRenderer can use anything in the data record. You might, for example, have an item in a record that's not for direct display, but which influences how an itemRenderer behaves.
This series includes the following articles:
 

 
Dynamically changing the itemRenderer

Here is the MXML itemRenderer from the previous article used for a TileList. I'm going to make it bit more dynamic by having it react to changes from an external source (I called this file BookItemRenderer.mxml):
 
<?xml version="1.0" encoding="utf-8"?> <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="250" height="115" > <mx:Script> <![CDATA[ ]]> </mx:Script> <mx:Image id="bookImage" source="{data.image}" /> <mx:VBox height="115" verticalAlign="top" verticalGap="0"> <mx:Text text="{data.title}" fontWeight="bold" width="100%"/> <mx:Spacer height="20" /> <mx:Label text="{data.author}" /> <mx:Label text="Available {data.date}" /> <mx:Spacer height="100%" /> <mx:HBox width="100%" horizontalAlign="right"> <mx:Button label="Buy" fillColors="[0x99ff99,0x99ff99]"> <mx:click> <![CDATA[ var e:BuyBookEvent = new BuyBookEvent(); e.bookData = data; dispatchEvent(e); ]]> </mx:click> </mx:Button> </mx:HBox> </mx:VBox> </mx:HBox>
Suppose you are showing a catalog of items in a TileList. You also have a Slider (not part of the itemRenderer) that lets the user give a range of prices; all items that fall outside of the range should fade out (the itemRenderers' alpha value should change). You need to tell all the itemRenderers that the criteria has changed so that they can modify their alpha values.
 
Your override of set data might look something like this:
 
override public function set data( value:Object ) : void { super.data = value; if( data.price < criteria ) alpha = 0.4; else alpha = 1; }
The question is: how to change the value for criteria? The "best practice" for itemRenderers is to always have them work on the data they are given. In this case, it is unlikely, and impractical, to have the test criteria be part of the data. So that leaves a location outside of the data. You've got two choices:
 
  • Part of the list itself. That is, your list (List, DataGrid, TileList, or other) could be a class that extends a list control and which has this criteria as a public member.
  • Part of the application as global data.
For me, the choice is the first one: extend a class and make the criteria part of that class. After all, the class is being used to display the data, the criteria is part of that display. For this example, I would extend TileList and have the criteria as a public data member.
 
package { import mx.controls.TileList; public class CatalogList extends TileList { public function CatalogList() { super(); } private var _criteria:Number = 10; public function get critera() : Number { return _criteria; } public function set criteria( value:Number ) : void { _criteria = value; } } }
The idea is that a control outside of the itemRenderer can modify the criteria by changing this public property on the list control.
 

 
listData

The itemRenderers have access to another piece of data: information about the list itself and which row and column (if in a column-oriented control) they are rendering. This is known as listData and it could be used like this in the BookItemRenderer.mxml itemRenderer example:
 
override public function set data( value:Object ) : void { super.data = value; var criteria:Number = (listData.owner as MyTileList).criteria; if( data.price < criteria ) alpha = 0.4; else alpha = 1; }
Place this code into the <mx:Script> block in the example BooktItemRenderer.mxml code, above.
 
The listData property of the itemRenderer has an owner field, which is the control to which the itemRenderer belongs. In this example, the owner is the MyTileList control—my extension of TileList. Casting the owner field to MyTileList allows the criteria to be fetched.
 
 
IDropInListItemRenderer
Access to listData is available when the itemRenderer class implements the IDropInListItemRenderer interface. Unfortunately, UI container components do not implement the interface that gives access to the listData. Control components such as Button and Label do, but for containers you have to implement the interface yourself.
 
Implementing this interface is straightforward and found in the Flex documentation. Here's what you have to do for the BookItemRenderer class:
 
  1. Have the class implement the interface.
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" ... implements="mx.controls.listClasses.IDropInListItemRenderer">
  1. Add the set and get functions to the <mx:Script> block in the itemRenderer file.
import mx.controls.listClasses.BaseListData; private var _listData:BaseListData; public function get listData() : BaseListData { return _listData; } public function set listData( value:BaseListData ) : void { _listData = value; }
When the list control sees that the itemRenderer implements the IDropInListItemRenderer interface, it will create a listData item and assign it to every itemRenderer instance.
 
 
invalidateList()
Setting the criteria in my class isn't as simple as assigning a value. Doing that won't tell the Flex framework that the data has been changed. The change to the criteria must trigger an event. Here's the modification to the set criteria() function:
 
public function set criteria( value:Number ) : void { _criteria = value; invalidateList(); }
Notice that once the _criteria value has been set, it calls invalidateList(). This causes all of the itemRenderers to be reset with values from the dataProvider and have their set data functions called.
 
The process then looks like this:
 
  1. The itemRenderer looks into its list owner for the criteria to use to help it determine how to render the data.
  2. The list owner class, an extension of one of the Flex list classes, contains public properties read by the itemRenderer(s) and set by external code—another control or ActionScript code (perhaps as the result of receiving data from a remote call).
  3. When the list's property is set, it calls the list's invalidateList() method. This triggers a refresh of the itemRenderers, causing them to have their data reset (and back to step 1).

 
Events

In the previous articles I showed how to use event bubbling to let the itemRenderer communicate with the rest of the application. I think this is certainly quick. But I also think there is a better way, one which fits the assumption that an itemRenderer's job is to present data and the control's job is to handle the data.
 
The idea of the MyTileList control is that it is the visible view—of the catalog of books for sale. When a user picks a book and wants to buy it, it should be the responsibility of the list control to communicate that information to the application. In other words:
 
<CatalogList bookBuy="addToCart(event)" />
 
The way things are set up right now, the event bubbles up and bypasses the MyTileList. The bubbling approach doesn't associate the event (bookBuy) with the list control (MyTileList), allowing you to move the control to other parts of your application. For instance, if you code the event listener for bookBuy on the main Application, you won't be able to move the list control to another part of the application. You'll have to move that event handler, too. If, on the other hand, you have the event associated with the control, you just move the control.
 
Look at it this way: suppose the click event on the Button wasn't actually an event dispatched by the Button, but bubbled up from something inside of the button. You'd never be able to do: <mx:Button click="doLogin()" label="Log in" />; you would have to put the doLogin() function someplace else, and that would make the application even harder to use.
 
I hope I've convinced you, so here's how to change the example from bubbling to dispatching from the list control.
 
  1. Add metadata to the CatalogList control to let the compiler know the control dispatches the event:
import events.BuyBookEvent; import mx.controls.TileList; [Event(name="buyBook",type="events.BuyBookEvent")] public class CatalogList extends TileList {
  1. Add a function to CatalogList to dispatch the event. This function will be called by the itemRenderer instances:
public function dispatchBuyEvent( item:Object ) : void { var event:BuyBookEvent = new BuyBookEvent(); event.bookData = item; dispatchEvent( event ); } }
  1. Change the Buy button code in the itemRenderer to invoke the function:
<mx:Button label="Buy" fillColors="[0x99ff99,0x99ff99]"> <mx:click> <![CDATA[ (listData.owner as CatalogList).dispatchBuyEvent(data); ]]> </mx:click> </mx:Button>
Now the Button in the itemRenderer can simply invoke a function in the list control with the data for the record (or anything else that is appropriate for the action) and pass the responsibility of interfacing with the rest of the application onto the list control.
 
The list control in this example dispatches an event with the data. The application can add event listeners for this event either using ActionScript or, because of the [Event] metadata in the CatalogList.as file, MXML; using [Event] metadata makes it easier for developers to use your code.
 

 
Where to go from here

itemRenderers should communicate any actions using events. Custom events allow you to pass information with the event so the consumer of the event doesn't have to reach out to the itemRenderer for any data.
 
itemRenderers should "react" to changes in data by overriding their set data functions. Inside of the function they can access values in their listData.owner. They could also access data stored in a static class or in the main application via Application.application.
 
In the next article we'll look at incorporating states into itemRenders.