第十二戒: 巡礼の道 〜 ページからページへ渡り歩け

「setResponsePage は瞬間移動、BookmarkablePageLink は巡礼の道標なり。目的に応じて使い分けよ」 ーー Wicket 聖典 第十二戒より

Wicket のページ遷移は、通常の Web フレームワークとは少し異なります。 ステートフルなページとブックマーク可能なページの概念を理解することが重要です。 この章では、ページ遷移の各種方法とURL マウントについて解説します。

方法用途ブックマーク可能
setResponsePage(new Page()) インスタンスを渡して遷移 いいえ(ステートフル)
setResponsePage(Page.class) クラスを渡して遷移 はい
BookmarkablePageLink リンクコンポーネント はい
Link + setResponsePage 処理後に遷移 遷移先による

setResponsePage

インスタンスを渡す方法(ステートフル)

ページのインスタンスを直接渡すと、そのインスタンスがレスポンスとして使われます。 コンストラクタでデータを渡したい場合に使います。

// Link の onClick 内で遷移
add(new Link<Void>("detailLink") {
    @Override
    public void onClick() {
        User user = getSelectedUser();
        setResponsePage(new UserDetailPage(user));
    }
});

// フォームの onSubmit 内で遷移
Form<Void> form = new Form<>("form") {
    @Override
    protected void onSubmit() {
        // 処理を実行してから遷移
        userService.save(user);
        setResponsePage(new UserListPage());
    }
};

インスタンスを渡す方法では、URL はランダムなセッション固有のものになります(例: ?2-1.ILinkListener-detailLink)。 ブラウザのブックマークや URL の共有には向きません。

クラスを渡す方法(ブックマーク可能)

ページクラスを渡すと、Wicket がそのクラスのデフォルトコンストラクタ(または PageParameters を受け取るコンストラクタ)を呼び出して新しいインスタンスを生成します。

// クラスだけを指定(デフォルトコンストラクタが呼ばれる)
setResponsePage(HomePage.class);

// パラメータ付きで遷移
PageParameters params = new PageParameters();
params.add("id", 42);
setResponsePage(UserDetailPage.class, params);

BookmarkablePageLink は、ブックマーク可能な URL を生成するリンクコンポーネントです。 通常の <a href="..."> タグとして出力されます。

// 基本的な使い方
add(new BookmarkablePageLink<>("homeLink", HomePage.class));

// パラメータ付き
PageParameters params = new PageParameters();
params.add("id", userId);
add(new BookmarkablePageLink<>("userLink",
    UserDetailPage.class, params));

HTML:

<a wicket:id="homeLink">聖堂</a>
<a wicket:id="userLink">ユーザー詳細</a>

SEO やブックマーク対応が必要なリンクには BookmarkablePageLink を使いましょう。 サーバー側処理が必要なリンクには Link + setResponsePage() を使います。

PageParameters

PageParameters は、URL パラメータをページに渡すためのクラスです。 ブックマーク可能なページでデータを受け渡す唯一の方法です。

パラメータの設定と取得

// パラメータの設定
PageParameters params = new PageParameters();
params.add("id", 42);
params.add("tab", "profile");
params.add("sort", "name");

// パラメータの取得(ページのコンストラクタで)
public UserDetailPage(PageParameters params) {
    Long id = params.get("id").toLong();
    String tab = params.get("tab").toString("info"); // デフォルト値付き
    String sort = params.get("sort").toOptionalString(); // null 可

    // パラメータが存在するかチェック
    if (params.get("id").isNull()) {
        throw new AbortWithHttpErrorCodeException(400);
    }
}

インデックスパラメータ

名前付きパラメータに加えて、インデックス(位置)ベースのパラメータも使えます。

// インデックスパラメータの設定
PageParameters params = new PageParameters();
params.set(0, "users");    // 1番目のパス要素
params.set(1, 42);          // 2番目のパス要素
// URL: /page/users/42

// インデックスパラメータの取得
String type = params.get(0).toString();
Long id = params.get(1).toLong();

マウント URL

デフォルトでは Wicket のブックマーク可能な URL は /wicket/bookmarkable/com.example.UserPage のような長い形式になります。mountPage() を使うと、 わかりやすい URL をページにマッピングできます。

// WicketApplication の init() で設定
@Override
public void init() {
    super.init();

    // 基本的なマウント
    mountPage("/users", UserListPage.class);
    mountPage("/about", AboutPage.class);

    // パラメータ付きマウント
    mountPage("/user/${id}", UserDetailPage.class);
    // URL: /user/42 → PageParameters で id=42 として取得

    // オプショナルパラメータ(#{} を使用)
    mountPage("/user/${id}/#{tab}", UserDetailPage.class);
    // URL: /user/42 も /user/42/profile も有効
}

