itemRenderer パート1 : inline itemRenderer

Flex / AIR のリスト系コンポーネント(List, DataGrid, TileListなど)はデフォルトのままでも各アイテム(行)を十分を奇麗に表示してくれるのですが、プロジェクトによってはデフォルトのままの表現では要求を満たさなかったり、ボタンを追加する必要がでてきたり等問題が発生します。

そこで、今のうちにしっかりとマスターしておこうということで Adobe Customer Care の Peter Ent さんのブログである itemRenderer シリーズを翻訳してみました。
今回は itemRenderer パート1 : inline itemRenderer のみの翻訳となっていますが、これから週に1、2回のペースで順次追加していきます。

○ 原文 itemRenderers: Part 1: inline renderers
http://weblogs.macromedia.com/pent/archives/2008/03/itemrenderers_p.html

■ Renderer の再利用

サーバーから受け取ったデータの値に基づき DataGrid コンポーネントの5行目X4列目のセルを緑色に変更する時などに、外部から itemRenderer にアクセスしようとしているのをよく見かけます。
しかし、外部から itemRenderer を取得し、それらを変更するのは Flex フレームワークとFlex コンポーネント・モデルに大きく違反しています。

itemRenderer を理解するには、まずそれが何なのか、また私たち Adobe Flex エンジニアリング・チームが何を意図してそれをデザインしたのかを理解する必要があります。
1000個レコードを見せたい時に Listコンポーネントが 1000個の itemRenderer を作成すると考えるのは誤りです。

もし Listコンポーネントが10行表示するとしたのなら、約12個の itemRenderer が作成されます。
その表示される10行分のitemRendererが10個と、バッファリング及びパフォーマンスのために2、3個作られます。
Listコンポーネントは初期時に1~10行目を表示しますが、List をスクロールし3~12行目が表示されたとしても、元々の同じ12個の itemRenderer が用いられます。
スクロールしたとしても新たな itemRenderer が作成されることはありません。

Listコンポーネントがスクロールされると、3~10行目を表示しているitemRendererは元のデータを表示し続け、位置だけが上へ移動します。
1行目と2行目を表示していた itemRenderer は、下へ移動し11行目と12行目を表示します。
Listのサイズを変更しない限り、itemRendererは新しいデータを表示するために位置移動するだけでインスタンスは再利用されます。

5行目X4列目のセルの背景色を変更する際、既にユーザが Listをスクロールしてしまっていて、そのセルの itemRenderer が21行目のデータを表示しているかもしれないので注意してください。

では、どのようにすればよいのでしょうか?

itemRenderer は、受け取ったデータの値に基づいて自身を変更しなければいけません。

もしListのitemRendererがデータの値に基づいて色を変えるものであれば、そのitemRendererは自身が受け取ったデータを精査し、自分自身を変更しなければいけません。

■ inline itemRenderer ( インライン アイテムレンダラ )

今回の記事では、inline itemRendererを用いて上記の問題を解決していきます。
inline itemRendererとは、MXMLファイル内のitemRendererを適応するコンポーネントの箇所に直接記述されたものです。
次回の記事では、external itemRendererを用います。

inline itemRendererはシンプルなものなので、一般的に簡単なレンダラーを作成する時、又は大きいアプリケーションのプロトタイピング時に用いられます。

inline itemRendererを用いることに何も問題は無いのですが、アプリケーションのコードが複雑になってきた時には一つのクラスとして独立させるほうが好ましいです。

今回の記事では全ての例にて同じデータを用います。データはbookを表し、author, title, publication date, thumbnail image等の値が存在します。
各レコードはXMLノードであり、下記のようなものとなります。

<book>
<author>Peter F. Hamilton</author>
<title>Pandora's Star</title>
<image>assets/pandoras_star_.jpg</image>
<date>Dec 3, 2004</date>
</book>

それでは <mx:List> コントロールの簡単なitemRendererを作ってみます。
表示されるものはauthor、続いてtitleとなります。

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

この itemRenderer はとてもシンプルなので labelFunctionを用いたほうが便利かもしれませんが、重要な点も示しています。
まず一つ目は、inlineRenderer を定義するために <mx:itemRenderer> タグを用いていること。
そして、そのタグ内に <mx:Component> タグが用いられていること。
この <mx:Component> タグは、Flex コンパイラにコンポーネントをインラインにて定義することを知らせます。
そのタグの意味を少し説明しておきます。

itemRendererは<mx:Component> タグ内に定義します。上の例では、<mx:Label> コントロールを1つ用いて、そのコントロールの
textフィールドにデータ・バインディング {data.author}: {data.title} にて値をセットしています。これはとても重要です。 List コントロールは、itemRenderer の data プロパティに値をセットすることにより、各 itemRenderer のインスタンスに dataProvider のレコード(各行)を渡すのです。
上記コードでは、Listの全ての行にて、inline itemRenderer のインスタンスが data プロパティを持ち、そのプロパティに <book> XMLノードがセットされます。
List コントロールをスクロール度に、新しく表示される行のために itemRenderer が再利用されて data プロパティの値が変化します。

