itemRenderer パート2 : external itemRenderer

itemRendererシリーズのPart1では、inline itemRenderer について説明しました。
inline itemRendererとは、MXML タグと ActionScript のコードが itemRenderer を適用するコンポーネントと同じファイルに存在するものです。

inline itemRenderer は別ファイルで定義されたクラスと同等だと説明しましたよね。
実際にFlex コンパイラは内部的に、インラインで書かれたコードを抜き出してクラスを作成しています。

今回の記事では、自分でそのクラスを作ってみます。
inline itemRendererの利点は、itemRendererを適用するコンポーネントと同じ個所に記述できるというものですが、itemRendererが複雑になってしまったときに読みづらいという欠点にもなります。

itemRenderer を外部ファイルに切り出すことには、いくつかの利点があります。
・itemRendererを複数のリストで利用できる
・メンテナンス性に優れる
・Flex Builderのデザイン・ビューで大枠をデザインすることができる

■An MXML itemRenderer

前回の記事では、DataGrid用に少々複雑なitemRendererを紹介しました。

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

itemRendererの基本はHBoxであり、Image とText を含んでいます。背景色は item レコードの pubDate フィールドの値によって決まります。
これと同じitemRendererを外部ファイルとして書く方法を説明しましょう。

1. もし Flex Builder を使っているのなら、新規 MXML コンポーネントを作成してください。(コンポーネント名は GridColumnSimpleRenderer とします。好きな名前を付けてもらって結構です。) ルートタグは HBox とします。サイズは気にする必要はありません。
2. もし SDK を使っているのなら、新規 MXML ファイルを作成してください。ファイル名は GridColumnSimpleRenderer.mxml としました。ルートタグは HBox とします。
3. 新規作成したファイルに上記コードの <mx:HBox>タグ内を全てコピーしてください。( <mx:HBox>タグ自体は新規作成したファイルに存在するものなのでコピーする必要はありません。 )

結果は以下のようになります。

<?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. ファイルを保存します。

DataGridColumn の定義からインライン itemRenderer を削除して、下記コードと入れ替えます。

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

アプリケーションを実行してみると、おっと..。DataGridの行の縦サイズが大きすぎますね。
これは itemRenderer の height プロパティに 300 が指定されているからです。

■ itemRenderer の width と height について

List コントロールは itemRenderer の width を自動で調節します。従って、今回の例の width=”400″ という指定は無視されます。
ユーザがカラムやリストの幅を変更するとitemRenderer の幅も変わってしまうのを考慮して、itemRendererを実装してください。

heightに関しては話が別です。もし List の rowHeight プロパティに明示的に値を指定すると、全ての行がその指定した高さになり、itemRenderer で指定した height は無視されます。
しかし、List の variableRowHeight プロパティに true をセットすると、itemRenderer に指定した height が優先されます。
今回の例では height に 300 をセットしているので、各行の高さは 300 ピクセルになります。

itemRendererから明示的に値を指定しているheightを削除することにより、正常に表示されるようになります。

■ Dynamically Changing the itemRenderer

今回の例では set data メソッドをオーバーライドし itemRenderer の背景色の変更する処理を行っています。
これはよく使う方法です。
set data メソッドをオーバーライドすることにより、新しい行のデータが data プロパティにセットされたタイミングで独自の処理を行わせることができます。(今回の例では、styleを変更しています)

よくあるミスとして:

・super.data = value; の記述を忘れてしまう。 これはとても重要です。この記述を忘れてしまうと itemRenderer の挙動がおかしくなってしまいます。

・条件に合致しなかったときに styleを戻す処理を忘れてしまう。 今回の例では、pubDate が未来の日付の時だけ背景色を変更し、過去の日付の時に背景色をデフォルトに戻し忘れてしまう。
itemRendererは再利用されるので、必ずデフォルトに戻すといった処理を含めてください。

■ ActionScript itemRenderer

さて、もう一つ itemRenderer を書いてみましょう。今度は ActionScript のクラスを使います。
前回の記事で、次のようなインライン itemRenderer を持つ TileList を使用しました:

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

この itemRenderer を外部ファイルの ActionScript で記述します。 以下のステップに従って進めてください。

1. ActionScript クラスの作成. クラス名は BookTileRenderer にし、インラインitemRendererと同様 HBox を継承します。

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. 子コンポーネントへの参照を保持するメンバ変数を定義します。

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. createChildren() メソッドをオーバーライドし、子コンポーネントをインスタンス化し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);
}

親と子の関係が分かりやすくなるように、インデントを入れてみました。 Buyボタンへのクリック・イベントのリスナーの追加も忘れずに行ってください。

