itemRenderers: Part 1: inline renderers

,,

I’m starting a new series of articles on itemRenderers. Our documentation team has great examples so please check that information out first. I’m giving you my distillation of it.

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 4th column of the 5th 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. BTW – when I say ‘we’ I really mean the Adobe Flex engineering team – I had nothing to do with it. Anyway, suppose you have 1000 records you want to show. If you think the list control creates 1000 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 through 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 were created, even after the list scrolled.

Here’s what we do. When the list is scrolled, those itemRenderers which 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.

If you want to change the background color of the cell in the 4th column of the 5th row, be aware that the itemRenderer for that cell may now be showing the contents of the 21st 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 we’ll look at the answer to this problem using inline itemRenderers. An inline itemRenderer is one which is written directly in the MXML file where the list control occurs. In the next article we’ll look at writing 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 we’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 which 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>

Let’s 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 complier you are defining a component inline. We’ll discuss what this really means in a bit.

Within the <mx:Component> tag 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 itemRenderer instance of its inline itemRenderer 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 a <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 <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 by 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

Let’s 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="[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>
</mx:Component>
</mx:itemRenderer>
</mx:TileList>

The itemRenderer looks like this when the application is run:

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 which 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, etc. 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. Afterall, 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; the receiver of the event won’t have to go hunt it down.

Summary

Using inline itemRenderers is a great and quick way to give your lists a custom look. Consider inline itemRenderers as separate ActionScript classes – afterall, 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.