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.

itemRenderers: Part 2: External renderers

In Part 1 of this series I showed you how to make an inline itemRenderer. That is, an itemRenderer whose MXML tags and ActionScript code are in the same file as the list using the itemRenderer. The code is "in line" with the rest of the code in the file.

You’ll also recall that I said you should think of inline itemRenderers are being separate classes. The Flex compiler in fact extracts that inline code and makes a class for you. What we’re going to do in this article is make the class ourselves. The benefit of inline itemRenderers is that the code is in the same place as the list, but that’s also a drawback when the itemRenderer becomes complex.

Extracting the itemRenderer into an external file has several benefits:

  • The itemRenderer can easily be used in multiple lists;
  • the code is easier to maintain;
  • you can use Flex Builder’s Design View to sketch out the initial itemRenderer.

An MXML itemRenderer

From the previous article you saw there was a complex itemRenderer used for a DataGrid:

<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>

The itemRenderer is based on an HBox, contains an Image and a Text, and the background color is set according to the pubDate field of the item record. You can write this same itemRenderer as an external file using these steps:

  1. If you are using Flex Builder, create a new MXML Component file (I’ve named mine GridColumnSimpleRenderer, but use whatever you like) and set the root tag to be HBox. Don’t worry about the size.
  2. If you are using the SDK alone, create a new MXML file (call it GridColumnSimpleRenderer.mxml) and set the root tag to be HBox.
  3. With the file open, copy everything between <mx:HBox> and </mx:HBox>, but do not copy those tags since they are already in the file. The result should look something like this:
    <?xml version="1.0" encoding="utf-8"?>
    <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="300">
    <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>
  4. Save the file.

Now modify the DataGridColumn definition by removing the inline itemRenderer and replacing it with this:

<mx:DataGridColumn headerText="Title" dataField="title" itemRenderer="GridColumnSimpleRenderer">

Now run the application. You’ll get a surprise.

The surprise is how tall the rows are. That’s because of the presence of height="300" on the itemRenderer.

Determining an itemRenderer’s width and height

The list control always sets the itemRenderer’s width. In this example, the explicit width="400" is ignored. You should write your itemRenderer to assume the width will change as the user changes the column or list’s width.

The height is a different matter. If the list has an explicit rowHeight set, it will impose that height on each row, ignoring any height you’ve set on the itemRenderer. However, if you set the list’s variableRowHeight property to true, then the list will seriously consider the itemRenderer’s height. In this example, the height is explicitly set to 300, so each row is 300 pixel’s high.

To fix this, remove the explict height from the itemRenderer file and the application will work correctly.

Dynamically Changing the itemRenderer

In this example the set data function has been overridden to examine the data and set the itemRenderer’s backgroundColor. This is very common. Overriding set data allows you to intercept the time when the data is being changed for a new row and you can you make style changes.

Common mistakes are:

  • Forgetting to call super.data = value; this is VITAL – failure to do this will really mess up your itemRenderer;
  • Forgetting to reset the style(s) if any tests fail. It might be tempting to just set the color when the pubDate is in the future, but you have to remember that itemRenderers are recycled and so the else statement is very necessary.

An ActionScript itemRenderer

Now we’ll write another itemRenderer, this time using an ActionScript class. In the previous article there is a TileList with this inline itemRenderer:

	<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>