4. commitProperties() メソッドをオーバーライドし、子コンポーネントに 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. Buyボタンのクリック・イベントのハンドラーを追加します。

private function handleBuyClick( event:MouseEvent ) : void
{
var e:BuyBookEvent = new BuyBookEvent();
e.bookData = data;
dispatchEvent(e);
}

6. メインのアプリケーションの TileList の itemRenderer を、 ActionScript で記述したものに変更します。
inline itemRenderer を削除して、TileList タグ内の itemRenderer プロパティに新たな itemRenderer のクラス名を記述します。

<mx:TileList id="mylist" x="29" y="542" width="694" itemRenderer="BookTileRenderer" 
dataProvider="{testData.book}" height="232" columnWidth="275" rowHeight="135" >

もしFlexの既存のコンテナクラス(HBoxなど)を基底クラスとするのであれば、私はわざわざ ActionScript を用いてitemRendererを作ろうとはしません。
見て分かる通りMXMLで作成したものと比べてとても複雑になりますし、実のところパフォーマンスもそれほどよくなりませんから。

■ 再利用可能な itemRenderer

次のコードは、CurrencyFormatterを用いて数字を表示する itemRenderer です。ファイル名を PriceFormatter.mxml とします。

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

この itemRenderer のポイントは赤字の部分です。ここで、バインダブルな formattedValue 変数をセットしています。
まず、cfmt という id の <mx:CurrentFormatter>タグがあるのが分かると思います(ここも ActionScriptで記述しても構いません)。
上記のコードでは CurrentFormatter の format() メソッドを呼び出した結果を formattedValue にセットしています。

format() メソッドは引数にNumber型のオブジェクトを取るので、引数をNumberにキャストしています。
なぜなら、リストのdataProviderはXMLであり、XMLに含まれるものは全てテキストだからです。
data として Object を使えば、これを数値型の値にすることができますが、Number型にキャストして問題になることはありません。

ご存じの通り、data とは itemRenderer で表示する値を保持した単なるプロパティです。よって、[ ] 表記を用いてデータのフィールドにアクセスすることも可能です。
例えば、data[‘price’] は金額のカラムです。
しかし、itemRenderer を再利用するには、特定のカラム名を指定するのではなく、もっと汎用的なデータの指定方法が必要となります。

そこで listData の登場です。IDropInListItemRenderer インターフェイスを実装する全ての Flex コンポーネントは、listData プロパティを持っています。

Text, Label, Button, CheckBox などほぼ全てのコントールは IDropInListItemRenderer インターフェイスを実装しています。
一方、HBox, Canvas などほぼ全てのコンテナは、IDropInListItemRenderer インターフェイスを実装していません。
もしコンテナクラスを継承した itemRenderer で listData を使いたければ、自前でIDropInListItemRendererを実装しなければなりません。
これについては次回の記事で説明します。

itemRenderer に与えられた listData には、rowIndex プロパティや、自身(itemRenderer)を保持している DataGrid, List, TileList などのコンポーネントへの参照である owner プロパティが存在します。

DataGrid で itemRenderer を使用すると、listData は実際には DataGridListData 型のオブジェクトになります。
DataGridListData には、columnIndex プロパティや カラム (DataGridColumn) の名前を表す dataFiled プロパティなどもあります。

先ほどの行を、内側から一つ一つ説明していきましょう。

* listData as DataGridListData – listData を DataGridListData にキャストすることにより dataField プロパティにアクセスできるようにします。
* .dataField – そのカラムに表示されるフィールドです。 これを用いることにより itemRenderer を汎用的にできます。 dataField を用いることで、この itemRenderer を複数のカラムに使用できます。 今回の例では、dataField プロパティの値は ‘price’ です。
* data[ … ] – 与えられたレコードの特定のフィールドにアクセスします。今回の例では、price カラムです。
* Number( … ) – format() メソッドはNumber型の引数を取るので、Number型へのキャストを行っています。
* cfmt.format( … ) – 引数に与えられた値を通貨の表記に変換しています。

■ Summary

itemRenderer を実装する方法(inline, external)を説明しましたが、自分が実装しやすい方法で良いと思います。
ActionScriptだけを使う人もいます。FlexとActionScriptの経験を積めば、この方法がうまく行くでしょう。
MXML はシンプルな itemRenderer を手早く仕上げるのに向いています.

今後は UIComponentを継承した ActionScript クラスとして記述したもっと効率的なitemRendererを作成していきます。
次回の記事では、itemRenderer とアプリケーションの他の部分とのデータのやりとりについて説明します。