前回はListViewを使って要素を繰り返す処理について説明しました。Wicketでは繰り返しは「繰り返し項目コンポーネント」であるListViewで実現することができました。ListViewのpopulateItem()メソッドをオーバーライドし,ListItemにコンポーネントを追加することで,繰り返している1行を生成しました。
今回はWebアプリケーションには必須の項目である「リンク」を取り上げます。Twitterタイムラインの各行にはステータスに対する操作を行うためのリンクが置かれています。このリンクをWicketから制御します。
リンクを生成する
twitter.comの各ステータス行には,ステータスをお気に入り登録するためのリンクと,リプライをするためのリンクがあります。今回のサンプルでも同様のリンクを各ステータスにつけました。MyTimeline.htmlには,次のような2つの<a>タグがあります。
リスト1 お気に入りリンクとリプライリンク
<span class="actions">
<a wicket:id="favLink" class="fav" href="#"><span wicket:id="favName">fav</span></a>
<a wicket:id="replyLink" class="reply" href="#">reply</a>
</span>
お気に入りリンクにはfavLink,リプライリンクにはreplyLinkというwicket:idを付けています。さらに,favLinkは自身の子要素としてfavNameという<span>タグを持っています。この部分は,お気に入り登録をしていない状態では「fav」,している状態では「unfav」と表示が切り替わります。Wicketから表示を切り替えられるように,名前部分にも<span>タグを付けてコンポーネントを適用できるようにしているのです。
HTMLリンクにWicketのLinkコンポーネントを適用することで,クリック操作をプログラムで受け取ることができます。
LinkコンポーネントはHTMLの<a>タグはもちろん,「クリック可能にしたい場所」であればどこにでも付けることができます。<a>タグ以外では,<input type="button">や<button>タグに付けることが多いでしょう。もちろん,ただの<span>タグに付けることもできます。Linkコンポーネントは適用先が<a>であればhref属性を利用してクリックを処理します。適用先が<a>以外のタグであれば,JavaScriptを使ってクリックを処理します。この切り替えはLink自体が適用先タグを判断して行います。
いずれにせよ,Linkを適用した箇所をクリックすると,WicketはそれをLinkコンポーネントの「onClick()」メソッド呼び出しに変換します。Buttonコンポーネントで「onSubmit()」をオーバーライドしたように,onClick()メソッドをオーバーライドして,クリック時の処理をプログラムします。
Buttonコンポーネントをすでに見ているために当たり前のように見えることでしょう。しかし,サーブレットで表の上にあるボタンを正しく検知するのは,意外と難しいものです。行を特定するための情報が確実に受け渡しできるよう,考えながらプログラムする必要がありました。
Wicketでは,ListViewとLinkコンポーネントがすべての面倒を見てくれます。プログラマはただ,Linkを追加し,onClick()メソッドをオーバーライドするだけでいいのです。その他の面倒なことは,オブジェクトの中に隠されています。
リスト2 リプライリンクの実装
final Status status = item.getModelObject();
(中略)
item.add(new Link<Void>("replyLink") {
@Override
public void onClick() {
String targetScreenName = status.getUser().getScreenName();
form.insertText("@" + targetScreenName + " ");
}
});
このプログラムはreplyLinkがクリックされた時の動作をプログラムしています。onClick()内で使用しているstatusオブジェクトは,ListView#populateItem()メソッドによって提供される,1行分のTwitterステータスです。itemオブジェクトから取り出すことができます。
ここでは,statusオブジェクトからユーザのスクリーンネームを取り出し,formに渡しています。入力フォームに「@screenName」という文字列を挿入しているのです。
このformオブジェクトは,ログイン処理でも使用した「Form」クラスを継承して作成した,「TweetForm」クラスのインスタンスです。TweetFormクラスはMyTimelineクラス内にstatic内部クラスとして定義しています。MyTimelineページ以外では使いようのないクラスのため,MyTimelineページ内で定義しているのです。
このように機能毎にクラスを分けていくのは,Wicketでプログラムを作る上でのコツの1つです。Wicketでは匿名クラスを使えば,ページのコンストラクタ内にすべての処理を書くことも可能です。しかしある程度以上に大きくなった場合は,コンポーネント単位で共通処理をまとめて,サブクラスを定義すると良いでしょう。
ListItemの再生成を止める
WicketのListViewは,ページにアクセスされるたびにpopulateItemで表を作り直します。この動作は表の内容を常に最新状態に保つためには適切なものですが,今回のようにリスト内をクリックできる場合には,期待しない動作をする可能性があります。
ユーザのクリックやサブミットを処理したあと別のページに遷移しなかった場合,Wicketは同じページを再度表示します。Wicketはこのとき,ListViewの全項目を常に再作成します。これは,表をリロードした場合も含めて,確実に表の内容を最新状態に切り替えるための仕組みです。
ところが,行に入力項目が存在してユーザ入力を保存していたり,オブジェクト・フィールドでなんらかの状態をプログラムで記録している場合には,勝手に行を破棄されては困ります。
そのための重要な処理が,次の1行です。
リスト3 ListItemの再生成を抑止する
timeline.setReuseItems(true);
ListViewのsetReuseItems()にtrueを渡すことで,Wicketは同じページをリロードしてもListItemを再生成しなくなります。また,表の再生成処理を行わなくなるため,表が大きければ大きいほど,パフォーマンス面でも効果があります。
逆に,ListViewの内容を更新することがプログラマの責任となる点には注意してください。
setReuseItems(true)を呼び出したあとに表を再生成させるには,ListViewのremoveAll()メソッドを使用して,全行を明示的に削除する必要があります。
状況により表示を切り替える
お気に入り登録リンクも,考え方としてはほとんど同じです。しかしお気に入り登録リンクは一度クリックすると「お気に入り解除リンク」に切り替わる必要があります。サンプルでは,Statusオブジェクトの状態によって,リンクの動作を切り替えています。
リスト4 お気に入りリンクの実装
final Label favName = new Label("favName", new AbstractReadOnlyModel<String>() {
@Override
public String getObject() {
return status.isFavorited() ? "unfav" : "fav";
}
});
Link<Void> favLink = new Link<Void>("favLink") {
@Override
public void onClick() {
try {
Twitter twitterSession = AppSession.get().getTwitterSession();
if(status.isFavorited()) {
twitterSession.destroyFavorite(status.getId());
info(getString("favorateRemoved"));
} else {
twitterSession.createFavorite(status.getId());
info(getString("favorateRegistered"));
}
} catch (TwitterException ex) {
String message = getString("catNotCreateFavorite") + ": " + ex.getStatusCode();
error(message);
LOGGER.error(message, ex);
}
}
};
item.add(favLink);
favLink.add(favName);
表示の切り替えは,Labelコンポーネントに渡すモデルによって行っています。favNameラベルのモデルはAbstractReadOnlyModelの匿名サブクラスとして定義されており,StatusオブジェクトのisFavorited()メソッドを使って「お気に入り登録されているかどうか」をチェックして,「fav」もしくは「unfav」を返します。
Wicketはプッシュではなくプルを基本としているフレームワークであることを思い出してください。状況に応じて表示する文字列をラベルに設定するのではなく、ラベルのモデル自体が状況を判断して、表示すべき文字列を返すのです。プルを使うと、手続きの流れではなくオブジェクトを組み合わせてアプリケーションを作る、というWicketらしいオブジェクト指向アプリケーションになります。
favNameラベルは行を表すitemオブジェクトではなく,リンクを表すfavLinkオブジェクトに追加されている点に注意してください。HTML上でラベルを表す<span>タグは,リンクを表す<a>タグの内側にありました。Wicketのコンポーネント階層は常にタグの階層と一致することを思い出してください。
favLinkコンポーネントの実装はほかに比べると若干長いですが,いままでに見てきた手法を思い出せば簡単に理解できます。AppSessionからTwitterオブジェクトを取得し,isFavorited()の結果によって,登録処理を行うか,解除処理を行うかを決めているだけです。いずれの操作にせよ,うまくいけばinfo(),例外が発生すればerror()を使ってFeedbackPanelにメッセージを表示します。ここでもgetString()を使用することで,プログラム中に直接メッセージ文字列が埋め込まれないように配慮しています。
populateItem()メソッド内の実装方法は,ページを組み立てる場合とあまり変わらないことがわかるでしょう。変わるのは,コンポーネントを追加する先がページではなくListItemになることと,ListItemから各行毎のオブジェクトを取り出すことができる点のみです。

