itemRenderers: Part 3: Communication

,,

In the previous article 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 the first article: 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 which 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.

Dynamically Changing the itemRenderer

Here is the MXML itemRenderer from the previous article used for a TileList. We’re 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) which lets the user give a range of prices; all items which fall outside of the range should fade out (the itemRenderers’ alpha value should change). You need to tell each itemRenderer 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:

  • Part of the list itself. That is, your list (List, DataGrid, TileList, etc) 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, it is the MyTileList – my extension of TileList – which is the owner. 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 which gives access to the listData. Control component 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 our BookItemRenderer class:

  1. Have the class implement the interface.

    <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" ... implements="mx.controls.listClasses.IDropInListItemRenderer">
  2. 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, and 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 visual – the 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 TileList. The bubbling approach doesn’t assoicate the event (bookBuy) with the list control (TileList), 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.

First, you have to 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
{

Second, 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 );
}

}

Third, 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 fo 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.

Summary

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.