JavaFX Advent Calendar 2012 26日目 GroovyのVetoableを使ったサンプルをJavaFXのバインディングを使って実装してみる

このエントリはJavaFX Advent Calendarの26日目のエントリとなります。前日は id:skrb / @ さんによる「JavaFX で Merry Christmas!」です。25日を過ぎちゃいました。まさか1日目と最終日を担当することになるとは。
25日分を突破するわ、海外のJavaFXエンジニアの方々に注目されてエントリを英語に翻訳するようになるわで、まさかの盛り上がりとなりましたねえ。

さて、今回のエントリですが、同じくAdvent Calendarの13日目に@さんがJavaFXでGroovyのVetoableが機能するか試してみたというエントリを書かれて、GroovyのBindable and Vetoable transformationを利用して、Model、View、Controllerを綺麗に分離する方法を紹介されていました。
ですが、JavaFX自体にもModel、View、Controllerを綺麗に分離するためのバインディングと呼ばれる機能が用意されています。このエントリではその機能を用いて上記ブログエントリのサンプルを再実装することで、バインディング機能について紹介してみたいと思います (なので先にみけさんのエントリを読んでおいてくださいね) 。

JavaFXバインディングとは?

バインディングとは、簡単に言うとあるデータと別のあるデータを結び付ける機能のことです。あるJavaBeansオブジェクトのプロパティが変化したら自動的に他のJavaBeansオブジェクトにその値を伝達できるようにします。機構の実現にはObserverパターンを利用しています。
この機構を用いることで、Modelオブジェクトのプロパティに対して、Viewに使用するコンポーネントのプロパティを「バインド」しておけば、Modelの値が変化したら自動的にViewの表示内容が変わるようになり、典型的なMVCパターンの構造を組むことができるようになりますね。

WPF等のXAMLフレームワークFlexなど、最近のGUIフレームワークは大抵このバインディング機構が備えられています。JavaScriptライブラリでもknockout.jsのようにバインディング機構を備えたライブラリが色々出てきています。

JavaFXバインディングは他のGUIフレームワークには余り見られない強力な機能が備えられています。ただし、既存のJavaBeans仕様を拡張したAPIを用いて実現しているため、記述は少々面倒だったりします。サンプル実装を通して紹介していきたいと思います。

Veotableの例をバインディングを用いて実装したサンプル

それでは実際にバインディングを用いて再実装したサンプルを示します。全ソースコードはgistにアップしています。
https://gist.github.com/4363405

まずはModelからです。

public class Person {
    // nameプロパティ
    private StringProperty name = new SimpleStringProperty();
    // 外部に公開するプロパティ
    public StringProperty nameProperty() {
        return name;
    }
    // JavaBeans仕様と互換を持たせるためのgetter、setter
    public final String getName() {
        return name.get();
    }
    public final void setName(String name) {
       this.name.set(name);
    }

    // messageプロパティ (read only)
    private ReadOnlyStringProperty message = new ReadOnlyStringPropertyBase() {
        {
            name.addListener(new InvalidationListener() {
                // nameプロパティの変化を観察し、変化したらこのプロパティも値が変化したことを通知する
                @Override
                public void invalidated(Observable o) {
                    fireValueChangedEvent();
                }
            });
        }
        @Override
        public String get() {
            String value = Person.this.getName();
            if (value.length() > 0) {
                return "Hello I'm " + value + ".";
            } else {
                return "";
            }
        }
        @Override
        public Object getBean() {
            return Person.this;
        }
        @Override
        public String getName() {
            return "message";
        }
    };
    // 外部に公開するプロパティ
    public ReadOnlyStringProperty messageProperty() {
        return message;
    }
    // JavaBeans仕様と互換を持たせるためのgetter
    public final String getMessage() {
        return message.get();
    }
}

まずは簡単な name プロパティから見てみましょう。なにやらいつものJavaBeansのプロパティとはちょっと違いますね。

    // nameプロパティ
    private StringProperty name = new SimpleStringProperty();
    // 外部に公開するプロパティ
    public StringProperty nameProperty() {
        return name;
    }
    // JavaBeans仕様と互換を持たせるためのgetter、setter
    public final String getName() {
        return name.get();
    }
    public final void setName(String name) {
       this.name.set(name);
    }

