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.

20 Responses to itemRenderers: Part 3: Communication

  1. Peter – I am really enjoying this series and working with the Advanced Data Grid every day, I am putting your tips and knowledge to good use! Keep ’em coming!

  2. thibaud says:

    Great article! Thanks a lot, that really helped

  3. Marcus Stade says:

    Peter, nice documentation!

    However, assuming a certain type of list also tightly couples your renderer with that type of list. I wouldn’t agree that this is better than event bubbling, even if the renderer is specific. Also, I don’t really agree with components controlling when and where other components fire events. I’d say a component should have the responsibility of firing it’s own events and only listen to others.

  4. sasa says:

    what else!
    i now undestand the great use one can make with itemrenderers.

    great work!!!

  5. Demian Holmberg says:

    Peter, thank you very much for this series. I’ve been fighting my way through a custom itemRenderer for about a week – you information on IDropInListItemRenderer was the missing piece that finally made it work!

    Thanks again for all your efforts.

  6. thanks for clarifying some points. very concise.

    as I side note, listData.owner can be cast to ListBase or does not even need to be cast to dispatch an event as .owner already implements IUIComponent.
    so you can reuse your renderer in other lists without changing anything

    thx

  7. Shep Walkingstaff says:

    What about classes that have dataProvider properties? How do I implement a custom component that consumes data from an ArrayCollection?

    Many thanks,
    Shep

  8. Peter Ent says:

    Generally, your component should have a setter and getter for a “dataProvider” property. Make sure the component is bindable an emits events whenever the dataProvider setter has been called.

    You can always look at the Flex SDK source for any component which has a dataProvider for ideas.

  9. luiz says:

    Great tutorial, a lot of useful ideas.

    I have some difficulty putting those pieces together (probably because we do not see the main application file itself). The CatalogList refers to external BookItemRenderer as instructed but
    BookItemRenderer does not see the varialbe criteria in CatalogList although there is a public getter in CatalogList.

    So this expression :

    if( data.price < criteria ) alpha = 0.4;

    results in compiler message “Access of undefined property criteria.”

    Any hint will be appreciated.

  10. Peter Ent says:

    That was meant for you to put in your own criteria.

  11. Kyle says:

    Peter, I just wanted to take a moment and say thank you so much for writing this series. I have been struggling with really understanding item renderers and how they function, and after reading these posts I feel both enlightened and confident in my understanding of itemrenderers. Thank you!

  12. Daniel says:

    Thank you very much for this. It is very useful.

    One question I need to solve for the refresh of items: could you explain how I can set “criteria” from outside the item renderer? Outside I have only access to the main application and to the main container where the itemrenderer is passed as an argument.

    Other way could be, could you explain the second option you mention for doing this, that is, “Part of the application as global data”.

    Thanks so much again for the great series of tutorials.

  13. Peter Ent says:

    You could have the itemRenderer use a static member of a class. For example:

    public class FilterCriteria() {
    public static var name:String;
    public static var age:int;
    }

    The itemRenderer could look at FilterCriteria.name and FilterCriteria.age to determine what to do.

    You can make the criteria a public member of your Application class and reference it either statically as above, or via Application.application.

  14. Daniel says:

    Right, but in that case a change in the variable criteria won’t trigger a “refresh” (or a set data) in the renderer, right? I have exactly that, but my problem is about how to notify all item renderers that a change in criteria occurred so they redraw their border colors. This is exactly the point I am missing.

    In your series you suggest calling the invalidateList method, but for that I need a reference to the itemrenderers, which I don’t have from the main application.

    Sorry for the trouble, and thank you very much for your help.

  15. Peter Ent says:

    Try doing a refresh on the dataProvider:

    yourList.refresh()

    and that should trigger all of the filters, sorts, and itemRenderers.

  16. Daniel says:

    It seems I am posing a new challenge :-). The dataprovider I am using is a graph (I am using a graphical library) and it does not support a refresh.

    Is there no way to monitor a change in a global variable from the renderer? I have tried to make a bindable local variable in the renderer, set it to a global variable and then listen to the PropertyChange event, but such an event is not risen when I change the property from the application.

    Otherwise, is there any other way to do this?

    I just have a graph dataprovider in a roamer and I visualize different communities using the item renderer (just showing/hiding items but the dataprovider never changes). Depending on one external variable, some nodes should change the border color.

    Thanks once more for your patience and help.

  17. Daniel says:

    Ok. I finally solved it by defining a bindable global variable in the application and adding the following to the mxml of the item:

    borderColor=”{app.criteria ? COLOR_STRONG_NODE : COLOR_DEFAULT_NODE}”

    It does not call any method so it worked for my simple example. I don’t if in case I needed to call a function it would work too.

    Thanks once more for your support. Cheers,

    D.

  18. Mike says:

    Have you run acrossed the case where the call to super.commitProperties() fails? I’m getting it on line 323 of TileListItemRenderer, which consists of label.text = _listData.label;
    Any clues?
    tia
    Mike

  19. Peter Ent says:

    Yes, I’ve had many cases where super.commitProperties() fails. In this case, I would imagine that listData hasn’t been initialized yet. Perhaps another property setter is trying to set it or invalidateProperties() wasn’t called from a property setter function. It can be hard to trace, but look for the places which set and use listData.

  20. Jonathan says:

    Overriding the data setter method seems to throw the list into an infinite loop if the variableRowHeight property is set to true. I believe you should be able to test this with your example using a large enough data set to require scrolling. Oddly, this only seems to happen if the list is scrolled.