2008/12/20

labelFunctionかItemRendererか。それとあとFormatter (6)

さらに前回の続き。

前回までで、「動的に表示単位とかを切り替える」を「DataGrid以外にもListやLabelでも使える」形にしてみた。
ただ、ここまでくると、「表示単位の切り替え」と「DataGrid以外にもListやLabelでも使える」というのが1つに収まってるのがなんとも具合が悪く見えてくる。
「表示単位の切り替え」以外にも要件が出てきたときに、結局同じようなものを作らないといけないというのがまずい。

というところまで考えると、「表示単位の切り替え」はそれこそFormatterにやらせればいいので、要はFormatterをlabelFunction用に合わせてくれるようなオブジェクトができればいいんでしょと。

ということで、上の2つを分離してみることにした。

利用イメージとしては、

<formatters:LabelFunctionAdaptor id="formatter" labelField="{list.labelField}">
<formatters:formatter>
<formatters:VariableCurrencyFormatter unit="{int(rbtnGrp.selectedValue)}" />
</formatters:formatter>
</formatters:LabelFunctionAdaptor>


という感じでformatterを宣言したい。
LabelFunctionAdaptorというのが、DataGridだろうがListだろうがLabelだろうがフォーマットを受け付けてくれるオブジェクトで、しかもフォーマッティングを変えたいときに動的に変える機能つき。
VariableCurrencyFormatterは、今までのとは違って、Flex標準のCurrencyFormatterを、表示単位が変えられうように継承して拡張したもの。
このFormatterを別のものに差し替えてあげれば、いろいろと他のものにも使えるようになるはず、というイメージ。

# 考えてみれば、こういう差し替えってのは、見方によってはまさにDIなんだなぁと思ってみた。
# MXMLってただの画面定義DSLっていうだけでなくて、オブジェクト定義に使うとしてもいいんじゃないかと。
# しかも、FlexBuilderとかを使ってるとMXMLは補完が効きまくるのでかなり生産性が高い。
# そのうえXMLだから、ActionScriptでチマチマ書くよりも少なくとも文法という意味では容易に覚えられるし。
# DSLとしてのMXMLというところを考えてみると、面白いことができるのかもしれない。
# まぁ、今はこれでとどめておいて後で書くことにする。


と、ここまで利用イメージを固めてみたところで、ひとつ困ったことが。
VariableCurrencyFormatterのオブジェクトがlabelFunctionのデータバインディング式に乗ってこないので表示単位の変更が伝わらずに、このままだと「動的に変更する」要件が実現できなくなっちゃうじゃん、と。

う~ん、諦めて継承とかにしたほうがいいんだろうか。MXMLの記述量も増えちゃったし。

まぁ、とりあえず「できるだけFlex標準のFormatterを使う」方法で進めていくとすると、
VariableCurrencyFormatterのプロパティが変わった時に、自力で再度LabelFunctionAdaptorのformat()が呼ばれるようにすれば実現できるということがわかる。

自力で再度LabelFunctionAdaptorのformat()が呼ばれるようにする、というのは、今までのイベント名付きの[Bindable]を使えばOK。
で、問題は、「VariableCurrencyFormatterのプロパティが変わった時に」をどう実現するか。

いままでよく分かってなかったのだけれど、それにはどうやらChangeWatcherを使えばよいらしい。
使い方としては、

ChangeWatcher.watch([監視したいオブジェクト], [監視したいプロパティ], [プロパティの変更時に呼び出されるイベントハンドラ関数]);

で良いとのこと。

ここのプロパティの指定は、実は"hoge.piyo.hogera"みたいに、子へと辿っていけるものらしい。
何となく、データバインディング式で書いてる内容がこれでうまくパースされてやられてる、ということの裏側な気がする・・・。

ということで、

private var _formatter:Formatter;

[Bindable]
public function get formatter():Formatter { return _formatter; }
public function set formatter(value:Formatter):void
{
if (_formatter != value)
{
_formatter = value;
triggerFormatChange();

if (value)
{
formatterChangeWatcher = ChangeWatcher.watch(value, "format", triggerFormatChange);
}
}
}

private function triggerFormatChange(event:Event = null):void
{
this.dispatchEvent(new Event("formatChanged"));
}



とやってみた。

これで、VariableCurrencyFormatterのformatプロパティが変わった時に、LabelFunctionAdaptorが変わったとチェーンされて、最終的にlabelFunctionの再実行が行われるはず。
実際にはVariableCurrencyFormatterのformatはプロパティじゃなくてメソッドなので、例のイベント名付きの[Bindable]を使って、プロパティの変更時にformatが変更されたかのようにイベントを飛ばせばOK。

ただ、う~ん、これだと、実はFlex標準のFormatterってformat関数に[Bindable]が付いていたりしないので、なんだかんだいって結局そのまま使えるわけじゃないって話になるのね。

