第9章: Behavior

Behavior は Wicket の最も強力な機能のひとつです。 コンポーネントを継承せずに、プラグインのように機能を追加できます。 HTML 属性の操作、AJAX イベントの追加、CSS/JavaScript の注入など、 さまざまな用途に使えます。

Behavior とは

Behavior は、コンポーネントに「後付け」で機能を追加する仕組みです。 デザインパターンでいうと Decorator パターン に相当します。

例えば、ある Label にツールチップを追加したいとします。 Label を継承して ToolTipLabel を作ることもできますが、 Behavior を使えば任意のコンポーネントにツールチップ機能を追加できます。

// コンポーネントに Behavior を追加する基本パターン
Label label = new Label("name", "田中太郎");
label.add(new AttributeModifier("title", "社員名を表示しています"));
label.add(new AttributeModifier("style", "font-weight:bold"));
add(label);
// 結果: <span title="社員名を表示しています" style="font-weight:bold">田中太郎</span>

Behavior の利点:

Behavior のライフサイクル

Behavior は以下のライフサイクルメソッドを持ちます。 これらをオーバーライドして機能を実装します。

メソッドタイミング用途
onComponentTag() コンポーネントのタグ出力時 HTML 属性の追加・変更
renderHead() ヘッダー出力時 CSS/JavaScript の追加
beforeRender() コンポーネント描画前 描画前の準備処理
afterRender() コンポーネント描画後 描画後の後処理
bind() コンポーネントに追加された時 初期化処理
unbind() コンポーネントから削除された時 クリーンアップ

AttributeModifier

AttributeModifier は、HTML タグの属性を追加・変更する最も基本的な Behavior です。 Wicket で最も頻繁に使われる Behavior のひとつです。

属性の設定(置換)

// class 属性を設定する
component.add(new AttributeModifier("class", "highlight"));

// style 属性を設定する
component.add(new AttributeModifier("style", "color:red"));

// data 属性を設定する
component.add(new AttributeModifier("data-id", userId));

// 動的な値(モデルを使用)
component.add(new AttributeModifier("title",
    new PropertyModel<>(user, "fullName")));

ファクトリメソッド

AttributeModifier には便利なファクトリメソッドが用意されています。

// replace: 既存の値を置換(new AttributeModifier と同じ)
component.add(AttributeModifier.replace("class", "new-class"));

// append: 既存の値に追加(スペース区切り)
component.add(AttributeModifier.append("class", "additional-class"));
// 結果: class="existing-class additional-class"

// prepend: 既存の値の前に追加
component.add(AttributeModifier.prepend("class", "first-class"));

// remove: 属性を削除
component.add(AttributeModifier.remove("style"));

AttributeAppender

AttributeAppenderAttributeModifier.append() と同じ機能のクラスです。 CSS クラスを追加する場合に特に便利です。

// CSS クラスを条件付きで追加
if (isActive) {
    component.add(AttributeModifier.append("class", "active"));
}
if (hasError) {
    component.add(AttributeModifier.append("class", "error"));
}
// 結果: class="active error"(両条件がtrueの場合)

AjaxEventBehavior

AjaxEventBehavior は、任意の JavaScript イベント(click, mouseover, keydown 等)に AJAX コールバックを紐づける Behavior です。 これにより、任意のコンポーネントを AJAX 対応にできます。

WebMarkupContainer box = new WebMarkupContainer("clickableBox");
box.setOutputMarkupId(true);
add(box);

box.add(new AjaxEventBehavior("click") {
    @Override
    protected void onEvent(AjaxRequestTarget target) {
        // クリックされた時の処理
        clickCount++;
        info("ボックスがクリックされました(" + clickCount + "回目)");
        target.add(feedbackPanel);
    }
});

さまざまなイベントに対応できます:

イベント名発生タイミング
"click"クリック時
"dblclick"ダブルクリック時
"mouseover"マウスオーバー時
"mouseout"マウスアウト時
"keydown"キーダウン時
"keyup"キーアップ時
"focus"フォーカス時
"blur"フォーカスが外れた時

OnChangeAjaxBehavior