JavaFXのプロパティは **Property 型 ( ** にはラップする値のクラスが入る) でラップします。そしてこの Property 型の get/set メソッドで値のやり取りをします。
外部に公開するときは "(プロパティ名)Property()" という名前のメソッドを宣言します。また、既存のJavaBeans仕様と互換性を持たせたい場合はこのように getter/setter も用意しておきます (ただし、メソッド内では Property を通してやり取りする) 。*1
Property 型は abstract クラスなので、継承して get/set 時の処理を実装することになりますが、単純に値の受け渡しだけをする場合は Simple**Property というデフォルト実装が用意されています。
この Property オブジェクトが「バインド可能」なオブジェクトになるわけです。

続いて message プロパティです。こちらは外部から値を設定するのではなく、name プロパティの値に依存したプロパティとなっています。なので、リードオンリーなプロパティとして実装してみました。

    // messageプロパティ (read only)
    private ReadOnlyStringProperty message = new ReadOnlyStringPropertyBase() {
        {
            name.addListener(new InvalidationListener() {
                // nameプロパティの変化を観察し、変化したらこのプロパティも値が変化したことを通知する
                @Override
                public void invalidated(Observable o) {
                    fireValueChangedEvent();
                }
            });
        }
        @Override
        public String get() {
            String value = Person.this.getName();
            if (value.length() > 0) {
                return "Hello I'm " + value + ".";
            } else {
                return "";
            }
        }
        @Override
        public Object getBean() {
            return Person.this;
        }
        @Override
        public String getName() {
            return "message";
        }
    };
    // 外部に公開するプロパティ
    public ReadOnlyStringProperty messageProperty() {
        return message;
    }
    // JavaBeans仕様と互換を持たせるためのgetter
    public final String getMessage() {
        return message.get();
    }

リードオンリーなプロパティの場合、オブジェクトの内部状態に応じた値を返すことになるので、多くの場合作り込みが必要になります。JavaFXでリードオンリーなプロパティを実装する方法は大きく分けて次の2つの方法があります。

  1. abstract な ReadOnly**PropertyBase を継承して実装する。
  2. 読み書き可能な ReadOnly**Wrapper 型のオブジェクトを内部で保持し、外部にはその getReadOnlyProperty() メソッドで返すオブジェクトを公開する。

ここでは前者の方法を用いて実装してみました。バインド可能なプロパティを用意する場合、次の処理を実装する必要があります。

  • 値の読み書きと、それに付随する処理。
  • 公開する値が変化した場合、それを外部に通知する処理。

まず、初期化処理で InvalidationListenerなるものを name プロパティに対して登録していますね。これは name プロパティの値の変化を観察しています。message プロパティは name プロパティの値に依存していますから name プロパティの変化を検知した場合、このように fireValueChangedEvent() メソッドをコールして、自身に登録されたリスナに対して変更を通知します。
そして、3つのabstractメソッドを実装しています。1つめの get() が値を返す処理を実装していますね。name に何も入っていない時は空文字を返し、値が入っているときは "Hello I'm " を付けて返しています。
getBean() メソッドはプロパティのオーナーのインスタンス (ここでは Person インスタンス) を返し、getName() はプロパティ名を文字列で返します。

注目すべき点は InvalidationListener です。これは観察しているプロパティの値が変化したことだけを知らせます。これがミソです。
実はJavaFXバインディングでは遅延評価が基本となっており、実際に値が取り出された時に初めて算出するようになっています。
今回は単純に1つの値を受け渡すだけですが、実際にバインディングを使用するときは複数の値を組み合わせて算出したり、あるいは取得した値から複雑な描画処理を行ったりする可能性があります。
そこで、JavaFXでは観察されている値が変化したときは、変化したことを知らせるフラグだけを立てておき、本当にその値が必要とされるときになって、フラグが立っていたら実際の値を算出するようになっているのです。実際に値を取り出す前にデータソース側が何度変化していても、計算するのはその時の1回だけです。
私はJavaFXのこの点がすごく気に入っています。以前Flexで開発していたことがあったのですが、Flexバインディングは即時評価であるため、濫用するとパフォーマンスの低下を招きやすく、折角の機能が余り使えなくて残念な思いをしていました。

さて、ここまでのコードは長かったですが、プロパティ側の準備ができたら後は簡単です。まずはFXMLから。

