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

第8回組み込みAjax機能で動的に変化するページを実現する

今回のテーマは、Wicketの組み込みAjax機能を使ってページ全体のリロードを無くすことです。最近のアプリケーションでは、JavaScriptおよびAjaxを使ってページを動的に作り替えることで、ページ全体のリロードを減らす方向にあります。Ajaxなしにアプリケーションを作れない時代になったと言えるでしょう。組み込みAjax機能はそのような動的なページ部分更新をWicket上で実現するためのものです。

組み込みAjax機能の位置づけ

あらかじめ注意しておくと、WicketはJavaScriptフレームワークでもありませんし、Ajax開発のためのフレームワークではありません。Ajaxだけに注目すると、組み込みAjaxの機能は不十分に見えることでしょう。実際、筆者はそういう指摘を受けたこともあります。JavaScript開発のための機能をWicketに求めるのは間違いです。JavaScriptの開発は、あなたのお気に入りのJavaScript用フレームワークを使って行うほうがずっと快適なはずです。WicketはJavaを使ってアプリケーションを構築するフレームワークです。今回紹介する組み込みAjax機能も、あくまでJavaからプログラムできる機能として提供されています。

WicketはJavaScript言語を使った開発のサポートは行いませんが、一方で、開発したJavaScriptソースファイルをコンポーネント内に閉じ込めるための機能は提供します。この機能を使って、コンポーネント使用者がJavaScriptの詳細を知ることなく使える「Wicketコンポーネント」を作ることができます。Wicketが提供するのは、コンポーネントの機能を実装するためにJavaScriptプログラムを利用するための「仕組み」なのです。

Wicketではページを部分的に更新するのは、特別なJavaScript操作を必要とする「つけたし」ではなく、フレームワーク自体に組み込まれた機能の1つです。その機能を実現するために裏ではAjaxを使っている、と考えると、組み込みAjax機能の位置づけが分かりやすいでしょう。今回の記事を読む際にも、JavaScriptのことは一旦忘れ、新しく紹介するWicketの機能を知るつもりで読むのが良いでしょう。

ページの部分更新を行う

Wicketでページの部分更新を行うには、AjaxRequestTargetオブジェクトが必要となります。AjaxRequestTargetオブジェクトはプログラマが自分で用意するものではなく、Wicketにより提供されるオブジェクトです。Wicketの提供するコンポーネントのうち、クラス名の先頭がAjaxで始まるコンポーネントがAjaxRequestTargetに対応しています。

例えば、ボタンをクリックした結果としてページの一部分だけを更新したい場合には、Linkコンポーネントの代わりにAjaxLinkコンポーネントを使います。LinkとAjaxLinkの使い方はほぼ同じで、どちらもユーザのクリックを検知すると自身のonClick()メソッドを呼び出します。ただし、onClick()にAjaxRequestTargetオブジェクトが渡されるかどうかだけが異なります。

リスト1 AjaxLinkのonClick()メソッド
new AjaxLink("link") {
  @Override
  public void onClick(AjaxRequestTarget target) {
    //ここにクリック時の処理を書く
  }
}

上記プログラムのとおり、変わったところはAjaxRequestTargetを引数として受け取っていることだけです。WicketがAjaxRequestTargetオブジェクトを作成して、onClick()に渡してくれます。

Linkコンポーネントでは、onClick()内部でモデルを適切に変更しさえすれば、ページ全体が再描画されることで、最新の状態が表示に反映されました。しかし、AjaxLinkでは何もしなければページは一切変化しません。変化させたい場所をWicketに伝えなくてはならないのです。

Wicketに変更箇所を指定する方法は、AjaxRequestTargetオブジェクトに再描画したいコンポーネントをaddComponent()メソッドでセットするだけです。

リスト2 リンクがクリックされたらlabelだけを更新する
final Label label = new Label("myLabel");
add(label);
(中略)
new AjaxLink("link") {
  @Override
  public void onClick(AjaxRequestTarget target) {
    target.addComponent(label);
  }
}

このプログラムでは、AjaxLinkをクリックするとaddComponent()メソッドでlabelコンポーネントをAjaxRequestTargetに登録しています。これだけで、Wicketが裏側でAjaxを利用して、labelコンポーネントが適用されているタグ部分だけを更新します。addComponent()は何度でも呼ぶことができます。Wicketコンポーネントを適用しているタグであれば、どこでも同じ方法で部分更新することができます。