OnChangeAjaxBehavior は、フォームコンポーネントの値が変わった時に AJAX コールバックを実行する Behavior です。 連動するドロップダウン(都道府県→市区町村)などの実装に便利です。

// 都道府県の選択に応じて市区町村リストを更新する例
DropDownChoice<String> prefectureChoice =
    new DropDownChoice<>("prefecture",
        new PropertyModel<>(this, "selectedPrefecture"),
        prefectures);

final DropDownChoice<String> cityChoice =
    new DropDownChoice<>("city",
        new PropertyModel<>(this, "selectedCity"),
        new PropertyModel<>(this, "availableCities"));
cityChoice.setOutputMarkupId(true);

prefectureChoice.add(new OnChangeAjaxBehavior() {
    @Override
    protected void onUpdate(AjaxRequestTarget target) {
        // 都道府県が変わったら市区町村リストを更新
        updateCityList(selectedPrefecture);
        target.add(cityChoice);
    }
});

form.add(prefectureChoice);
form.add(cityChoice);

AjaxFormSubmitBehavior

AjaxFormSubmitBehavior は、指定したイベントが発生した時に フォームを AJAX で送信する Behavior です。 例えば、Enter キーを押した時にフォームを送信する、といった使い方ができます。

textField.add(new AjaxFormSubmitBehavior(form, "keydown") {
    @Override
    protected void onSubmit(AjaxRequestTarget target) {
        // フォーム送信成功時の処理
        target.add(resultLabel);
    }

    @Override
    protected void onError(AjaxRequestTarget target) {
        target.add(feedbackPanel);
    }
});

ヘッダーへの CSS/JS 追加

Behavior の renderHead() メソッドをオーバーライドすると、 <head> タグに CSS や JavaScript を追加できます。 コンポーネントが使われている時だけ必要な CSS/JS を読み込む場合に有用です。

CSS ファイルの追加

public class HighlightBehavior extends Behavior {
    @Override
    public void renderHead(Component component,
                           IHeaderResponse response) {
        super.renderHead(component, response);

        // CSS ファイルを追加
        response.render(CssHeaderItem.forReference(
            new CssResourceReference(
                HighlightBehavior.class, "highlight.css")));

        // インライン CSS を追加
        response.render(CssHeaderItem.forCSS(
            ".highlight { background: yellow; }", "highlight-style"));
    }
}

JavaScript ファイルの追加

public class ChartBehavior extends Behavior {
    @Override
    public void renderHead(Component component,
                           IHeaderResponse response) {
        super.renderHead(component, response);

        // JavaScript ファイルを追加
        response.render(JavaScriptHeaderItem.forReference(
            new JavaScriptResourceReference(
                ChartBehavior.class, "chart.js")));

        // インライン JavaScript を追加
        response.render(OnDomReadyHeaderItem.forScript(
            "console.log('DOM ready!');"));
    }
}

OnDomReadyHeaderItem は DOM の読み込み完了後に実行されるスクリプトを追加します。 jQuery の $(document).ready() と同等です。 コンポーネントの初期化スクリプトに適しています。

カスタム Behavior の作成

独自の Behavior を作成することで、プロジェクト固有の機能をコンポーネントに追加できます。 以下にいくつかの実践的な例を示します。

例1: ツールチップ Behavior

public class TooltipBehavior extends Behavior {
    private final IModel<String> tooltipModel;

    public TooltipBehavior(String tooltip) {
        this(Model.of(tooltip));
    }

    public TooltipBehavior(IModel<String> tooltipModel) {
        this.tooltipModel = tooltipModel;
    }

    @Override
    public void onComponentTag(Component component,
                               ComponentTag tag) {
        super.onComponentTag(component, tag);
        tag.put("title", tooltipModel.getObject());
        tag.put("data-toggle", "tooltip");
    }
}

// 使い方
label.add(new TooltipBehavior("この項目の説明です"));

例2: 確認ダイアログ Behavior

public class ConfirmBehavior extends Behavior {
    private final String message;

    public ConfirmBehavior(String message) {
        this.message = message;
    }