<AnchorPane id="AnchorPane" prefHeight="200.0" prefWidth="320.0" xmlns:fx="http://javafx.com/fxml" fx:controller="aoetk.bindingsample.SampleController">
  <children>
    <Label layoutX="35.0" layoutY="31.0" text="Your name" />
    <TextField fx:id="txtName" layoutX="129.0" layoutY="28.0" prefWidth="177.0" />
    <Label fx:id="name" layoutX="35.0" layoutY="133.0" />
    <Label fx:id="message" layoutX="129.0" layoutY="133.0" />
  </children>
</AnchorPane>

特に変わったところはありませんね。注目して欲しいのはmike_neckさんのエントリでは TextField にイベントを定義していましたが、ここでは定義していません。
続いてControllerです。

public class SampleController implements Initializable {
    @FXML
    private TextField txtName;
    @FXML
    private Label name;
    @FXML
    private Label message;

    private Person person;

    // 初期化時に必要なバインドを行う
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        person = new Person();
        person.nameProperty().bind(txtName.textProperty());
        name.textProperty().bind(person.nameProperty());
        message.textProperty().bind(person.messageProperty());
    }
}

初期化処理を行う initialize() メソッド内でバインディングを行っています。値の変更を反映する側のプロパティの bind() メソッドを呼び出し、引数に変更元のプロパティを渡します。
まず最初に、Person#name に対して、テキストフィールドの text プロパティをバインドしています。こうすることで、テキストフィールドへの入力が name プロパティに勝手に反映されることになります。
次の2行でラベル name に対して Person#name を、ラベル message に対しては Person#message をバインドしています。
これだけでテキストフィールドの入力内容をラベルに反映する処理を実装することができました!

ちなみにControllerでバインドを行っているのは、現時点ではFXMLは単純なバインディングしか記述できないためです (FXML内で宣言した変数のみバインドが書ける) 。将来的には複雑なバインドも記述できるようにするそうです。

ともあれ、バインディングを活用すれば容易にModelとViewの値を結びつけ、しかも疎結合にできる (ModelはViewの事を一切知りません) ことがこのサンプルで分かると思います。

Property型についての補足

なお、プロパティの定義に使った **Property 型は **Expression 型を継承しています。Expressionという名前から想像できるように、様々な演算を行うためのメソッドが用意されています。これを利用することで、バインドする値を様々に加工したり、複数の値を組み合わせてバインドすることができるようになります。
これを利用すると、先ほどの例を次のようにして同じ機能を実現することが可能です。*2

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        person = new Person();
        person.nameProperty().bind(txtName.textProperty());
        name.textProperty().bind(person.nameProperty());
//        message.textProperty().bind(person.messageProperty());
        message.textProperty().bind(
                new When(person.nameProperty().isEqualTo(""))
                    .then("")
                    .otherwise(new SimpleStringProperty("Hello I'm ").concat(person.nameProperty()).concat("."))
        );
    }

流れるようなインターフェースで条件分岐やら文字列の結び付けが可能になっていることが分かります。ちょっと見辛いですけどね。
このようにある程度複雑なプレゼンテーションロジックも Expression 型の備えるメソッドで実現が可能になっています。詳しくはJavaFXのAPIドキュメントで Expression 型のメソッドを確認してみてください。

まとめ

これまで見てきたように、JavaFXには強力なバインディング機構があり、コンポーネント間の関係を疎結合にしつつ、関連する値を結びつけることが可能になります。遅延評価や流れるインターフェースを用いた演算など、なかなか強力な機能が備わっています。
難点はちょっと記述が面倒な点ですね。このあたりはJavaの表現力の限界なので致し方ないところではありますが。GroovyFXScalaFXでは言語の表現力を活かして書きやすくしてくれています。個人的にはScalaFXのバインディングの書き方は分かり易くていいなあと思っています。*3

*1:既存のJavaBeans仕様を満たす必要がない場合は省略するのもありです。

*2:最終的に実現する機能は同じですが、意味合いは大きく異なることに注意してください。この例では文字列を結合する処理がプレゼンテーション層に存在することになります。対して message プロパティを利用する例では、文字列を結合する処理はドメイン層に属することになります。

*3:自分のこのエントリでScalaFXのバインディングについて触れています。