by Peter Ent
 
Peter Ent

Adobe

 

Created

28 July 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
 
Intermediate
 
Required products
 
Flex Builder 3 (Download trial)
 
Sample files
 
Flex provides a number of controls to display large amounts data in a variety of ways. There is the List control itself, the DataGrid, the Tree, and the visualization classes, which include the charts and the AdvancedDataGrid. By default, the Flex list controls display the data they are given as simple text. But Flex is capable of much more, and the list controls provide a way to customize their content using itemRenderers. By giving you complete control over the content of each row (or cell) of a list using itemRenderers, Flex enables you to write more engaging, more creative, and more useful applications than ever before.
 
This series discusses Flex itemRenderers and how to use them effectively and efficiently. The first part of the series focuses on inline itemRenderers, which are coded within the MXML tags describing the List control. Further articles explore more complex itemRenderers using both MXML and ActionScript.
 
 
 

 
Recycling Renderers

One thing many people try to do is access an itemRenderer from outside of the list. For example, you might want to make the cell in the fourth column of the fifth row in a DataGrid turn green because you've just received new data from the server. Getting that itemRenderer instance and modifying it externally would be a huge breech of the Flex framework and component model.
 
To understand itemRenderers, you have to understand why they are what they are and what our intentions were when we designed them. By the way, when I say "we," I really mean the Adobe Flex engineering team. I had nothing to do with it. Anyway, suppose you have 1,000 records you want to show. If you think the list control creates 1,000 itemRenderers, you are incorrect. If the list is showing only 10 rows, the list creates about 12 itemRenderers—enough to show every visible row, plus a couple for buffering and performance reasons. The list initially shows rows 1–10. When the user scrolls the list, it may now be showing rows 3–12. But those same 12 itemRenderers are still there: no new itemRenderers are created, even after the list scrolls.
 
Here's what Flex does. When the list is scrolled, those itemRenderers that will still be showing the same data (rows 3–10) are moved upward. Aside from being in a new location, they haven't changed. The itemRenderers that were showing the data for rows 1 and 2 are now moved below the itemRenderer for row 10. Then those itemRenderers are given the data for rows 11 and 12. In other words, unless you resize the list, those same itemRenderers are reused—recycled—to a new location and are now showing new data.
 
This behavior by Flex complicates the situation for certain programming scenarios. For instance, if you want to change the background color of the cell in the fourth column of the fifth row, be aware that the itemRenderer for that cell may now be showing the contents of the twenty-first row if the user has scrolled the list.
 
So how do you make changes like this?
 
The itemRenderers must change themselves based on the data they are given to show. If the itemRenderer for the list is supposed to change its color based on a value of the data, then it must look at the data it is given and change itself.
 
 
 

 
inline itemRenderers

In this article I'll present the answer to this problem using inline itemRenderers. An inline itemRenderer is one that is written directly in the MXML file where the list control occurs. In the next article I'll show you how to write external itemRenderers. The inline itemRenderers are the least complex and are generally used for very simple renderers or for prototyping a larger application. There's nothing wrong with inline itemRenderers, but when the code becomes complex, it is better to extract it into its own class.
 
In all of the examples I'll use the same data: a collection of information about books—author, title, publication date, thumbnail image, and so forth. Each record is an XML node that looks like this:
 
<book> <author>Peter F. Hamilton</author> <title>Pandora's Star</title> <image>assets/pandoras_star_.jpg</image> <date>Dec 3, 2004</date> </book>
I'll start with a simple itemRenderer using a <mx:List> control. Here, the author is listed followed by the title of the book.
 
<mx:List x="29" y="67" dataProvider="{testData.book}" width="286" height="190"> <mx:itemRenderer> <mx:Component> <mx:Label text="{data.author}: {data.title}" /> </mx:Component> </mx:itemRenderer> </mx:List>
 
 
This itemRenderer is so simple that a labelFunction would probably have been better, but it at least lets you focus on the important parts. First, an inline itemRenderer uses the <mx:itemRenderer> tag to define it. Within this tag is the <mx:Component> tag. This tag must be here, as it tells the Flex compiler you are defining a component inline. I'll describe what this really means in a bit.
 
Within the <mx:Component> tagm you define your itemRenderer. For this example it is a single <mx:Label> with its text field set to a data-binding expression: {data.author}: {data.title}. This is very important. The List control gives each itemRenderer instance the record of the dataProvider by setting the itemRenderer's data property. Looking at the code above, it means that for any given row of the list, the inline itemRenderer instance will have its data property set to a <book> XML node (such as the one above). As you scroll through the list, the data property is being changed as the itemRenderers are recycled for new rows.
 
In other words, the itemRenderer instance for row 1 might have its data.author set to "Peter F. Hamilton" now, but when it scrolls out of view, the itemRenderer will be recycled and the data property—for that same itemRenderer—may now have its data.author set to "J.K. Rowling". All of this happens automatically as the list scrolls—you don't worry about it.
 
Here's a more complex inline itemRenderer using the <mx:List> control again:
 
<mx:List x="372" y="67" width="351" height="190" variableRowHeight="true" dataProvider="{testData.book}"> <mx:itemRenderer> <mx:Component> <mx:HBox > <mx:Image source="{data.image}" width="50" height="50" scaleContent="true" /> <mx:Label text="{data.author}" width="125" /> <mx:Text text="{data.title}" width="100%" /> </mx:HBox> </mx:Component> </mx:itemRenderer> </mx:List>
 
 
This really isn't much different. Instead of a <mx:Label> , the itemRenderer is an <mx:HBox> with an <mx:Image>, <mx:Label>, and an <mx:Text> control. Data-binding still relates the visual with the record.
 

 
DataGrid

