itemRenderer パート3 : Communication ( データのやりとり )

○ 原文
itemRenderers: Part 3: Communication

前回の記事では external itemRenderer の作成方法を、MXML と ActionScript の両方で説明しました。
サンプルコードでは、itemRenderer 内の Button がクリックされるとカスタム・イベント(BuyBookEvent)をディスパッチして、itemRenderer外でそのイベントに応じた処理を行えるようにしました。
今回の記事では、itemRenderer とのデータのやり取りを掘り下げていきたいと思います。

絶対に破ってほしくないルールが一つあります。それは「(外部から) itemRenderer のインスタンスを保持し、(publicなプロパティをセットすることにより)itemRendererを変更したり、itemRenderer の public メソッドを呼び出してはいけない」ということです。
Part 1 の記事で述べたように、 itemRenderer は再利用されるものなので保持するのはとても難しく、もしそれを行ってしまうと Flex frameworkの挙動を狂わせかねません。

このルールをに則って考えると、itemRenderer でできるのは次のようなことです。

* itemRenderer は自身を適用したコンポーネントを通じてイベントをディスパッチできます。(既にバブリングを紹介しました。この方法が優れていることを後で説明します)
* itemRenderer では Application.application などのクラス変数が使用可能です。Application オブジェクトに「グローバルに」変数を定義したのなら、この方法でその変数にアクセス可能です。
* itemRenderer は自身を適用したコンポーネントのpublic 変数にアクセス可能です。
* itemRenderer は data のレコードの全てのフィールドにアクセスできます。例えば、直接画面に表示するデータでなくとも、itemRendererの動作に影響を与えるフィールドにアクセスしたりできます。

■ itemRenderer の処理を動的に変更する

