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 していて浮いてる気がするけど気にしない。