しかも、ChangeWatcherって、内部で[監視対象オブジェクト] -> ChangeWatcherインスタンス、という参照が生じるらしいので、下手するとメモリリークになるっぽい。
cf.) SDK-14891

対象のChangeWatcherインスタンスのreset()メソッドにnullを引数として与えれば解消できるみたいだけど、でも、必ず先にLabelFunctionAdaptor側が要らなくなるような場合、やっぱりリークしちゃうじゃん。

というわけで、Flex標準のFormatterをうまく組み込んでlabelFunction用に簡単にできるようにする、という目論見はやっぱりやめたほうが無難っぽい。

これに汎用性を持たせたければ、Formatterを呼ぶ最後の部分だけabstractな関数として切り出して、継承によってバリエーションを持たせておく、というのがいいかな。

labelFunctionかItemRendererか。それとあとFormatter (5)

前回の続き。

前回までで「動的に1000単位とかに切り替えられる仕組み」は完結。
ただ、「いちいちそれ用に作ってあげないといけない」という課題があった。
「それ用に」と言っているのは、たとえば同じような話をListコンポーネントのlabelFunctionにやってあげたりとか、あるいはLabelコンポーネントのtextプロパティにこれを使おうとしても、format関数のシグネチャがDataGrid専用になってるので使い回しが利かない、ということ。

というわけで、これを解決すべく、前回までのVariableCurrencyFormatterのformat関数のシグネチャを変えて対応できるようにしてみようかと。

このVariableCurrencyFormatterの使われそうなところを考えてみると、上記のとおりDataGridとListとLabelがあるので、それぞれどういう感じか考えてみる。

まず、DataGridは今までのとおり、

public function format(item:Object, column:DataGridColumn):String

のシグネチャでOK。

Listは、

public function format(item:Object):String

なんだけど、item はその行が表示対象とするオブジェト、まぁ大抵はいわゆるDTOってやつが来るんだけど、どのプロパティを表示するかを渡してあげないとまずい。
ListコンポーネントにはlabelFieldっていうのがあって、Listの場合はそれで指定することになるので、同じように外部から与えられるようにすればいいでしょう。

ていうか、listっていうListコンポーネントがあったとしたら、

<formatters:VariableCurrencyFormatter labelField="{list.labelField}" />

ってなっているのがわかりやすい。

Labelの場合、使い方としては、

<mx:Label text="{formatter.format(hoge)}" />

という感じだろうから、引数として1個のObjectを取ってStringを返すので十分。


シグネチャが違うのだけれど、ActionScriptだと引数にデフォルト値をしておけば関数のオーバーロードみたいなのができるので、これを使えばできそう。

というわけで、

[Bindable(event="formatChanged")]
public function format(item:Object, column:DataGridColumn = null):String
{
var dat:Object = item;

if (column != null && item.hasOwnProperty(column.dataField))
{
dat = item[column.dataField];
}
else if (labelField != null && item.hasOwnProperty(labelField))
{
dat = item[labelField];
}

var curFormatter:CurrencyFormatter = new CurrencyFormatter();
return curFormatter.format(Number(dat) / unit);
}

private var _labelField:String;

[Bindable]
public function get labelField():String { return _labelField; }
public function set labelField(value:String):void
{
if (_labelField != value)
{
_labelField = value;
dispatchEvent(new Event("formatChanged"));
}
}


と書いてみた。

これでDataGrid、List(ついでにlabelFieldが動的に変わっても対応可能)、Labelのそれぞれで同じ感じに使えるようにできた。

2008/12/19

labelFunctionかItemRendererか。それとあとFormatter (4)

前回の続き。


[Bindable]
public var format:Function;

とやった上で、format プロパティをセットし直すことで再フォーマットさせる、という風にして解決を図ったのだけれと、このformatプロパティをセットし直さないといけない、というのと外から変えられてしまうのがどうもなぁ、というところ。

とりあえず外から変えられないようにgetterを作ってみたけれど、コンパイラに怒られるはバインディングされないわでダメだった。


で、これを解決すべくググってみたりとかして調べてみた結果、実は読み取り専用プロパティでもデータバインディング元にできるということが判明。
ていうか、探せばFlex SDKのなかにたくさん溢れているようで、たとえばRadioButtonGroupのnumRadioButtonsプロパティとか。

ポイントとしては、


  1. getterにイベント名付きの[Bindable]タグを付けて宣言

    e.g.)

    [Bindable(event="hogeChange")]
    public function get hoge() { return _hoge; }


  2. バインドした先にプロパティの変更を通知したい場合、[Bindable]タグでつけたイベントをディスパッチする

    e.g.)

    private function setHoge(value:String)
    {
    _hoge = value;
    dispatchEvent(new Event("hogeChange"));
    }




で良いらしい。

