Wicketで始めるオブジェクト指向ウェブ開発

第6回Twitterタイムラインで見るWicketのオブジェクト指向プログラミング(後編)

前回は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から各行毎のオブジェクトを取り出すことができる点のみです。

つぶやきを投稿する

最後にあなたのつぶやきをTwitterにポストする処理を書いて、このプログラムを完成させましょう。

つぶやきの投稿機能はFormのサブクラスTweetFormクラス内にあります。TweetFormコンストラクタでButtonオブジェクトを1つ作っています。

リスト5 つぶやきを投稿する
add(new Button("post") {
  @Override
  public void onSubmit() {
    if(tweet != null && tweet.length() > 0) {
      try {
        Twitter twitterSession = AppSession.get().getTwitterSession();
        twitterSession.updateStatus(tweet);
        setResponsePage(MyTimeline.class);
      } catch (TwitterException ex) {
        String message = getString("updateFailed");
        LOGGER.error(message, ex);
        error(message);
      }
    }
  }
});

このプログラム中で使用しているtweetという変数はTweetFormクラスのフィールドで、ユーザの入力したつぶやきを格納しています。引用したプログラム内には含まれていませんが、PropertyModelによって入力値が格納されるよう、設定しています。

ボタンが押されたときにユーザ入力値が空でなければ、TwitterオブジェクトのupdateStatus()メソッドを使ってTwitterに投稿しているのです。非常にシンプルです。

ここまでWicketのプログラムを見ていると、もはや当たり前のプログラムに見えることでしょう。しかしその裏では、Wicketによるイベント振り分けやモデルによるフィールド変更、バリデータによる入力チェックなどが動いているのです。ボタンが押されたときの動作をボタンに書く、というこのプログラミングスタイルを実現するために、Wicketがさまざまな処理を行ってくれているのです。

サブミット後のURLをきれいに保つ

Buttonのプログラムは、最後に「setResponsePage(MyTimeline.class);」を呼び出します。実は、この1行が無くともWicketはMyTimelineページを再度表示します。WicketはsetResponsePage()呼び出しが為されなかった場合には現在と同じページを再表示するからです。

しかしこの呼び出しには別の意味があります。

Wicketにはユーザ入力をページに保存して、入力前と入力後のページを区別します。そのために、フォームがサブミットされた後の結果ページには、Wicketが自動的にURLを割り振ります。Wicketは「ページID」と呼ばれる番号を元にURLを生成して、ブラウザがどのページにアクセスしようとしているのかを管理するのです。

フォームをサブミットすると、Wicketは次のようなURLを生成してページを再表示します。

http://localhost:8080/wicket-sample/?wicket:interface=:1::::

ユーザ入力によって状態が変わったページには、固定のURLは存在し得ません。ページの状態は入力内容によって次々と変わりますし、無数のパターンが存在するからです。そのため、フォームがサブミットされると、Wicketは自動的に「一時的なURL」を生成するのです。サブミットによって状態が変わったページを表すURLです。

しかし、さすがにこのURLは綺麗とは言えません。ユーザ入力途中の確認ページなどであれば、一時的なURLで問題ないでしょう。そういったページは決してブックマークされませんし、あとからアクセスすることも出来ない、本当に一時的なページだからです。しかし、タイムラインを表示するページは固定のURLを持つべきでしょう。

今回のプログラムの場合、ユーザのつぶやきがポストされたあとには、ユーザ入力値は必要ありません。フォームは空になり、タイムラインも新しく再表示して、まっさらなページを作ればいいのです。

setResponsePage(MyTimeline.class)の呼び出しは、フォームのサブミット後のページを破棄して、新しいMyTimelineページを生成して表示することを指示しているのです。

WicketはsetResponsePage()メソッドにクラスが渡された場合は常に、そのページクラスがマウントされているかどうかをチェックします。第1回で紹介したmountBookmarkablePage()メソッドを思い出してください。

MyTimelineページは、WicketApplicationクラス内で「/friends」というURLにマウントされています。setResponsePage(MyTimeline.class)を呼び出すことにより、ブラウザはWicketが自動生成したページID付きURLではなく、⁠/friends」にアクセスします。そしてMyTimelineクラスから新しいページを生成します。初めてこのページにアクセスした時とまったく同じように、ページを構築するのです。

この操作により、ブラウザのURLは綺麗なままに保たれます。

Wicketには「状態を持ったページ」「状態を持たないページ」が存在します。状態を持ったページはWicketの管理下にあり、その時々にURLが割り振られます。このようなページはユーザ入力途中の一時的なページであることが通例で、いずれは入力が確定し、URLの固定したページに戻ってくるでしょう。これが「状態を持たないページ」です。

この2つの違いを意識して、⁠どのページが綺麗なURLで表示される必要があるか」と決めておくことが重要です。綺麗なURLを持つべきページへは「setResponsePage(ページクラス)」によって遷移し、ページクラスをURLにマウントしておくことです。

次回:ページングの実装

ここまでで、シンプルなTwitterアプリケーションがひととおり完成しました。

これから先は、このアプリケーションに改良を加えていくことで、Wicketの豊富な機能を紹介していきます。

今回のアプリケーションを使ってみると、気になる点が大きく2点あります。1つは、現在の数十件だけを表示して、もっと古い発言を表示できないことです。ページング処理によってもっと古い発言も表示できるようにしましょう。

もう1つは、お気に入りリンクやリプライリンクをクリックする度にいちいちページ全体が再描画されてしまいます。ここをAjaxによって改善しましょう。

次回はまず、Wicketのコンポーネント作成機能の1つ「パネル」を使って、ページング用リンクを作りたいと思います。

おすすめ記事

記事・ニュース一覧