第8章: AJAX 対応

Wicket はフレームワークレベルで AJAX をサポートしており、JavaScript を一行も書かずに ページの部分更新を実現できます。この章では、Wicket の AJAX の仕組みと主要なAJAXコンポーネントの使い方を解説します。

Wicket の AJAX とは

従来の Web フレームワークでは、AJAX を使うにはクライアント側の JavaScript コードと サーバー側の API エンドポイントを別々に作成し、JSON などでデータをやり取りする必要があります。

Wicket では、AJAX はコンポーネントの機能として組み込まれています。 開発者は「どのコンポーネントを更新するか」を Java コードで指定するだけで、 Wicket が自動的に以下を処理します:

AjaxRequestTarget

Wicket の AJAX の中核は AjaxRequestTarget です。 AJAX コールバック内で target.add(component) を呼ぶと、 そのコンポーネントだけが再描画され、ブラウザに差分として送信されます。

// AJAX コールバック内での典型的なパターン
@Override
protected void onSubmit(AjaxRequestTarget target) {
    // 1. データを更新
    label.setDefaultModelObject("更新されました");

    // 2. 更新したコンポーネントをターゲットに追加
    target.add(label);        // label だけが再描画される
    target.add(feedbackPanel); // 複数のコンポーネントを追加可能
}

setOutputMarkupId の重要性

AJAX で更新するコンポーネントには、必ず setOutputMarkupId(true) を設定する必要があります。 これにより HTML タグに id 属性が出力され、JavaScript が更新対象の DOM 要素を特定できるようになります。

Label label = new Label("message", "初期値");
label.setOutputMarkupId(true);  // AJAX 更新に必須!
add(label);

setOutputMarkupId(true) を忘れるのは、Wicket の AJAX で最もよくあるミスです。 設定し忘れると、AJAX リクエストは送信されますがコンポーネントが更新されません。

AjaxLink は、クリック時にページ全体をリロードせず、 AJAX でサーバー側の処理を実行するリンクです。通常の Link の AJAX 版です。

CounterPage.java
public class CounterPage extends WebPage {
    private int count = 0;

    public CounterPage() {
        final Label countLabel = new Label(
            "count", () -> String.valueOf(count));
        countLabel.setOutputMarkupId(true);
        add(countLabel);

        add(new AjaxLink<Void>("increment") {
            @Override
            public void onClick(AjaxRequestTarget target) {
                count++;
                target.add(countLabel);
            }
        });

        add(new AjaxLink<Void>("reset") {
            @Override
            public void onClick(AjaxRequestTarget target) {
                count = 0;
                target.add(countLabel);
            }
        });
    }
}
CounterPage.html
<html xmlns:wicket="http://wicket.apache.org">
<body>
  <h1>AJAX カウンター</h1>
  <p>現在のカウント:
    <span wicket:id="count">0</span>
  </p>
  <a wicket:id="increment">+1</a>
  <a wicket:id="reset">リセット</a>
</body>
</html>

+1 リンクをクリックすると、ページ全体のリロードなしにカウント表示だけが更新されます。 ブラウザの開発者ツールの Network タブを見ると、AJAX リクエストが飛んでいることが確認できます。

AjaxButton

AjaxButton は、フォームを AJAX で送信するボタンです。 通常の Button と異なり、ページ全体のリロードが発生しません。 バリデーションも通常のフォームと同様に動作します。

public class SearchPage extends WebPage {
    private String query = "";
    private String resultMessage = "";

    public SearchPage() {
        final FeedbackPanel feedback = new FeedbackPanel("feedback");
        feedback.setOutputMarkupId(true);
        add(feedback);

        final Label result = new Label("result", () -> resultMessage);
        result.setOutputMarkupId(true);
        add(result);

        Form<Void> form = new Form<>("searchForm");
        add(form);

        TextField<String> queryField =
            new TextField<>("query",
                new PropertyModel<>(this, "query"));
        queryField.setRequired(true);
        form.add(queryField);

        form.add(new AjaxButton("searchBtn") {
            @Override
            protected void onSubmit(AjaxRequestTarget target) {
                resultMessage = "「" + query + "」の検索結果: 42件";
                target.add(result);
                target.add(feedback);
            }

            @Override
            protected void onError(AjaxRequestTarget target) {
                // バリデーションエラー時は FeedbackPanel を更新
                target.add(feedback);
            }
        });
    }
}

AjaxButtononError() では、必ず FeedbackPaneltarget.add() に追加しましょう。そうしないとエラーメッセージが表示されません。

AjaxFallbackLink は、JavaScript が有効な環境では AJAX で動作し、 無効な環境では通常のページリロードにフォールバックするリンクです。 アクセシビリティや SEO を考慮する場合に有用です。

