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 ProjectSnippets に TwoWayBinding クラスとして公開してみた。

1人だけ mx 名前空間を import していて浮いてる気がするけど気にしない。