カスタムコンポーネントで注意すること(invalidateProperties / commitProperties)

カスタムコンポーネントを作っていて、ちょっとはまりそうになったことのメモです。

Flex の作法ではプロパティ値の変更時にいきなり UI を更新してはいけません。プロパティ値が変更されたときは、変更を記録しておいて invalidateProperties を呼び出すに留めます。invalidateProperties を呼び出しておくと、一連の処理後*1に commitProperties が呼び出されます。このタイミングで変更のあった*2プロパティの値とそれに影響される他の値を確定し、必要であれば invalidateSize や invalidateDisplayList を呼び出して、UI を更新します。

この辺りのことは、id:s-ohira さんの asでカスタムコンポーネント(1)(2)(3) が詳しいです。

簡単にまとめると、

  1. プロパティ値を変更する
  2. invalidateProperties を呼び出す
  3. commitProperties が呼び出される
  4. commitProperties 内でプロパティ値の計算等をする

という流れになります。

通常はこの流れに従い commitProperties を実装するだけなのですが、commitProperties の中で他のプロパティ値を変更する場合は注意が必要です。というのも commitProperties の処理中は invalidateProperties の呼び出しが無効化されるようです*3。このため、commitProperties の処理中に変更したプロパティの内容は、その commitProperties の処理中に反映する必要があります。

例えばコンボボックスを継承したカスタムコンポーネントで、commitProperties 処理中に ComboBox の selectedItem プロパティを変更する場合を考えます。selectedItem プロパティのセッターでは invalidateProperties 呼び出しが記述されていると思うのですが、commitProperties の処理中では無効になってしまい、表示が更新されません。そのため、selectedItem プロパティの変更を反映するには、プロパティ変更後に super.commitProperties を呼び出す必要があります。
ここで、プロパティ変更と super.commitProperties 呼び出しの順序を逆にしてしまうと*4、プロパティ変更に対する commitProperties 呼び出しが無いため、表示が更新されない状態になります。

というわけで commitProperties をオーバーライドした場合、super.commitProperties はメソッド末尾で呼び出すのが良いようです。

以下、ソースコード例です。foo プロパティで設定した値を選択するコンボボックスです。処理内容に意味はありません。*5
コンパイルしていないので間違っているかもしれないです :-P

public class ComboBoxEx extends ComboBox {
  private var _invalidateFoo:Boolean;
  private var _foo:Number;
  public function set foo(value:Number):void {
    _foo = value;
    _invalidateFoo = true;
    invalidateProperties();
  }

  protected override function commitProperties():void {
    if (_invalidateFoo) {
      _invalidateFoo = false;
      selectedItem = _foo;
    }
    super.commitProperties();
  }
}

*1:次のフレーム描画時

*2:変更の有無は自分で記録しておく

*3:invalidateProperties が無効化されるおかげで、commitProperties がループして呼び出されるようなことが起こらないですみます

*4:super.commitProperties 後にプロパティ変更した場合

*5:この程度であれば、_foo 等の変数は使用せずに、foo プロパティのセッタで selectedItem = value; としてしまったほうがシンプルですね