add(new AjaxFallbackLink<Void>("toggleLink") {
    @Override
    public void onClick(Optional<AjaxRequestTarget> targetOptional) {
        // データを更新
        showDetail = !showDetail;

        // AJAX が有効な場合のみ部分更新
        targetOptional.ifPresent(target -> {
            target.add(detailPanel);
        });
        // JavaScript が無効な場合は自動的にページ全体がリロードされる
    }
});

Wicket 10 では onClick の引数が Optional<AjaxRequestTarget> に 変更されています。Wicket 9 以前の AjaxRequestTarget(null の可能性あり)とは異なるので注意してください。

AjaxCheckBox

AjaxCheckBox は、チェックボックスの状態が変わるたびに AJAX でサーバーに通知するコンポーネントです。

WebMarkupContainer detailPanel = new WebMarkupContainer("detailPanel");
detailPanel.setOutputMarkupPlaceholderTag(true);
detailPanel.setVisible(false);
add(detailPanel);

add(new AjaxCheckBox("showDetails", Model.of(false)) {
    @Override
    protected void onUpdate(AjaxRequestTarget target) {
        detailPanel.setVisible(getModelObject());
        target.add(detailPanel);
    }
});

HTML:

<label>
  <input wicket:id="showDetails" type="checkbox"/> 詳細を表示
</label>
<div wicket:id="detailPanel">
  <p>ここに詳細情報が表示されます。</p>
</div>

タイマーで自動更新

AjaxSelfUpdatingTimerBehavior を使うと、 コンポーネントを一定間隔で自動的に AJAX 更新できます。 リアルタイムの時計やダッシュボードなどに使えます。

import java.time.Duration;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

Label clock = new Label("clock",
    () -> LocalTime.now().format(
        DateTimeFormatter.ofPattern("HH:mm:ss")));
clock.setOutputMarkupId(true);
clock.add(new AjaxSelfUpdatingTimerBehavior(
    Duration.ofSeconds(1)));
add(clock);

HTML: <span wicket:id="clock">00:00:00</span>

タイマーの間隔を短くしすぎると、サーバーに大きな負荷がかかります。 本番環境では適切な間隔(5秒以上など)を設定しましょう。 また、Wicket 10 では Durationjava.time.Duration を使用します。

AbstractAjaxTimerBehavior

更新処理をカスタマイズしたい場合は AbstractAjaxTimerBehavior を使います。

component.add(new AbstractAjaxTimerBehavior(Duration.ofSeconds(5)) {
    @Override
    protected void onTimer(AjaxRequestTarget target) {
        // カスタム更新ロジック
        if (shouldStop()) {
            stop(target); // タイマーを停止
        }
        target.add(statusLabel);
    }
});

AutoCompleteTextField

AutoCompleteTextField(wicket-extensions)は、 ユーザーの入力に応じてサジェスト候補を AJAX で表示するテキストフィールドです。

AutoCompleteTextField を使うには wicket-extensions への依存が必要です。

import org.apache.wicket.extensions.ajax.markup.html.autocomplete.AutoCompleteTextField;

List<String> allCities = Arrays.asList(
    "東京", "大阪", "名古屋", "福岡",
    "札幌", "仙台", "横浜", "神戸");

AutoCompleteTextField<String> cityField =
    new AutoCompleteTextField<String>("city", Model.of("")) {
        @Override
        protected Iterator<String> getChoices(String input) {
            return allCities.stream()
                .filter(c -> c.contains(input))
                .iterator();
        }
    };
form.add(cityField);

AJAX での表示/非表示の切り替え

AJAX でコンポーネントの表示/非表示を切り替える場合、 setOutputMarkupId(true) ではなく setOutputMarkupPlaceholderTag(true) を使う必要があります。

なぜ PlaceholderTag が必要なのか?

setVisible(false) のコンポーネントは HTML に出力されません。 そのため setOutputMarkupId(true) だけでは、非表示時に DOM 要素が存在しないため、 AJAX が更新先を見つけられません。

setOutputMarkupPlaceholderTag(true) を使うと、 非表示時でも空の <span style="display:none"> がプレースホルダーとして出力され、 AJAX で再表示できるようになります。

// 正しい方法: PlaceholderTag を使う
WebMarkupContainer panel = new WebMarkupContainer("togglePanel");
panel.setOutputMarkupPlaceholderTag(true); // これが重要!
panel.setVisible(false);
add(panel);

add(new AjaxLink<Void>("toggleBtn") {
    @Override
    public void onClick(AjaxRequestTarget target) {
        panel.setVisible(!panel.isVisible());
        target.add(panel);
    }
});

FeedbackPanel の AJAX 更新

AJAX フォーム送信で FeedbackPanel にエラーメッセージを表示するには、 FeedbackPanel も target.add() で更新する必要があります。

final FeedbackPanel feedback = new FeedbackPanel("feedback");
feedback.setOutputMarkupId(true);
add(feedback);