ページの部分更新はWicketの機能の1つであり、そのために裏側でAjaxを使っている、という意味が伝わるでしょうか。Wicketでは、部分更新したければそのコンポーネントをAjaxRequestTargetに追加するだけでよいのです。あとは、Wicketが裏側で処理をしてくれます。極端に言えば、裏でAjaxが使われていることを知らないとしても、部分更新を行うことができます。

これがWicketの組み込みAjax機能です。

リプライリンクをAjax化する

ページを部分更新する方法が分かりました。早速、TwitterアプリケーションをAjax化してみましょう。まずは、リプライリンクをクリックするとページ全体が再描画されてしまう点を改善しましょう。

サンプルプログラム内にある、jp.gihyo.wicket.page.ajax.AjaxTimelineクラスを見てください。前回作成したPagingTimelineクラスを改良したものです。これから説明するプログラムは、すべてこのクラスに記述されています。

サンプルプログラムでは、replyLinkコンポーネントの次のようにAjaxLinkとして定義し直しました。

リスト3 replyLinkをAjaxLink化した例
item.add(new AjaxLink<Void>("replyLink") {
  @Override
  public void onClick(AjaxRequestTarget target) {
    String targetScreenName = status.getUser().getScreenName();
    form.insertText(target, "@" + targetScreenName + " ");
  }
});

PagingTimelineクラスまでの例では、formのinsertText()メソッドの引数は1つだけでした。今回は引数は2つになり、第1引数としてAjaxRequestTargetを渡しています。実際のフォーム更新処理はinsertText()メソッド内で行われています。

リスト4 入力テキスト欄だけを更新する例
public void insertText(String text) {
  updateText.setModelObject(text);
}

public void insertText(AjaxRequestTarget target, String text) {
  insertText(text);
  target.addComponent(updateText);
  target.focusComponent(updateText);
}

引数が2つの新しいinsertText()メソッドは、まず従来の引数が1つのinsertText()メソッドを呼び出すことで、モデルを更新します。しかし今回は、単にモデルを更新してもページは再描画されません。そこで、テキスト入力欄を表すコンポーネントupdateTextをAjaxRequestTargetに追加することで、入力欄だけを再描画するように指定しました。

さらに、リプライリンクをクリックする場合、ユーザはすぐに入力を行うでしょうから、AjaxRequestTargetクラスのfocusComponent()メソッドを使って、入力フォーカスをupdateTextコンポーネントに移動しています。Wicketではフォーカス制御もこのように簡単に、コンポーネントで指定して行うことができるのです。

リプライリンクをAjax化して入力欄だけを部分更新する手順は、これだけです。Wicketの部分更新機能を使うことで、JavaScriptを意識することなく、ただ「このコンポーネントを再描画してください」と指令することで部分更新が行われるのです。ページの部分更新は、Wicketの標準機能の1つなのです。

お気に入りリンクのAjax化

お気に入りリンクは、クリックしたステータスをお気に入り登録すると同時に、ステータス上の「fav」リンクを「unfav」リンクに更新します。たった数文字が変化するだけなのですが、PagingTimelineまでの例では、常にページ全体が更新されていました。部分更新機能を利用して改善しましょう。

リスト5 お気に入りリンクのAjaxLink化
AjaxLink<Void> favLink = new AjaxLink<Void>("favLink") {
  @Override
  public void onClick(AjaxRequestTarget target) {
    try {
      Status currentStatus = getCurrentStatus(status.getId());
      Twitter twitterSession = AppSession.get().getTwitterSession();
      if(currentStatus.isFavorited()) {
        twitterSession.destroyFavorite(currentStatus.getId());
        info(getString("favorateRemoved"));
      } else {
        twitterSession.createFavorite(currentStatus.getId());
        info(getString("favorateRegistered"));
      }
      favName.setNeedRefresh(true);
      target.addComponent(feedback); //登録メッセージ表示のため、フィードバックパネルも更新する。
      target.addComponent(favName);
    } catch (TwitterException ex) {
      String message = getString("catNotCreateFavorite") + ": " + ex.getStatusCode();
      error(message);
      target.addComponent(feedback);
      LOGGER.error(message, ex);
    }
  }
};

少し長いですが、前回までのサンプルプログラムと見比べてみれば、大きく変化していないことが分かります。処理の最後で部分更新したいfavNameラベル[1]をAjaxRequestTargetに登録している点と、同時にFeedbackPanelコンポーネントも登録している点が一番大きな変化です。AjaxRequestTargetを使ったページ更新では、addComponent()メソッドで登録したコンポーネント以外は一切更新されませんので、メッセージを表示するためにはFeedbackPanelもきちんと更新対象として登録する必要があります。