We’ll make that into an ActionScript, external, itemRenderer. You’ll need to follow these steps:

  1. Create a new ActionScript class. Call it BookTileRenderer.as and make it extend HBox, just like the inline itemRenderer.
    package
    {
    import flash.events.MouseEvent;
    
    import mx.containers.HBox;
    import mx.containers.VBox;
    import mx.controls.Button;
    import mx.controls.Image;
    import mx.controls.Label;
    import mx.controls.Spacer;
    import mx.controls.Text;
    
    public class BookTileRenderer extends HBox
    {
    public function BookTileRenderer()
    {
    super();
    }
    
    }
    }
  2. Create member variables to hold the references to the child components.
    		private var coverImage:Image;
    private var titleText:Text;
    private var spacer1:Spacer;
    private var authorLabel:Label;
    private var pubdateLabel:Label;
    private var spacer2:Spacer;
    private var buyButton:Button;
  3. Override the createChildren() function to create the child components and add them to the HBox.
    		override protected function createChildren():void
    {
    coverImage = new Image();
    addChild(coverImage);
    
    var innerBox:VBox = new VBox();
    innerBox.explicitHeight = 115;
    innerBox.percentWidth = 100;
    innerBox.setStyle("verticalAlign","top");
    innerBox.setStyle("verticalGap", 0);
    addChild(innerBox);
    
    titleText = new Text();
    titleText.setStyle("fontWeight","bold");
    titleText.percentWidth = 100;
    innerBox.addChild(titleText);
    
    spacer1 = new Spacer();
    spacer1.explicitHeight = 20;
    innerBox.addChild(spacer1);
    
    authorLabel = new Label();
    innerBox.addChild(authorLabel);
    
    pubdateLabel = new Label();
    innerBox.addChild(pubdateLabel);
    
    spacer2 = new Spacer();
    spacer2.percentHeight = 100;
    innerBox.addChild(spacer2);
    
    var buttonBox:HBox = new HBox();
    buttonBox.percentWidth = 100;
    buttonBox.setStyle("horizontalAlign","right");
    innerBox.addChild(buttonBox);
    
    buyButton = new Button();
    buyButton.label = "Buy";
    buyButton.setStyle("fillColors",[0x99ff99,0x99ff99]);
    buyButton.addEventListener(MouseEvent.CLICK, handleBuyClick);
    buttonBox.addChild(buyButton);
    }

     I’ve indented the code to show the parent-child relationships. Also, make sure you include an event listener on the Buy button.

  4. Override the commitProperties() function and set the user interface controls from the data.
    		override protected function commitProperties():void
    {
    super.commitProperties();
    
    coverImage.source = data.image;
    titleText.text = data.title;
    authorLabel.text = data.author;
    pubdateLabel.text = data.date;
    }
  5. Add the click event handler for the Buy button.
    		private function handleBuyClick( event:MouseEvent ) : void
    {
    var e:BuyBookEvent = new BuyBookEvent();
    e.bookData = data;
    dispatchEvent(e);
    }
  6. Modify the TileList in the main application to use the itemRenderer ActionScript class. Simply remove the inlineItemRenderer and replace it with an itemRenderer property right in the tag.
    <mx:TileList id="mylist" x="29" y="542" width="694" itemRenderer="BookTileRenderer"
    dataProvider="{testData.book}" height="232" columnWidth="275" rowHeight="135" >

If you are going to use an existing container class, such as HBox, I wouldn’t bother doing this in ActionScript. You can see it is more complex than using an MXML file and, quite frankly, there is little performance benefit to it.

Reusable itemRenderers

Here’s an example of an itemRenderer that displays a numeric value using the CurrencyFormatter. I call it PriceFormatter:

<?xml version="1.0" encoding="utf-8"?>
<mx:Text xmlns:mx="http://www.adobe.com/2006/mxml">

<mx:Script>
<![CDATA[
import mx.controls.dataGridClasses.DataGridListData;

[Bindable] private var formattedValue:String;

override public function set data(value:Object):void
{
super.data = value;

formattedValue = cfmt.format( Number(data[(listData as DataGridListData).dataField]) );
}
]]>
</mx:Script>

<mx:CurrencyFormatter precision="2" id="cfmt" />

<mx:text>{formattedValue}</mx:text>

</mx:Text>

The key to this itemRenderer is shown in red, setting the bindable variable, formattedValue. First, you’ll see that <mx:CurrentFormatter> was defined as an MXML tag (you can do this in ActionScript, too, if you prefer) with an id of cfmt. In the example above, the formattedValue is set to the result of calling the CurrentFormatter’s format() function.