form.add(new AjaxButton("submit") {
    @Override
    protected void onSubmit(AjaxRequestTarget target) {
        success("保存しました");
        target.add(feedback); // 成功メッセージを表示
    }

    @Override
    protected void onError(AjaxRequestTarget target) {
        target.add(feedback); // エラーメッセージを表示
    }
});

AJAX のデバッグ

AJAX がうまく動かない場合のデバッグ方法です。

ブラウザの開発者ツール

ブラウザの開発者ツール(F12)の Network タブで AJAX リクエストを確認できます。 Wicket の AJAX リクエストは ?_= パラメータを含む XML レスポンスを返します。

Wicket Ajax Debug ウィンドウ

wicket-devutils を依存に追加し、開発モードで以下のように設定すると、 ブラウザ上に AJAX デバッグウィンドウを表示できます。

// WicketApplication の init() で設定
if (usesDevelopmentConfig()) {
    getDebugSettings().setDevelopmentUtilitiesEnabled(true);
}

よくあるトラブルと対処法

症状原因対処法
コンポーネントが更新されない setOutputMarkupId(true) を忘れている AJAX で更新する全コンポーネントに設定する
非表示→表示に切り替えできない setOutputMarkupPlaceholderTag(true) を忘れている 表示/非表示を切り替えるコンポーネントに設定する
FeedbackPanel にメッセージが出ない target.add(feedbackPanel) を忘れている onSubmitonError 両方で add する
AJAX リクエスト自体が発生しない JavaScript エラーが発生している ブラウザの Console タブでエラーを確認する
セッションタイムアウトエラー セッションが切れた後に AJAX リクエスト セッションタイムアウト時のハンドリングを実装する

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

  • 問題
    「表示する」に切り替えたのに、Ajax 更新後も画面に何も出ない。ボタンは反応しているのに見た目が変わらない。
    原因
    非表示時に DOM 要素そのものが出力されておらず、差し替える場所がない。Ajax が更新対象を見つけられない。
    対策
    表示切替するコンポーネントには setOutputMarkupPlaceholderTag(true) を設定する。非表示時もプレースホルダを残しておく。
  • 問題
    入力値を変えても、想定した Ajax 処理が実行されない。UI上は変更できているのに反応がない。
    原因
    イベント名を onchange で指定しており、バージョンに合っていない。Wicket 側がイベントを拾えない。
    対策
    Wicket 8 以降は change を使う。移行時はイベント名の差分を先にチェックする。
  • 問題
    クリックしても1回目は反応せず、2回目でやっと処理される。利用者には「ボタンが壊れている」ように見える。
    原因
    DOM 構造の崩れ、イベント競合、JavaScript エラーなどが混在している。Wicket 側だけ見ても原因が見つからないことがある。
    対策
    まずブラウザコンソールでエラー有無を確認し、イベントがどの要素にバインドされているかを検証する。
  • 問題
    セッション切れ後、ボタンを押しても画面が固まったように見える。ユーザーは何が起きたか判断できない。
    原因
    Ajax リクエストに通常画面遷移向けのリダイレクトを返している。Ajax クライアントが期待する応答形式になっていない。
    対策
    Ajax 向けのタイムアウトハンドリングを実装し、セッション切れ時はログイン画面へ復帰できる導線を用意する。
  • 問題
    StalePageException が出た瞬間に、利用者が次に何をすべきか分からなくなる。問い合わせにつながりやすい。
    原因
    例外がログに出るだけで、画面側に説明や復帰導線がない。利用者視点では「急に壊れた」ように見える。
    対策
    例外を捕捉して分かりやすいメッセージを出し、再読み込みや再操作へ誘導する。復帰手順を一貫させる。
  • 問題
    jQuery などで独自 Ajax を足したあと、stale エラーの発生頻度が急に上がる。再現条件が読みにくくなる。
    原因
    Wicket 管理下のページ URL を独自通信で直接呼び、renderCount の整合を壊している。Wicket の状態管理と衝突する。
    対策
    独自通信は専用エンドポイントへ分離し、Wicket のページ更新ルートと混在させない。役割を明確に分ける。
  • 問題
    初回表示では動く jQuery プラグインが、Ajax 更新後だけ効かなくなる。画面ごとに挙動差が出る。
    原因
    Ajax で DOM が再生成されたのに、プラグイン初期化が再実行されていない。初期表示時のバインドが失われる。
    対策
    Ajax 更新完了後にプラグイン初期化処理を再実行する。対象要素を限定して再バインドする。
  • 問題
    自作した入力コンポーネントだけ、Ajax 更新時に階層例外や更新失敗が起きる。標準部品では再現しない。
    原因
    無効化・非表示時に更新対象として必要なプレースホルダが出力されていない。DOM 上の位置が保持されない。
    対策
    自作コンポーネントでも placeholder 出力を有効化する。標準コンポーネントと同じ更新前提を満たす。