別の言い方をすると、1行目の itemRenderer のインスタンスが data.author に”Peter F, Hamilton”がセットされているとしても、表示外にスクロールされると
そのインスタンスは再利用されて “J.K, Rowling” がセットされるかもしれません。 これらは全て自動で処理されるので心配する必要はありません。

次の例は、<mx:List> コントロールを用いた少々複雑なinline itemRenderer です。

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

先のコードの <mx:Label> が <mx:HBox>と<mx:Image>、<mx:Label>、<mx:Text>に変わった以外あまり違いはありません。
レコードとのデータ・バインディングも有効です。

■DataGrid

DataGrid にも inline itemRenderer が使用可能です。下記コードは DataGridColumn に用いた例です。

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

先の2つの例よりもかなり複雑ですが、itemRenderer を適応するタグの中で <mx:itemRenderer>と<mx:Component> が 定義されているという点では同じ構成になっているのが分かると思います。

<mx:Component>の目的は、MXML内で ActionScript のクラスを作成するためです。
<mx:Component> タグ内のものを別のファイルにコピーしてクラス名を付けるのをイメージしてみてください。
inline itemRenderer は完全な MXML ファイルのようですよね。 ルートタグ ( 上記コードでは<mx:HBox> ) があり、<mx:Script> タグもあります。

上記コードの <mx:Script> タグの目的は、data プロパティの set 関数をオーバーライドすることにより、itemRenderer の背景色を変えるためです。
今回の例では、発行日(pubDate)が未来の場合に背景色を白(0xffffff)から紫(0xff99ff)にします。
itemRenderer は再利用されるので、発行日が過去の場合には白に戻す必要があります。
この色を戻すという処理を行わなければ、ユーザがListを何度もスクロールしていると最終的に全ての itemRenderer の背景色が紫になってしまいます。

■ outerDocument

スコープも変わります。 何を意味しているかというと、<mx:Component> タグ内で定義した変数のスコープは、そのコンポーネント/inline itemRenderer 内のみとなります。
同じように <mx:Component> タグの外のものは別ファイルにて定義されたコンポーネントのように別スコープとなります。
例えば、クリックすると本を購入できる Button を itemRenderer に追加するとします。
クリックイベントに反応し購入処理を行うメソッドをコールする Button は、下記のように定義できます。

<mx:Button label="Buy" click="buyBook(data)" />

もし buyButton() メソッドがファイルの <mx:Script> タグ内に定義されていると「buyBook() は定義されていないメソッドです」という旨のエラーが発生します。
これは butyBook() が <mx:Component> 内のスコープではなく、ファイルのスコープにて定義されているからです。これはよくあるケースなので outerDocument 識別子
を用いて回避してみます。

<mx:Button label="Buy" click="outerDocument.buyBook(data)" />

outerDocument 識別子を用いてファイルもしくは外側のドキュメントを参照できます。
注意事項として、outerDocumentを用いてアクセスするメソッドは protected や private ではなく、public でなければいけません。
<mx:Component> は外部で定義されたクラスのように扱われるのを思い出してください。

■ Bubbling Events

更に複雑な例を見ていきましょう。下記コードは、同じデータを TileList を用いて表示します。

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

実行すると itemRenderer は下のように表示されます。

TileListItemRenderer

DataGrid で用いた itemRenderer と似ていますが、Button のクリック・イベントの発生時に outerDocument を通じて buyBook() メソッドを呼び出していません。
今回の例では、クリック・イベントの発生時にカスタムのイベントをディスパッチしています。
そのイベントはバブルアップし、itemRenderer を抜け TileList を通り上位のヴィジュアル・コンポーネントによって受け取られます。

itemRenderer にButton, LinkButton 等のインタラクティブなコントロールを含ませることがよくあります。行を削除したり、今回のケースでは本の購入などクリックした時に何か処理を行わせるといった具合にです。

しかし itemRenderer に何か処理を行わせるのは賢明ではありません。itemRenderer の本来の役割は、見た目を良くすることだからです。
イベントをバブリングさせることで itemRenderer は別のものに処理を渡すことが可能になります。
ここでカスタム・イベントが有用となります。何故ならイベントは各行の data に紐付いているので、そのイベントに data を含めてディスパッチすればいいからです。
そうすることにより、イベントの受け取る側は data を取得するために、わざわざ data を探しにいく必要がなくなるからです。

■Summary

inline itemRenderer を用いることにより List の見た目を簡単に変更できます。

inline itemRenderer は別ファイルで定義された ActionScript のクラスのようなものとして扱ってください。 スコープも別ファイルで定義された ActionScript のクラスと同等となります。
itemRenderer のインタラクションの結果としてデータのやりとりが必要であれば、カスタムイベントを使ってください。

重要 : itemRenderer は再利用されるものなので保持しようせずに、各 itemRenderer に与えられたデータのみを扱うようにしてください。

次回は external itemRenderer を見ていきます。