リスト上のitem要素を再利用する際の注意点

ところで、以前にListViewの使い方を説明したときに、setReuseItems()というメソッドについて説明をしました。このプログラムではsetReuseItems(true)とすることで、同じページオブジェクトである限りはListView内のItem要素(各行を表すオブジェクト)を再利用するように指定しています。この指定は、今回のように行の部分更新するためには重要です。

リンクをクリックすると、Wicketは直前に表示していたページ・オブジェクトをメモリ上に自動的に復元し、クリックされたコンポーネントを探し、onClick()を呼び出します。もしsetReuseItems(true)が呼ばれていなければ、Wicketはページを復元する段階で、ListViewの中身を再構築します。結果、クリックしたはずの行が正しく復元されるかどうかは確実ではなくなります。新しいステータス群で置き換わっているかもしれません。さきほどまで1行目に表示されていたステータスは、再構築時には5行目になっているかもしれません。

ListView内の要素への入力や操作を伴なうことを行う場合には、setReuseItems(true)を呼び出してください。

ローカルクラスで新しいクラスを定義する

サンプルプログラムを見ると、もう一箇所大きく変わっている場所があります。お気に入りリンクの文字を表示するためのラベルです。PagingTimelineまでのラベルは、各行のステータスがお気に入り登録されているかどうかに応じて「fav」「unfav」を表示するだけの、シンプルなプログラムでした。

AjaxTimelineクラス内では、より複雑なものとなっています。

リスト6 お気に入りリンク文字列を表示するためのローカルクラス
class FavoriteLabel extends Label {
  private static final long serialVersionUID = -2194580825236126312L;
  private Status targetStatus;
  private boolean needRefresh;
  
  public FavoriteLabel(String id) {
    super(id);
    this.targetStatus = status;
  
    setDefaultModel(new AbstractReadOnlyModel() {
      @Override
      public String getObject() {
        try {
          if(needRefresh) {
              targetStatus = getCurrentStatus(status.getId());
              needRefresh = false;
          }
          return targetStatus == null ? "" : targetStatus.isFavorited() ? "unfav" : "fav";
        } catch (TwitterException ex) {
          LOGGER.error("Can not fetch current status for status id = " + status.getId(), ex);
          return "error";
        }
      }
    });
  }
  
  public void setNeedRefresh(boolean needRefresh) {
      this.needRefresh = needRefresh;
  }
}

新しいラベルは、ただのLabelとしてではなく、FavoriteLabelという名前の新しいクラスとして定義しています。このクラス定義がコンストラクタ内に書かれている点に注目してください。この記法は「ローカルクラス」と呼ばれる、あるメソッド内でのみ有効なクラスを定義するための技法です。古くからJavaに存在する手法ですが、あまり多用されることはありません。

ローカルクラスは、概念的には今までも何度か使用した「匿名サブクラス」と同じようなものです。匿名サブクラスではクラス定義をすることなく即座にサブクラスを作ることができました。これは「名前のついていないローカルクラス」を作っているのと同じなのです。

ローカルクラスは匿名クラスと同じ特性を持っており、メソッド内のfinalのつけられたローカル変数にアクセスできますし、外部オブジェクトのフィールドにもアクセスすることができます。匿名クラスと異なるのは、完全なクラス定義をもつため、新しいpublicメソッドを定義できることです。

今回はローカルクラスの特性を生かし、ListView各行のステータスを表すstatusオブジェクトにアクセスしています。statusオブジェクトはFavoriteLabelクラスの外側にある点に注目してください。同時に、setNeedRefresh()というメソッドを新しく定義している点にも注目してください。このpublicメソッドの追加こそが、ここでローカルクラスを使っている理由です。

最新状態にあわせてラベル表示を切り替える

今回新しいラベルクラスを定義したのは、ページの部分更新を行う場合には、ページ全体が一斉に更新される場合とは異なる配慮が必要だからです。

ページ全体が更新される場合は、各行のstatusオブジェクトの状態を見れば、その行がお気に入り登録済みか否かを簡単に確認できました。ラベルとリンクは常に同じタイミングで更新されたからです。

部分更新ではすこし状況が異なります。ユーザがクリックすると、まずAjaxLink内でTwitterにリクエストが投げられ、ステータスがお気に入りに登録もしくは解除されます。これはTwitterサイト側で行われます。そして次に、ラベルだけが更新対象に加えられます。ラベルは再描画指令に答えるべく、自身の状態を確認します。ここでstatusオブジェクトの状態を確認することに意味はありません。先程リンクをクリックしたことで、Twitter側ではステータスの状態が変化しているのです。もはやstatusは古い情報を持っているにすぎません。ラベルが正しく表示されるには、ステータスの今の状態を再取得するしかありません[2]⁠。

