torutkのブログ

ソフトウェア・エンジニアのブログ

TableViewで既に行として表示済みのデータの列を更新する際の注意点

JavaFX 2のTableViewを使う際、行データをインサート、削除する場合は自動的に描画が更新されます。しかし、既にTableViewが抱える行データの列(属性)の値を変更するときは、注意点が必要です。

次のJavaFXチュートリアルでTableViewのサンプルが紹介されています。

TableViewの行データを表すPersonクラス(ネストクラス)のコード

public static class Person {
  private final SimpleStringProperty firstName;
  private final SimpleStringProperty lastName;
  private final SimpleStringProperty email;

  private Person(String fName, String lName, String email) {
    this.firstName = new SimpleStringProperty(fName);
    this.lastName = new SimpleStringProperty(lName);
    this.email = new SimpleStringProperty(email);
  }
  
  public String getFirstName() {
    return firstName.get();
  }
  public void setFirstName(String fName) {
    firstName.set(fName);
  }
  
  public String getLastName() {
    return lastName.get();
  }
  public void setLastName(String lName) {
    lastName.set(lName);
  }

  public String getEmail() {
    return email.get();
  }
  public void setEmail(String email) {
    email.set(email);
  }
}

このPersonをデータとするTableViewがあります。

たとえば、emailを変更した人物がいた場合、理想では

  Person person = ...
  person.setEmail(newEmail);

とデータを変更するだけで表示が更新されるのが理想です。
しかし、実際はこれではTableViewの変更がかかりません。

スクロールをする等で該当人物の表示行が更新されると、
email欄も変更後になります。

つまり、TableViewが持つObservableListにPersonインスタンス
出し入れするような変更を行ったときは、その変更を検知してTableViewの
表示が変わります。しかし、ObservableListの1個の要素であるPerson
インスタンスの属性だけが変わるような変更を行ったときは、その変更を
TableViewが検知することができないようです。

この問題の回避策として、TableViewが持つObservableListの1つ目の
要素を削除して再挿入する、TableViewのlayoutメソッドを呼ぶ、などの
手段でTableViewに再描画させる方法を見かけました。

しかし、属性1つの変更でTableView全体を再描画するのは効率が悪く
なるべく避けたいところです。

解決策は、行を表すデータクラスにバインドのためのオブジェクトである
SimpleStringPropertyを取り出すメソッドを決まった命名規約で定義する
ことです。

Personクラスに追加するメソッド

  public StringProperty firstNameProperty() {
    return firstName;
  }
  public StringProperty lastNameProperty() {
    return lastName;
  }
  public StringProperty emailProperty() {
    return email;
  }

ここで、データを表の列に対応づけているコードを抜粋します。

  firstNameCol.setCellValueFactory(
      new PropertyValueFactory<Person, String>("firstName")
  );
  ...

ここで、PropertyValueFactoryクラスのJavadocを見ると

この例では、文字列"firstName"が指定されたことにより、Personクラス型にfirstNameProperty() メソッドがあることが想定される。このメソッドはPropertyインスタンスを返却しなければならない。...(中略)...さらに、TableViewは自動的にこの戻り値に対するオブザーバーを設ける。オブザーバーに変更が伝達されると、TableViewは直ちにセルを更新する。

とあります。

また、次の記事(日本語)でもこのメソッドを定義する話が言及されています。

はまりポイント

スペルミスには注意しましょう。
namePropertyとするところを誤ってnameProperyとしてしまい、更新されないとしばらく悩んでしまいました。

謝辞

この問題に出くわして原因がわからずTwitterにツイートしたときに、対処方法を教えていただいた@aoetkさん、@skrbさんに感謝いたします。