マウントパターンの記法

記法意味
${param} 必須パラメータ /user/${id}/user/42
#{param} オプショナルパラメータ /user/${id}/#{tab}/user/42 or /user/42/profile
固定文字列 URL のパス部分 /admin/users

マウント URL を使うと、SEO フレンドリーで覚えやすい URL になります。 すべてのブックマーク可能なページにマウント URL を設定することを推奨します。

リダイレクト

RestartResponseException

処理を即座に中断して別のページにリダイレクトしたい場合は RestartResponseException を使います。

// 認証チェック: 未ログインならログインページへリダイレクト
public SecurePage() {
    if (!isUserLoggedIn()) {
        throw new RestartResponseAtInterceptPageException(
            LoginPage.class);
    }
    // 通常の初期化処理...
}

// ログイン成功後、元のページに戻る
public void onLoginSuccess() {
    continueToOriginalDestination();
    // 元のページがない場合のフォールバック
    setResponsePage(HomePage.class);
}

ステートフル vs ステートレス

ページ遷移の方法によって、ページのステートフル/ステートレスが変わります。

観点ステートフルステートレス
URL セッション固有のID付き きれいなURL(マウント可能)
セッション ページがセッションに保存される セッション不要
戻るボタン ページストアから復元 毎回新規生成
スケーラビリティ セッションメモリが必要 メモリ消費少
用途 フォーム、AJAX を使うページ 表示のみのページ、一覧

エラーページ

Wicket では、エラーページもカスタマイズできます。

@Override
public void init() {
    super.init();

    // 404 Not Found ページ
    getApplicationSettings()
        .setPageExpiredErrorPage(PageExpiredPage.class);

    // 500 Internal Error ページ
    getApplicationSettings()
        .setInternalErrorPage(InternalErrorPage.class);

    // アクセス拒否ページ
    getApplicationSettings()
        .setAccessDeniedPage(AccessDeniedPage.class);
}
やりたいこと使う方法
単純なページ間リンク BookmarkablePageLink
パラメータ付き遷移 BookmarkablePageLink + PageParameters
処理後の遷移 setResponsePage()
きれいな URL mountPage() で設定
認証リダイレクト RestartResponseAtInterceptPageException

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

  • 問題
    ログインに成功しても、さっき開こうとしていたページへ戻れない。利用者体験が分断される。
    原因
    認証前の遷移先を保持せず、ログイン後に固定ページへ飛ばしている。意図した導線が失われる。
    対策
    認証成功時に元の遷移先へ復帰させる導線を実装する。途中遷移でも目的画面に戻れるようにする。
  • 問題
    URL を共有しても同じ画面が再現できず、サポートや運用が難しくなる。ブックマーク運用にも向かない。
    原因
    パラメータを隠すために stateful 遷移へ寄せすぎている。再訪可能なURLの利点を捨ててしまっている。
    対策
    「秘匿」と「再現性」の優先度を先に決め、必要最小限だけ stateful 化する。公開導線は bookmarkable を優先する。
  • 問題
    戻る操作やタブ複製をしただけで、PageExpiredException / StalePageException が発生する。通常操作に見えるため混乱しやすい。
    原因
    古い renderCount を持つページ状態で再操作している。Wicket の状態整合チェックに引っかかる。
    対策
    再読み込みで復帰できる導線とユーザー通知を用意する。更新系画面では多重操作の抑止も入れる。
  • 問題
    同じ画面を複数タブで開くと、片方の更新が失敗しやすい。利用者には不規則な不具合に見える。
    原因
    同一ページインスタンスを並行操作し、状態整合が崩れている。更新順やタイミングの競合が起こる。
    対策
    多重タブ利用を前提に、操作制限や画面設計を見直す。必要なら編集画面に排他制御を入れる。
  • 問題
    想定していない操作でも stale 扱いになり、ユーザーが戸惑う。問い合わせ時に再現説明が難しい。
    原因
    View Source など通常外の操作でも状態差分が生じる場合がある。利用者の操作意図と例外理由が一致しない。
    対策
    エラー文言に「再読み込みで復帰」を明記し、サポート手順を統一する。復旧方法を迷わせない。
  • 問題
    StalePageException の調査に時間がかかり、復旧が遅れる。ログはあるのに原因が特定できない。
    原因
    ログ解析だけで追っており、実際の操作シナリオを確認していない。再現条件の軸が定まっていない。
    対策
    まず別タブ、戻る、二重送信の再現条件から切り分ける。操作ログと例外ログをセットで確認する。