    @Override
    public void onComponentTag(Component component,
                               ComponentTag tag) {
        super.onComponentTag(component, tag);
        tag.put("onclick",
            "return confirm('" + message + "');");
    }
}

// 使い方: 削除リンクに確認ダイアログを追加
deleteLink.add(new ConfirmBehavior("本当に削除しますか?"));

例3: 条件付き CSS クラス Behavior

public class ConditionalCssBehavior extends Behavior {
    private final String cssClass;
    private final SerializableBooleanSupplier condition;

    public ConditionalCssBehavior(String cssClass,
                                  SerializableBooleanSupplier condition) {
        this.cssClass = cssClass;
        this.condition = condition;
    }

    @Override
    public void onComponentTag(Component component,
                               ComponentTag tag) {
        super.onComponentTag(component, tag);
        if (condition.getAsBoolean()) {
            String existing = tag.getAttribute("class");
            if (existing == null) {
                tag.put("class", cssClass);
            } else {
                tag.put("class", existing + " " + cssClass);
            }
        }
    }
}

// 使い方: 在庫がない場合に "out-of-stock" クラスを追加
row.add(new ConditionalCssBehavior("out-of-stock",
    () -> product.getStock() == 0));

Behavior vs コンポーネント継承

機能を追加する方法として「コンポーネントの継承」と「Behavior の追加」のどちらを選ぶべきでしょうか。

観点Behaviorコンポーネント継承
適用範囲 任意のコンポーネントに適用可能 特定のクラス階層に限定
組み合わせ 複数の Behavior を同時に適用可能 Java は単一継承のため1つだけ
独自のマークアップ 持てない Panel なら独自の HTML を持てる
向いている場面 属性操作、イベント処理、CSS/JS 追加 独自 UI を持つ再利用コンポーネント

判断基準: 独自の HTML マークアップが必要なら Panel(コンポーネント)、 既存のコンポーネントに機能を付加するなら Behavior を使いましょう。 迷ったら Behavior を選ぶ方が柔軟性が高くなります。

組み込み Behavior 一覧

Wicket には多くの組み込み Behavior が用意されています。主要なものをまとめます。

属性操作

クラス説明
AttributeModifierHTML 属性を設定・置換・削除
AttributeAppenderHTML 属性に値を追加

AJAX 関連

クラス説明
AjaxEventBehavior任意の JS イベントで AJAX コールバック
AjaxFormSubmitBehaviorイベント発生時に AJAX フォーム送信
AjaxFormChoiceComponentUpdatingBehavior選択系コンポーネントの AJAX 更新
OnChangeAjaxBehavior値変更時の AJAX コールバック
AjaxSelfUpdatingTimerBehavior定期的な AJAX 自動更新
AbstractAjaxTimerBehaviorカスタム定期更新の基底クラス

その他

クラス説明
AbstractDefaultAjaxBehaviorカスタム AJAX Behavior の基底クラス
Behaviorすべての Behavior の基底クラス

実務でハマりやすい注意点

  • 問題
    Behavior を追加したのに Ajax がまったく反応しない。コード上は正しそうに見えるため、原因に気づきにくい。
    原因
    Wicket のバージョン差分でイベント名が変わっており、古い書き方のままになっている。イベントが発火条件に一致していない。
    対策
    対象バージョンのイベント名を確認し、change など正しい名称へ統一する。移行時は一覧で見直す。
  • 問題
    同じ操作でも効いたり効かなかったりして、挙動が安定しない。現場では「たまに失敗する」不具合として報告される。
    原因
    Behavior 実装だけでなく、DOM構造やJavaScript側に不整合がある。イベント伝播や生成タイミングで取りこぼしが起きる。
    対策
    イベント伝播、要素生成タイミング、JavaScript エラーを合わせて確認する。Wicket側とフロント側を分けて切り分ける。
  • 問題
    表示制御まわりが重くなり、画面描画が遅くなる。さらに想定外の再評価で挙動が不安定になる。
    原因
    重い判定ロジックを isVisible() に書いているため、描画中に何度も評価される。副作用がある実装だと特に崩れやすい。
    対策
    表示判定は onConfigure() に寄せて1箇所で制御する。副作用のない判定にして再評価コストを抑える。