しかも、もうちょっといろいろと試してみると、別にgetterでなくとも、関数に直接[Bindable(event="hogeChange")]を書いてもいいらしいということがわかった。
ちょっとわけがわからなくなりそうだけど、要は、関数に[Bindable]を付けておけば、データバインディングでその関数を使っているところに対して、好きなタイミングでその関数をコールさせることができるということか。
あるいは、好きなタイミングでデータバインディング式を評価させることが可能か。
う~ん、今回の用途にぴったり。

上記を踏まえて、以下のようにして作ってみた。


[Bindable]
public function get unit():int { return _unit; }
public function set unit(value:int):void
{
_unit = value;
dispatchEvent(new Event("formatChange"));
}

[Bindable(event="formatChange")]
public function format(item:Object, column:DataGridColumn):String
{
(略)
}


なんだ、逆にだいぶ簡単になっちゃったじゃん。

あれ、でもこのオブジェクト、別にEventDispatcherを継承してるわけじゃないんだけど。。。
Mix-Inってやつか?まぁ、あんまり気にしないでみよう・・・。

さて、これでだいぶいい感じになってきたけど、どうもしっくりきてないのが、Formatterの存在。
Flex標準で提供されている割には、いまいちlabelFunctionとかと相性が悪いんだよね。
結局、いちいち今まで作ってきたようなオブジェクトを作って、その中でFormatterを作って利用、みたいな流れなので、面倒。
最後はこれを何とかしてみたい。

labelFunctionかItemRendererか。それとあとFormatter (3)

前回の続き。

「動的に1000単位とかに切り替えられる仕組み」に挑戦中なわけですが、ここで「動的に」といっているのは、前回のVariableCurrencyFormatterの、「表示単位」を


<formatters:VariableCurrencyFormatter id="formatter" unit="{int(rbtnGrp.selectedValue)}"/>
<mx:RadioButtonGroup id="rbtnGrp" selectedValue="1000" />
<mx:HBox>
<mx:RadioButton group="{rbtnGrp}" label="(1)" value="1" />
<mx:RadioButton group="{rbtnGrp}" label="(千)" value="1000" selected="true" />
<mx:RadioButton group="{rbtnGrp}" label="(百万)" value="1000000" />
</mx:HBox>


という感じで変えてやりたい、ということです。
上記だと、ラジオボタンを選べばその単位になってほしい、というところ。

ここまでだと、確かに VariableCurrencyFormatter のunitプロパティは、データバインディングがうまくはたらいてラジオボタンの選択した値になってくれます。
これを、


<mx:DataGridColumn dataField="int1" labelFunction="{formatter.format}" textAlign="right"/>


と書いたlabelFunctionまで伝搬させてあげたいわけです。


というところで、関数も第一級のオブジェクトとして、つまりは変数として扱えるActionScriptの性質を利用してやればできるんじゃないかなぁと思ってみた。
変数の値が変わればいいので、強引に関数オブジェクトを入れ替えれてやればいいだろうと。

で、unitが変わったら関数オブジェクトを入れ替えてやるので、


private var _unit:int = UNIT_ONE;

[Bindable]
public function get unit():int { return _unit; }
public function set unit(value:int):void
{
_unit = value;
this.format = null;
this.format = formatFunction;

}

[Bindable]
public var format:Function = formatFunction;

private function formatFunction(item:Object, column:DataGridColumn):String
{
var dat:Object = item;

if (column != null && item.hasOwnProperty(column.dataField))
{
dat = item[column.dataField];
}

var curFormatter:CurrencyFormatter = new CurrencyFormatter();

return curFormatter.format(Number(dat) / unit);
}


とやってみた。。。


まぁ、少々強引だけれども、確かに意図したとおりに動くは動くみたい。
format プロパティに一度nullをセットしているのは、どうもデータバインディングは同じ参照が渡されてもバインディング先に通知されないから。


なんとも美しくないのと、format プロパティがpublicなので外からトンデモナイものをセットされないとも限らない、というのが怖いので、次のステップはこれを何とかしてみたい。

2008/12/17

labelFunctionかItemRendererか。それとあとFormatter (2)

前回の続き。

まずは、「動的に1000単位とかに切り替えられる仕組み」に挑戦。
labelFunctionで試してみる。
とはいえいきなりは難しいので、まずは1000単位にした金額フォーマットするlabelFunctionあたりから作成。

呼び出し側は

<formatters:VariableCurrencyFormatter id="formatter" unit="1000" />
<mx:DataGrid id="dg" dataProvider="{dp}">
<mx:columns>
<mx:DataGridColumn dataField="str1" />
<mx:DataGridColumn dataField="str2" />
<mx:DataGridColumn dataField="str3" />
<mx:DataGridColumn dataField="str4" />
<mx:DataGridColumn dataField="int1" labelFunction="{formatter.format}" textAlign="right"/>
</mx:columns>
</mx:DataGrid>