かといって、表示のたびに1つ1つのラベルが最新状態を再取得する必要があるわけでもありません。ラベルを表示する度にTwitterにアクセスするようプログラムしてしまうと、ページ表示時にListViewの行数分のアクセスが発生してしまいます。再取得が必要なラベルは、お気に入りリンクがクリックされたステータス上にあるラベルのみです。

「ステータスの再取得が必要かどうか」を判断するための基準として、ローカルクラスFavoriteLabelはneedRefreshフィールドを持っています。FavoriteLabelはneedRefreshフィールドがtrueのときだけ、Twitterから該当ステータスを再取得します。trueにならない限りは、保持しているステータス情報をもとに表示を行うのみです。

改めてお気に入りリンク部分のプログラムを見直してみると、ラベルにステータス再取得を促している行があることに気がつくでしょう。

リスト7 FavoriteLabelのneedRefreshフラグをtrueにする
favName.setNeedRefresh(true);

お気に入りリンクは、クリックタイミングでFavoriteLabelオブジェクトの状態を変化させ、その後に部分更新を促しているのです。更新要求を受けたFavoriteLabelは、自分の状態をもとに新しい表示文字列を確定します。リンクとラベルが連携しつつも、行っていることは「ステータスの再取得が必要」というフラグを立てているだけです。お気に入りリンクがラベルに新しいステータスを「プッシュ」するのではなく、⁠情報の更新が必要ですよ」と伝えているだけです。あとはFavoriteLabelが自主的に情報を取得して描画を行います。

部分更新とオブジェクト指向

ページの部分更新を採用すると、いままでは「ページ」という大きな単位で考えればよかったものが、もう少し狭い「コンポーネント」の単位で考える必要が出てきます。ページはもはや、ページ全体として一括更新されるものではなく、部分部分で別々に更新されるものとなりました。

ページ一括更新の世界では、すべての状態をページに集中して、ページ全体の更新タイミングで状態を確認するだけで問題ありませんでした。部分更新を採用すると、コンポーネントの単位で状態を持つべきケースが出てきます。FavoriteLabelがその例です。コンポーネント単体で更新される場面が出てきます。そのコンポーネントがどう表示されるべきかは、コンポーネントが単独で判断しなければならないのです。

部分更新の世界では、ページは全体で1つではなく、オブジェクトの塊となります。それぞれが個別に状態をもち、個別に描画できるコンポーネント同士が、互いに状態を変化させることでページが構成される世界です。

サーブレットでのプログラミングでは想像のできなかった、オブジェクト指向プログラミングの世界です。Wicketでは、Webアプリケーションを「処理の振り分け」ではなく、⁠それぞれが独自の機能をもったオブジェクトの組み合わせ」として作ることができるのです。部分更新機能では、その特徴がよりはっきりと現れます。

Javaのすべての機能をつかってプログラミングする

コンポーネントとモデル、ビヘイビアといったWicketの機能により、アプリケーション機能はさまざまな単位のオブジェクトに分割できます。この連載では、Wicketが備えるオブジェクト指向開発をサポートする機能を、おおまかではありますが紹介してきました。

それに加え、Wicketでは匿名サブクラスやローカルクラスなど、Javaに本来備わっていつつもあまり使われない機能をも有効に使える場面が多くあります。Javaらしいオブジェクト指向の世界を作り上げているため、Javaのオブジェクト指向言語としての機能をフルに発揮できるのです。いままでWebアプリケーションではあまり使われなかったオブジェクト生成手法も、Wicketでは便利に使えます。

もちろん制限はゼロではありませんが、サーブレットに比べると遥かにJavaらしいオブジェクト指向をベースにプログラムを作ることができるのが、Wicketでプログラミングする魅力です。

次回:OAuth対応

次回は本連載の最後として、いままでに作成してきたアプリケーションのログイン部分を、OAuthを利用したものに変更します。OAuthを使った認証を行うためにはWicketアプリケーション外部との情報のやりとりも必要となります。いままではほぼすべて、アプリケーション内部での情報のやりとりでした。外部サイトとのやりとりを行うためには、オブジェクトによるやりとりではなく、URLによるやりとりが必要になります。OAuth化を行うことで、Wicketでの外部サイトとの連携方法を見ていきたいと思います。

おすすめ記事

記事・ニュース一覧