このエントリはJavaFX Advent Calendarの26日目のエントリとなります。前日は id:skrb / @skrb さんによる「JavaFX で Merry Christmas!」です。25日を過ぎちゃいました。まさか1日目と最終日を担当することになるとは。
25日分を突破するわ、海外のJavaFXエンジニアの方々に注目されてエントリを英語に翻訳するようになるわで、まさかの盛り上がりとなりましたねえ。
さて、今回のエントリですが、同じくAdvent Calendarの13日目に@mike_neckさんが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つの方法があります。
- abstract な ReadOnly**PropertyBase を継承して実装する。
- 読み書き可能な 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 型のメソッドを確認してみてください。