You can use inline itemRenderers on a DataGrid, too. Here's one applied to a column:
 
<mx:DataGrid x="29" y="303" width="694" height="190" dataProvider="{testData.book}" variableRowHeight="true"> <mx:columns> <mx:DataGridColumn headerText="Pub Date" dataField="date" width="85" /> <mx:DataGridColumn headerText="Author" dataField="author" width="125"/> <mx:DataGridColumn headerText="Title" dataField="title"> <mx:itemRenderer> <mx:Component> <mx:HBox paddingLeft="2"> <mx:Script> <![CDATA[ override public function set data( value:Object ) : void { super.data = value; var today:Number = (new Date()).time; var pubDate:Number = Date.parse(data.date); if( pubDate > today ) setStyle("backgroundColor",0xff99ff); else setStyle("backgroundColor",0xffffff); } ]]> </mx:Script> <mx:Image source="{data.image}" width="50" height="50" scaleContent="true" /> <mx:Text width="100%" text="{data.title}" /> </mx:HBox> </mx:Component> </mx:itemRenderer> </mx:DataGridColumn> </mx:columns> </mx:DataGrid>
 
 
As you can see, this is much more complex than the last two, but it has the same structure: <mx:itemRenderer> with an <mx:Component> definition inside of it.
 
The purpose of <mx:Component> is to provide an MXML syntax for creating an ActionScript class right in the code. Picture the code that appears in the <mx:Component> block being cut out and put into a separate file and given a class name. When you look at the inline itemRenderer, it does look like a complete MXML file, doesn't it? There's the root tag ( <mx:HBox> in this case) and even a <mx:Script> block.
 
The purpose of the <mx:Script> block in this example is to override the set data function so the background color of the itemRenderer can be changed. In this case, the background is changed from white whenever the publication data for a book is in the future. Remember that itemRenderers are recycled, so the color must also be set back to white if the test fails. Otherwise, all of the itemRenderers will eventually turn purple as the user scrolls through the list.
 

 
outerDocument

The scope has also changed. What I mean is, variables that you define from within a <mx:Component> are only scoped to that component/inline itemRenderer. Likewise, the content outside of the <mx:Component> is in a different scope, just as if this component were defined in a separate file. For instance, suppose you add a Button to this itemRenderer that allows the user to buy the book from an online retailer. Buttons call functions on their click event, so you might define the button like this:
 
<mx:Button label="Buy" click="buyBook(data)" />
If the buyBook() function were defined in the <mx:Script> block of the file, you would get an error saying that buyBook() is an undefined method. That's because buyBook() is defined in the scope of the file, not in the scope of the <mx:Component>. Since this is a typical use case, there is a way around that using the outerDocument identifier:
 
<mx:Button label="Buy" click="outerDocument.buyBook(data)" />
The outerDocument identifier changes the scope to look into the file, or outer document, with reference to the <mx:Component>. Now beware: the function has to be a public function, not a protected or private one. Remember that <mx:Component> is treated as an externally defined class.
 

 
Bubbling Events

Now look at another, even more complex example. This is a TileList using the same data.
 
<mx:TileList x="29" y="542" width="694" dataProvider="{testData.book}" height="232" columnWidth="275" rowHeight="135" > <mx:itemRenderer> <mx:Component> <mx:HBox verticalAlign="top"> <mx:Image 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="[0×99ff99,0×99ff99]"> <mx:click> <mx:Script> <![CDATA[ var e:BuyBookEvent = new BuyBookEvent(); e.bookData = data; dispatchEvent(e); ]]> </mx:Script> </mx:click> </mx:Button> </mx:HBox> </mx:VBox> </mx:HBox> </mx:Component> </mx:itemRenderer> </mx:TileList>
 
 
The itemRenderer looks like Figure 1 when the application is run:
 
An itemRenderer implemented in a TileList.
Figure 1. An itemRenderer implemented in a TileList.
 
 
This itemRenderer is pretty close to the one used in the DataGrid, but the Buy button's click event doesn't use outerDocument to call a function. In this case the click event creates a custom event that bubbles up out of the itemRenderer, through the TileList, and is received by some higher component in the visual chain.
 
This is a very common problem: you have an itemRenderer which has some interactive control in it, usually a Button, LinkButton, or other component that is supposed to cause some action to take place when clicked. Perhaps it is to delete the row or in this case, buy the book.
 
It is unreasonable to expect the itemRenderer to do the work. After all, the itemRenderer's job is to make the list look good—period. Event bubbling allows the itemRenderer to pass off the work to something else. A custom event is useful here because the event is related to the data on the row; so why not include that data in the event? That way, the receiver of the event won't have to go hunt it down.
 

 
Conclusion

Using inline itemRenderers is a great and quick way to give your lists a custom look. Consider inline itemRenderers as separate ActionScript classes—after all, they are scoped as if they were. If you must refer to functions or properties in the containing file, use the outerDocument identifier to change the scope. If you need to communicate information as the result of an interaction with the itemRenderer, use a custom, bubbling event.
 
And remember: don't try to get hold of itemRenderers—they are recycled for a purpose. Make them responsible only to the data given to them.
 
In the next article I'll discuss external itemRenderers.
 

 
Where to go from here

Read Part 2 of the series for external itemRenderers.