Apache Click 2.2.0: Explicit Binding

BobによるClick 2.2.0の新機能紹介シリーズの翻訳第二弾です。原文は以下のエントリです。

今回はexplicit binding(明示的なバインディング)について触れます。これは開発者がコントロールの値をリクエストパラメータにバインドすることを可能にします。ただし、すべてのコントロールがバインディングをサポートするわけではありません。最も一般的なコントロールはField、Form、AbstractLinkです。明示的なバインディングは、たとえばonInitイベントやページクラスのコンストラクタなどで必要なタイミングでコントロールの値をバインドしたり、参照できるようにしたりできるようにすることで動的なページとフォームの振る舞いをシンプルにします。
明示的なバインディングについて説明する前に、暗黙的なバインディングについても言及しておく必要があります。暗黙的なバインディングはリクエストごとにonProcessイベントの一部として自動的に行われます。このためコントロールの値はonProcessイベントが発生すると自動的にセットされます。Cickのコントロールは値のバインドにbindRequestValue()メソッドを使用します。
典型的なbindRequestValue()メソッドの実装は以下のようなものです。

public void bindRequestValue() {
  Context context = getContext();
  String controlName = getName();
  String value = context.getRequestParameter(controlName);
  if(value != null) {
    setValue(value);
  }
}
 
// For completeness sake we show the onProcess implementation as well.
// onProcess delegates the binding logic to bindRequestValue
public void onProcess() {
  bindRequestValue();
  ...
}

通常、コントロールの名前がリクエストパラメータのルックアップに使用されますが、それ以外の方法ももちろん可能です。
ほとんどの場合、暗黙的なバインディングで充分なはずですが、稀にonProcessイベントが発生する前にコントロールの値を知りたいというケースがあるかもしれません。たとえば、onInit()イベントの中で別のFieldをFormに追加するために、ドロップダウンリストの中からユーザがどの値を選択したかをチェックする必要があるかもしれません。親のページ/コンテナに追加されたコントロールだけがonProcessイベントに参加することを思い出してください。このため、onProcessイベントの前にコントロールの生成と親のページ/コンテナへの追加を行う必要があるのです。
すでにご存知の通り、onInitイベントはonProcessイベントの前に発生します。そして、暗黙的なバインディングはonProcessイベントで行われるので、onInitイベントではコントロールの値を参照することはできません。
onInitイベントでコントロールの値を参照するにはどのようにしたらよいのでしょうか?ここで明示的なバインディングが登場します。明示的なバインディングは開発者がコントロールの値がいつバインドされるかを決めることを可能にします。明示的なバインディングを行うにはコントロールのbindRequestValue()メソッドを明示的に呼び出します(すべてのコントロールがリクエストパラメータのバインディングをサポートしているわけではなく、bindRequestValue()メソッドを提供していないコントロールも存在するという点に注意してください)。
直接bindRequestValue()メソッドを呼び出す場合、いくつかの注意点があります。

  • Fieldはそれらの親Formがサブミットされたときだけバインドされるべきです。それ以外の場合、もし1つのページに複数のフォームが存在するとForm2のフィールドをForm1のフィールドにバインドしてしまうかもしれません。
  • フォワードされたすでに処理が行われたリクエストをバインディングのために使用すべきではありません。

Clickはこれらの注意点を回避するためのヘルパーメソッドとして、ClickUtilsクラスにbind()メソッドとbindAndValidate()メソッドを提供しています。これらのメソッドはFormなどのコンテナに対して適用することもできます。その場合、その子コントロールの値もバインドされます。
bindAndValidate()メソッドは値のバインドとバリデーションの両方をFieldもしくはFormに対して行います。
また、コントロールの値をバインドする代わりにContextオブジェクトから直接リクエストパラメータを参照するという方法も覚えておくとよいでしょう。
次にいくつかの例を見てみましょう。
最初のサンプルではチェックボックスのチェックの有無によってTextFieldがFormに追加されるかどうかが決定されます。

public class DynamicFormDemo extends Page {
 
  @Override
  public void onInit() {
    super.onInit();
    Form form = new Form("form");
    addControl(form);
    Checkbox chk = new Checkbox("chk");
    form.add(chk);
 
    Submit ok = new Submit("ok");
    form.add(ok);
 
    // Explicitly bind the checkbox in the onInit event which allows us to query
    // whether the Checkbox was checked or not.
    ClickUtils.bind(chk);
    if(chk.isChecked()) {
      form.add(new TextField("name"));
    }
  }
}

Checkboxの値を明示的にバインドするためにClickUtils.bind()メソッドを使っています。これによってCheckboxがチェックされているかどうかを参照することができます。
2つ目の例はSelectフィールドを加えることで最初の例を拡張しています。

public class DynamicFormDemo extends Page {
 
  @Override
  public void onInit() {
    super.onInit();
    Form form = new Form("form");
    addControl(form);
    Checkbox chk = new Checkbox("chk");
    form.add(chk);
    Select countries = new CountrySelect("countries");
    countries.getOptionList().add(Option.EMPTY_OPTION);
    form.add(countries);
 
    Submit ok = new Submit("ok");
    form.add(ok);
 
    // Explicitly bind the Form (and all it's child controls) in the onInit
    // event, allowing us to query whether he user checked the Checkbox and
    // which country was selected.
    ClickUtils.bind(form);
    if (chk.isChecked()) {
      form.add(new TextField("name"));
    }
 
    if (StringUtils.isNotBlank(countries.getValue())) {
      form.add(new TextField("location"));
    }
  }
}

CheckboxとSelectを別々にバインドする代わりにFormに対してClickUtils.bind()メソッドを呼び出していることに注意してください。Formなどのコンテナに対してClickUtils.bind()を呼び出すと、コンテナが持つすべてのバインド可能なコントロールに値が場院dのされます。これは複数のコントロールの値をバインドするための簡単なショートカットです。
最後のサンプルはFormのバインドとバリデーションの両方を行う場合の例です。

public class DynamicFormDemo extends BorderPage {
 
  @Override
  public void onInit() {
    super.onInit();
    Form form = new Form("form");
    addControl(form);
    Checkbox chk = new Checkbox("chk");
    form.add(chk);
    Select countries = new CountrySelect("countries", true);
    countries.getOptionList().add(Option.EMPTY_OPTION);
    form.add(countries);
 
    Submit ok = new Submit("ok");
    form.add(ok);
 
    // Explicitly bind and check that the Form (and all it's child
    // controls) is valid in the onInit event, allowing us to safely
    // query whether he user checked the Checkbox and
    // which country was selected.
    if (ClickUtils.bindAndValidate(form)) {
      if (chk.isChecked()) {
        form.add(new TextField("name"));
      }
 
      // The form validation passed and since the countries field is required
      // we can safely assume that a valid country has been selected
      form.add(new TextField("location"));
    }
  }
}

ClickUtils.bindAndValidate()メソッドはバインドとバリデーションを行い、もしバリデーションがパスすればtrueを返却します。それ以外の場合はfalseを返却します。