という感じで。

で、中で使ってるVariableCurrencyFormatterの、format()関数はこんな感じ。

public function format(item:Object, column:DataGridColumn):String
{
var dat:Object = item;

if (column != null && item.hasOwnProperty(column.dataField))
{
dat = item[column.dataField];
}

var curFormatter:CurrencyFormatter = new CurrencyFormatter();

return curFormatter.format(Number(dat) / unit);
}


さすがにこれは問題なく1000単位での表示になる。

問題は、このunitプロパティを変えてあげた時に、動的に表示が変わってほしいっていうこと。
変えるたびにdp.refresh()とかやってあげれば、DataGrid側にPropertyChangeイベントが飛んで実現できそうだけど、さすがにそれはスマートじゃないような。。。
やっぱりせっかく用意されているデータバインディングでうまくやりたいよね。

2008/12/13

labelFunctionかItemRendererか。それとあとFormatter (1)

FlexのDataGridはいろいろと便利機能が組み込まれてるのだけれど、その中でも表示対象のデータの中身に応じて表示文字列を変えられるラベル関数(labelFunction)はなんだかんだいって多用する。
と、その一方でリッチな文字列だろうがアイコン画像だろうが表示をカスタマイズしてくれるItemRendererなんていうのもあって、どっちを使えばいいのか悩むってことが結構あったりして。

何にせよ揺れたりするのは良くないので、ここいらでポリシーを固めておきたいところ。

両者の特徴としては、
・labelFunction: 簡便。文字列の変換にしか使えない。典型的には金額形式での表示とか。
・ItemRenderer: 仕組みとしては複雑。一方で何にでも使える。 典型的にはデータ状態に応じて赤くするとか。
というあたり。

ただ、重要なことは、labelFunctionでフォーマットした結果ががItemRendererに渡ってくるということ。
# 正確に言えば違くて、DataGridのデフォルト実装であるところのDataGridItemRendererにしろ、典型的なアイテムレンダラであるLabelにしろ、dataのsetter内で自身の表示文字列をlistData.labelに設定しているため、その動きに見える、というだけ。
# 少なくとも、labelFunctionとItemRendererの指定は両立しうる、ということは言えると思う。

ちうことは、役割をちゃんと分担させるという意味で、ItemRendererでは特にフォーマットしないでおいて、表現上の問題(色を変えたり、とか)に徹するのが吉?なのかなと。

まぁ、それは基本なので普通にやってるうちはいいのですが、いろいろと応用を考えた場合、それでいいのかどうかはちょっと疑問。
しばらくそこら辺を深掘りしてみようかと。

たとえば、上記の金額の表示とか。
でかい金額を扱うような話だと、ときに1000単位とか100万単位とかに表示を切り替えてみたりとか、
そういうのが必要になったりする。
さすがにDataGridの表示対象のデータ(データプロバイダ)を直接いじるのは嫌なので、やっぱりそこはlabelFunctionかItemRendererかということなんだろうけど、そもそもそれって出来るのかな、みたいな。
それも可能なら上記のポリシーに合わせてlabelFunctionにしてみたいなぁって。

あと、そういえばFlexの標準でFormatterっていうのが提供されているけど、てんでDataGridのlabelFunctionのことなんか考えてくれてないから、そこんとこ上手くやりたいよねぇ、とか。

2008/12/12

FlexでEnum

JavaにしろFlexにしろ、堅く作ろうと思うと、単にstatic const (Javaならstatic final)を付けて定数を使ったりするわけですが、もうちょっと行くと、やはりENUMとかを使いたいところ。

典型的な用途としては、DBのカラムに入っているフラグですよね。
性別を格納するのに'M'か'F'なのはいいとして、それをプログラムの中で文字列とかで判定したくはない・・・。
まぁ、この例においてその要素が増えるとは思わないけれど、増えた時とか、型が明確なので非常に影響範囲が特定しやすいわけで。
反対に定数だと、定数を使わずにハードコードされたりしたら目も当てられないしな。

というわけで是非とも使いたんですが、
「Flexだとサポートされないし」
「ましてやJavaのENUMをFlexにマップするなんてできんだろうし」
と悩んでいたところ、それっぽくはできるようです。

http://flexblog.faratasystems.com/?p=242

ただし、FlexのENUMは完全にインスタンスをENUMで宣言したものだけにできるわけではなく、「同じ値を意味するけどインスタンスは別」という状況になる模様。
まぁ、ここまでできればequals()みたいなのを使ってやればいいので、だいたいOKかな。

もう一個の問題は、私んとこだとS2Flex2とか使うんで、そっちをうまく対応させてあげる必要のにもうちょっと研究が必要そうですかね。
あと、S2BlazeDSとかも同じような話。