The function takes a Number as its parameter type, so the value is cast to Number – that’s because the dataProvider for the list is XML and everything in XML is text; if you use a Object for your data and you have real numeric values, doing the Number cast will be harmless.

As you know, data is the property which holds the item being displayed by the itemRenderer. Using [ ] notation is another way of accessing the fields of the data item. For example, data['price'] would be the price column. But to make this itemRenderer resuable we cannot code for a specific field, so a more generic way is needed.

That’s where listData comes in. All Flex components which implement the IDropInListItemRenderer interface have a listData property.

Most controls such as Text, Label, Button, CheckBox, and so forth, implement IDropInListItemRenderer. Most containers, such as HBox, Canvas, etc. do not implement that interface. If you want to use listData in an itemRenderer that extends a Container you will have to implement IDropInListItemRenderer yourself – I’ll cover that in the next article.

The listData given to an itemRenderer contains, among other things, the rowIndex and the control which owns the itemRenderer – the DataGrid, List, or TileList. When you have an itemRenderer being used for the DataGrid, the listData is actually a DataGridListData object – which includes the columnIndex and the dataField associated with the DataGridColumn. Here’s the breakdown of the statement above, starting from the inside:

  • listData as DataGridListData - This casts the listData to a DataGridListData object so you have access to its dataField
  • .dataField - the field for the column being rendered. This is what makes this itemRenderer generic. You can use this itemRenderer for multiple columns. In this example the dataField is ‘price’.
  • data[ ... ] - This accesses the data for the specific field in the item. In this example it will be the price column.
  • Number( … ) - This casts the value to a Number because the format() function requires a Number parameter.
  • cfmt.format( … ) - This formats the value as a currency.

Summary

Use whatever makes you comfortable when implementing itemRenderers. Some people only work in ActionScript which is great when you’ve got experience with Flex and ActionScript. MXML makes quick work of simple itemRenderers, too.

In a future article we’ll look at making more efficient itemRenderers, which are ActionScript classes, but they extend UIComponent. In the next article I’ll discuss more communication between itemRenderers and the rest of the application.

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.

Migrating from Flex 2 to Flex 3

Check this link before migrating your applications from Flex 2 to Flex 3. In my own experience the issues have been very minor and fixed within a few minutes. But your results will varying depending on what controls you’ve used and how large your application is.

http://learn.adobe.com/wiki/display/Flex/Backwards+Compatibility+Issues

Adobe Releases Flex 3 and AIR

It’s finally here: Adobe Flex 3 has offically been released. Check out the press release for all the details. Be sure to check the Flex Product Page. You’ll find a product information, demos, tips, and more on the product pages.

If you have been using Flex 3 Beta – we can’t thank you enough for helping us make the product better. Your Flex 3 Beta 3 licenses should work for a little while longer.

Tip: If you have been using Flex Builder 2 PLUGIN for Eclipse 3.2, and want to move to Flex 3 PLUGIN, I suggest you uninstall the Flex Builder 2 PLUGIN, upgrade to Eclipse 3.3, then install the Flex Builder 3 PLUGIN – a couple more steps but the transition will be cleaner.

If you don’t intend to start using Flex 3 right away, it is still a good idea to move to Flex Builder 3. That’s because Flex Builder 3 can also handle projects created with the Flex 2 SDK. That’s right – Flex 3 can use multiple SDKs. This will allow you to continue to work on your Flex 2 projects AND allow you to explore Flex 3 and test your software with the new SDK.

