第八戒: AJAX の神通力 〜 ページを壊さずして世界を変えよ
Wicket はフレームワークレベルで AJAX をサポートしており、JavaScript を一行も書かずに ページの部分更新を実現できます。この章では、Wicket の AJAX の仕組みと主要なAJAXコンポーネントの使い方を解説します。
Wicket の AJAX とは
従来の Web フレームワークでは、AJAX を使うにはクライアント側の JavaScript コードと サーバー側の API エンドポイントを別々に作成し、JSON などでデータをやり取りする必要があります。
Wicket では、AJAX はコンポーネントの機能として組み込まれています。 開発者は「どのコンポーネントを更新するか」を Java コードで指定するだけで、 Wicket が自動的に以下を処理します:
- AJAX リクエストの送信(JavaScript 自動生成)
- サーバー側でのコンポーネント再レンダリング
- ブラウザ上での DOM 部分更新
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
AjaxLink は、クリック時にページ全体をリロードせず、
AJAX でサーバー側の処理を実行するリンクです。通常の Link の AJAX 版です。
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);
}
});
}
}
<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);
}
});
}
}
AjaxButton の onError() では、必ず FeedbackPanel を
target.add() に追加しましょう。そうしないとエラーメッセージが表示されません。
AjaxFallbackLink
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 では Duration に java.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) を忘れている |
onSubmit と onError 両方で 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 出力を有効化する。標準コンポーネントと同じ更新前提を満たす。