次のコードは、前回の記事で TileList 用に作成した MXML の itemRenderer です。
これを外部のデータによってitemRendererの処理を動的に変えるようにします。 ( ファイル名を 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>

TileList を用いてアイテムのカタログの表示をしようとしているとします。
金額の範囲を設定できる Slider コントロールも存在するとしましょう(Slider コントロールは itemRenderer の外側にあります)。
金額の範囲外のアイテムは、フェードアウトさせます(itemRendererのアルファ値を変化させます)。
itemRenderer の alpha 値を変更するためには、金額の範囲が変わったことを全ての itemRenderer に伝える必要があります。

set data メソッドを次のようにオーバーライドします。

override public function set data( value:Object ) : void
{
super.data = value;
if( data.price < criteria ) alpha = 0.4;
else alpha = 1;
}

問題は、どのように criteria (金額の範囲)の値を変えればいいかです。
itemRenderer を使う場合のベスト・プラクティスは、「itemRenderer は常に与えられたデータのみに基づいて動作させる」ことです。
しかし、今回のようなケースでは criteria を data に含めるのは良い方法とは言えませんので、data の外に存在するようにしましょう。
これを実現する方法はいくつかあります。

* list に含める。 list ( List, DataGrid, TileList 等 ) コンポーネントを継承し、その継承したクラスに public な変数として criteria (金額の範囲) を保持させる。
* グローバル変数として application オブジェクトに含める。

私ならば、一番目のクラスを継承して criteria をクラスに含める方法を選びます。
つまるところ、クラスはデータを表示するために使われていて、criteria は表示される項目の一部なのですから。
今回の例では、TileListを継承して criteria を public なデータメンバーとして持たせます。

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

itemRenderer の外に存在するコントロール (今回の例では、Sliderコントロール) がこのクラスの criteria プロパティに値をセットすることで、itemRenderer に criteria の値を通知できるようになりました。

■ listData

itemRenderer は、itemRenderer がセットされたリスト自身の情報と、itemRenderer がどの行と列(DataGrid のように列を持つコンポーネントの時のみ)描画しているかを知ることができます。
この情報が listData です。listData を使うと、次のように BookItemRenderer.mxml を書き直すことができます。

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

先に見せた BooktItemRenderer.mxml の <mx:Script> タグ内にこのメソッドを追加してみてください。

listData プロパティは itemRenderer が属するコントロールへの参照である owner プロパティを持っています。
今回のサンプルでは、owner は TileList を継承した MyTitleList です。 owener プロパティを MyTitleList へキャストすることにより、criteria へアクセス可能になります。

■ IDropInListItemRenderer

listData は、IDropInListItemRenderer インターフェイスを実装している itemRenderer にのみ存在します。
残念ながら、コンテナ(HBoxなど) はそのインターフェイスを実装していません。
コントロール (ButtonやLabelなど) はそのインターフェイスを実装していますが、コンテナの場合は自分でそのインターフェイスを実装しなければなりません。

このインターフェイスの実装方法は簡単で、Flexのドキュメントにも記述されています。
IDropInListItemRenderer を実装した BookItemRenderer クラスを自分で作るには、次のようにします。

1. IDropInListItemRenderer インターフェイスを実装したクラスを用意する

<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" ... implements="mx.controls.listClasses.IDropInListItemRenderer">

2. listData の set と get メソッドを itemRenderer となるクラスの <mx:Script> タグ内に追加する。

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

itemRenderer が IDropInListItemRenderer インターフェイスを実装していれば、list コンポーネントは listData を全ての itemRenderer にセットします。

■ invalidateList()

criteriaをクラスに(正常に機能するものとして)含めるのは少々複雑で、単にcriteriaに値を代入しただけではFlex frameworkはその値の変化に気付いてくれません。criteriaの値の変更時には、その変更を伝えるイベントのトリガーが必要です。
次のコードは、set criteria メソッドを変更したものです。

public function set criteria( value:Number ) : void
{
_criteria = value;

invalidateList();
}

_criteria へ値をセットした後にinvalidateList()を呼び出しています。
invalidateList()を呼び出すことにより、全てのitemRenderer に dataProvider の値でデータをリセットさせます。リセットすることにより set data メソッドが再度呼ばれます。
これらの処理は次のように表現できます。

1. itemRenderer は与えられたデータをどのように表示するか判断するために、list オーナー(itemRendererが属しているlistコンポーネント) のcriteriaの値をチェックします。
2. Flex の list クラスを継承した listオーナーには、itemRenderer が読み込むことができる public なプロパティがあり、そのプロパティに外部のコード(他のコントロールやActionScriptコード)が値をセットします。
3. list のプロパティがセット(値が変更)されると、そのlistの invalidateList() メソッドが呼び出されます。それが itemRenderer をリフレッシュさせるトリガーとなり、結果としてdataがリセットされます。 ( そしてステップ1へとまた戻ります。)

■ Event

以前の記事で、itemRenderer とアプリケーションの他の部分のデータをやりとりのために、どのようにイベントをバブリングさせれば良いか示しました。 それはそれで簡単で良いのですが、「itemRendererの役割はデータを表現(表示)すること、コントロールの役割はデータを操作すること」という考えに基づいた、より良い方法があるのではないと考えています。

MyTileList コントロールの本質は、売り物の本のカタログを表示することです。ユーザーがある本を選んで購入しようとしたときに、それをアプリケーションに伝えるのはlist コントロールの役割であるべきです。これをコードで書くと次のようになります。

<CatalogList bookBuy="addToCart(event)" />

現在の状況では、イベントはバブルアップして TileList を通り抜けてしまいます。
イベントをバブリングさせる方法では bookBuy イベントを リスト (TileList) コントロールに関連付けないので、コントロールをアプリケーションの別の場所に移動させられます。
例えば、bookBuy イベントのハンドラをメインの Application に記述すると、( その bookBuy イベントをディスパッチする ) リストコントロールをアプリケーションの別の場所に移動させたいとした時に、ハンドラも共に移動しなければいけません。
逆に、もしイベントをリストコントロールに関連付けていれば、そのコントロールを移動させるだけで済みます。

Button のクリック・イベントが実はButtonによりディスパッチされるのではなく、Button 内の別の何か他のものによりディスパッチされてバブリングされてくるものだとしたらどうでしょうか。
<mx:Button click=”doLogin()” label=”Log in” />とは書けなくなります。 doLogin() メソッドをどこか別の箇所に移動しなければならず実装がややこしくなります。

おわかりいただけたでしょうか。では、イベントをバブリングさせる方法から listコントロールにイベントをディスパッチさせる方法へと、サンプルを変更する手順を説明します。

第1に、CatalogList にメタデータを追加して、コンパイラにそのコントロールがイベントをディスパッチすることを知らせます。

import events.BuyBookEvent;
import mx.controls.TileList;

[Event(name="buyBook",type="events.BuyBookEvent")]

public class CatalogList extends TileList
{

第2に、CatalogList にイベントをディスパッチさせるメソッドを追加します。 このメソッドは itemRenderer により呼び出されます。

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

}

第3に、itemRenderer 内の Buy ボタンが上記メソッドを呼び出すようにコードを変更します。

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>

これで itemRenderer 内のボタンは、dataを引数としてlistコントロールのdispatchBuyEvent() メソッドを呼び出すことができるようになりました。
そうすることにより、アプリケーションの他の部分とのデータのやりとりを行うという責務をlistコントロールに移しています。

今回のサンプルの list コントロールは、data を保持するイベントをディスパッチします。
ActionScriptとMXML(CatalogList.as ファイルで[Event]メタデータで指定されているので)のどちらでも好きな方を使って、アプリケーションはこのイベントに対するイベントリスナをセットすることができます。
[Event]メタデータを使うと、あなたのコードは他の開発者にとって使いやすくなります。

■ Summary

itemRenderer は、イベントを用いてアクションを伝達するべきです。 カスタム・イベントを用いることによりデータを受け渡すことができるので、イベントを受け取る側はわざわざ itemRenderer にまでデータを取得しにいく必要がありません。

itemRenderer は set data メソッドをオーバーライドして、その data の変更に反応するようにしなければいけません。
そのメソッド内では listData.owner から必要なデータにアクセスできます。 また static なクラスや、メインのアプリケーション( Application.application )に保存されたデータにもアクセスできます。

次回の記事では、itemRenderer の状態について見ていきます。