Here are few more related announcements:

  • opensource.adobe.com/flex & opensource.adobe.com/blazeds projects are live. In addition, we are announcing that we are joining the SQLite Consortium (http://www.sqlite.org).
  • Big companies are launching production apps on Adobe AIR today, concurrent with our release. Press Release
  • 30onAIR – Join in the launch fun by recording a 30-second video on Why Flex, Why AIR or Why CF.

Enjoy!

Gauge Revisited

I received a comment on my recent blog article about the Gauge component wanting to know how to use a graphic skin for the gauge’s needle. I should have included that, but since I didn’t I thought I would supplement the article. As it turns out, you can’t just swap a graphic skin for the programmatic skin – in that version of the Gauge control. My bad.

It has to do with the rotation of the needle. I used the Flex Rotate effect to turn the needle and made it rotate the needle about the needle’s (0,0) point.

The problem with using (0,0) is that in a graphic, you cannot have negative pixels. To center the needle you’d have to draw half of it with negative pixel coordinates. That’s not possible, so I had to come up with another solution.

First, I changed my philosophy of using Rotate. Now, rather than rotating about (0,0) I let Rotate determine the center of the needle and rotate it about its own center point.

I then created another class for the Gauge called GaugeNeedle and it hosts the skin. The Gauge class creates an instance of GaugeNeedle, positions it at (0,0) – the upper left corner of the gauge, and sizes it to match the gauge. If the gauge face is a circle, picture the GaugeNeedle as floating in the center of a box with the box sized to fit the gauge’s face inside of it.

The needle skins are now drawn under the assumption that their centers are aligned with the center of the Gauge. If you were drawing a programmatic needle skin, you’d determine the center point and draw from there. Suppose the Gauge is 200×200 pixels. The GaugeNeedle will be sized to fit 200×200 also. So the needle skin should draw its base at 100,100 and draw the point to the right.

A graphic skin does much the same: you’d make a 200×200 image with a transparent background (PNG works best) and put the needle’s base at 100,100 and draw it pointing to the right.

The needle graphic is positioned so that its base is in the center of the image.

When the needlePlacement value is not "center", the box (GaugeNeedle) is positioned so its center point is shifted to where the needle’s base should be. For instance, if needlePlacement is "left", then the GaugeNeedle is not positioned at (0,0) but somewhere to the left of that so its center point is drawn closer to the left edge of the Gauge face.

The updated code is here for you to download.

I think you’ll find skinning this version of the Gauge a bit easier.

30 on AIR site launches

www.30onair.com site launches tomorrow – Feb 21 2008.

Revised Gauge Control

I’ve been looking over my past posts and thought it was time to update the old Gauge control I introduced way-back-when. In looking over that code I thought it was time for a re-write and a simplification.

This is the result:

This gauge control is simplified in a couple of ways:

  • First, there are just two parts: the face and the needle. Both of these are skins which you can change.
  • Second, the skins are directly ‘attached’ to the control. In the previous versions of this component I had created classes which then held the skins. I wanted to show you a more simplified approach and it works well here.

Continue reading…

creationPolicy vs Binding

I just read a comment on an article I wrote a few years ago, creationPolicy vs. Form Data. The commenter suggested that the Save button must be able to read the values from the controls, even if those controls haven’t been created – probably to make the code easier to write or at least consistent.

I disagree with this, but after reading that article again, I see that I could have given more information to support my position.

Continue reading…

DataCalendar

The DataCalendar is a combination of DateChooser and DataGrid. Like the DateChooser, the DataCalendar displays a standard calendar with controls to navigate to another month and year. And like the DataGrid, the cells of the DataCalendar display data.

Also similar to the DataGrid, the DataCalendar uses itemRenderers – either the default ones I’ve included with the package, or ones you create. The image below shows an example using custom itemRenderers and skins.

A long time ago I helped a friend of mine who owns a nightclub write an application which lets her booking manager create an online calendar of the bands performing at her nightclub. The central part of this application is the calendar which displays the bands playing each night, along with any notes and ticket information. I wrote all of that using PHP and I’ve been meaning to, over the years, write it again in Flex. The only thing holding me back was the calendar.

The DateChooser is fine to pick dates, but you cannot display anything other than the date in the calendar. The DataGrid is also fine but I needed something a bit more flexible. So I finally sat down, planned it out, and came up with the DataCalendar.

See a demo of DataCalendar here

The demonstration is of the nightclub booking tool. The information is stored in XML files and is available for the months of Dec 2007 through Feb 2008; I haven’t modified the program to create new months, but feel free to experiment with the code on your own.

Screen shot of the BookingTool

View Documentation

In hindsight, you can probably do this with the DataGrid. Once I had it planned I wanted to see it through, but if you wanted to do something similar with the DataGrid, I believe you could.

Source Code

These ZIP files are Flex 3 Beta 3 project archives.

DataCalendar Flex Library. This is the source to the DataCalendar control and supporting classes.
Test Application. This is the source to the test applications, including the BookingTool (above).

DataProvider

To use the DataCalendar you need a special data provider. I had toyed around with using the conventional ArrayCollection and XMLListCollection classes, but the more I thought about how someone would use this control, the more I thought I needed something specialized. When you look at a calendar, what do you see? Dates, right? And wouldn’t it make sense to orient the data to the dates of the calendar?

My first thought was to include the day of the month as one of the fields: {day:18, value:"Tim’s Birthday"}, {day:4, value:"Big Meeting"}, and so forth. The problem with this is that you have to repeatedly search through the collection for the information on a particular day. In this example, the information for the 4th comes after the information for the 18th. I could do a one-time pass and create a mapping, but that seemed like too much work and could have issues with data binding events.

Instead, I created the DataCalendarDataProvider class. The class implements ICollectionView, but it allows data to be assigned by day. For instance:

var januaryData:DataCalendarDataProvider = new DataCalendarDataProvider();
januaryData.addItemAt( 18, {value:"Tim's Birthday});
januaryData.addItemAt( 4, {value:"Big Meeting"});

The DataCalendarDataProvider class allocates 32 slots – slot 0 is not used – so each slot corresponds to a day of the month.

Certain properties such as sort and functions such addItem() do not make sense for a calendar, so those will throw exceptions if used.

Layout

The calendar is a grid of 7 columns and 6 rows (to accomodate those months which require 6 weeks, eg. March 2008). Regardless of the width, the DataCalendar makes all of the week day columns the same size. If you specify an explicit height the DataCalendar also makes all of the week rows the same height. However, if you do not specify a height, then the DataCalendar will allow each week row’s height to be different from the others, using the content of the day cells to determine a week row’s maximum height.

Renderers

I wanted this control to be as flexible as possible, so I gave it the ability to use itemRenderers. There are several renderers you can change:

itemRenderer: This is the renderer for the contents of a day cell. The default (DataCalendarItemRenderer) renderer takes the contents and displays it in a Text field. The BookingTool demonstration uses a complex itemRenderer. You can also add video and audio. Imagine showing the most popular YouTube™ video for each day of the month.
todayRenderer: This renderer displays the indicator for the day cell representing the current date. The default renderer simply displays the DataCalendarTodaySkin.
dayNumberRenderer: This renderer displays the date in the day cell. The default (DataCalendarDayNumberRenderer) displays the numerals for the date in the upper right corner of each day cell.
dayNameRenderer: This renderer displays the name of the day of the week across the top of each week day column. The default is DataCalendarDayNameRenderer.

Skins

I also wanted the control to have a great deal of flexability when it came to its look, so there are also a number of skins you can change:

DataCalendarDayNameSkin: the skin used for the background of the week day names.
DataCalendarDayNumberSkin: the skin used for the background of the dates for each day of the month (day cell).
DataCalendarSkin: the skin used for the background and border of the DataCalendar control.
DataCalendarTodaySkin: the skin used to show the indicator for the current day.

Version 2.0

Of course, once you finish a project you think about things you’d like to add or change. I may never get to this, but I can think of some "enhancement requests":

  • Scrolling like the DataGrid. That is, keep the DataCalendar’s title area and day names fixed in place and just scroll the day cells.
  • Selecting by day of the week. This would allow you to select all of the Mondays, for instance.
  • Adding itemEditor capability. This might make the BookingTool even easier to use.
  • Starting the week on some day other than Sunday. A typical business week begins Monday, for example.
  • Items which span rows or columns. For example, a vacation over a long weekend.