2007年09月10日
AS3 で双方向データバインディング
ActionScript 3.0 で双方向にデータバインディングしたいことがあったりする。
MXML に {hogehoge} 形式でデータバインディングを作ると問題なく実現できるんだけど、スクリプトから BindingUtils.bindProperty でやろうとするとスタックオーバーフローしてしまうことがある。
スタックオーバーフローする例
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" applicationComplete="appComplete();">
<mx:Script>
<![CDATA[
import mx.binding.utils.*;
[Bindable]
public var items:Array = ["item1", "item2", "item3"];
[Bindable]
public var selected:Array = [];
private function appComplete():void
{
BindingUtils.bindProperty(list, "selectedItems", this, "selected");
BindingUtils.bindProperty(this, "selected", list, "selectedItems");
}
]]>
</mx:Script>
<mx:List id="list" dataProvider="{items}"/>
</mx:Application>
list.selectedItems が変更されると、this.selected が書き換えられ、今度は this.selected が変更されるので list.selectedItems を書き換えて... と無限に関数呼び出しが発生してしまうから。
list.selectedItems と this.selected が等しくなればオーバーフローは発生しないはずなのだけど、List クラスは selectedItems を返すときに、新たに Array を生成して返しているので、いつまでたっても等しくならずにオーバーフローしてしまうというわけだ。(mx.controls.listClasses.listBase 参照)
問題ない例
当初はこの例を問題ある例と紹介していましたが、問題ありませんでした。
package
{
import flash.display.Sprite;
import mx.binding.utils.*;
public class TwoWayBindingNg extends Sprite
{
[Bindable]
public var obj1:Object;
[Bindable]
public var obj2:Object;
public function TwoWayBindingNg():void
{
BindingUtils.bindProperty(this, "obj1", this, "obj2");
BindingUtils.bindProperty(this, "obj2", this, "obj1");
obj1 = {a : 1}; // stack overflow
}
}
}
この例だと、obj1 == obj2 になった時点で無限に呼び出しは発生ししない。
そこで汎用化
フラグをつけて、2回以上呼び出さないようにする関数としてまとめ上げて、何も考えずに相互データバインディングできるようにする。
public static function createTwoWayBinding(src1:Object, prop1:String, src2:Object, prop2:String):void
{
var flag:Boolean = false;
ChangeWatcher.watch(src1, prop1, function(event:Event):void
{
if(!flag)
{
flag = true;
src2[prop2] = src1[prop1];
flag = false;
}
});
ChangeWatcher.watch(src2, prop2, function(event:Event):void
{
if(!flag)
{
flag = true;
src1[prop1] = src2[prop2];
flag = false;
}
});
}
これで OK。
最初の MXML も
BindingUtils.bindProperty(list, "selectedItems", this, "selected"); BindingUtils.bindProperty(this, "selected", list, "selectedItems");
を次のように書き換えれば動くようになる。
createTwoWayBinding(this, "selected", list, "selectedItems");
便利そうなので TwoWayBinding クラスにした
Spark Project の Snippets に TwoWayBinding クラスとして公開してみた。
1人だけ mx 名前空間を import していて浮いてる気がするけど気にしない。