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

第7回自作コンポーネントで再利用可能オブジェクトを作る

前回まででタイムラインの表示が完成しました。なんとかTwitterらしいものが表示できましたが、最新20件しか表示できないという欠点がありました。ここを改善するために、過去20件を表示できるようにするのが、今回の目的です。

しかしその目的の前に、前回作ったプログラムのもうひとつの欠点「アイコン画像の表示が遅い」という問題を解決しておきましょう。

タグの属性を操作する

前回のプログラムで画像表示が遅いのは、画像表示のためにImageコンポーネントを使用したことが原因です。Imageコンポーネントは、プログラム的に動的に作成する画像を表示するために用意されたもので、Twitterアイコン画像のような固定画像表示には向いていません。理由は2点あります。

  • Imageを使うとプログラムが動いてしまう。画像への直接のリンクであれば、レスポンスはWicketのプログラムを必要としません。
  • Imageは画像のURLを動的に生成する。Imageは同じ画像であってもWebResourceごとに個別のURLを生成します。ブラウザは同じ画像のために複数のアクセスを必要とします。

前回のプログラムで表示したページのソースを見てみると、同じユーザのステータスであっても、<img>タグのsrc属性が毎回異なっていることが確認できます。それぞれがプログラムで作成したWebResourceへのリンクです。ブラウザはすべてのアイコン画像に対して個別のリクエストを投げるため、ブラウザにとっても、サーバにとっても負荷が高くなります。

ブラウザが効率良く動作するように変更するならば、<img>タグのsrc属性に、Twitterアイコン画像へのURLを直接埋め込むのが良いでしょう。そのために使うのが、ビヘイビアです。

ビヘイビアはコンポーネントのプラグイン

Wicketのコンポーネントの大きな目的は、HTMLタグを変更・生成することです。ComponentクラスにあるonComponentTag()メソッドがタグ生成の役割を担っており、オーバーライドすればタグ生成方法を細かく制御することができます。

しかし属性変更のようなちょっとしたタグ変更のためにいちいちコンポーネントのサブクラスを作っていたのでは、さすがにサブクラスが増えすぎてしまいます。また、同じような変更をするだけなのに、クラスが異なるためにいちいち別のサブクラスを作らなくてはいけません。

そのために、Wicketにはコンポーネントのタグ生成に介入するためのプラグイン的機構があります。それが「ビヘイビア」です。

ビヘイビアをコンポーネントに追加することで、コンポーネントのタグ生成処理に介入することができます。コンポーネントは「ビヘイビアが登録された直後」⁠タグ生成処理実行直後」⁠タグ出力前」⁠タグ出力後」といったタイミングで、ビヘイビアに処理を委譲します。多くのコンポーネントに共通の操作をビヘイビアとしてまとめておくことで、コンポーネント操作をオブジェクトとして再利用可能になります。

しかも、ビヘイビアはコンポーネントに対していくつでも登録することができます。継承ではひとつの機能しか追加することができませんが、ビヘイビアにすることで、複数個の機能をまとめてコンポーネントに追加できるのです。

SimpleAttributeModifierビヘイビアで属性を変更する

ビヘイビアはコンポーネントへの操作をオブジェクト化して再利用可能としたものです。Wicketはビヘイビアとして、多くのクラスをあらかじめ提供しています。その中に「タグ属性を変更するビヘイビア」であるSimpleAttributeModifierがあります。

サンプルプログラムのjp.gihyo.wicket.page.paging.PagingTimelineを見てください。このプログラムは、前回までに作ったMyTimelineクラスを改変したものです。プログラムはほぼ同じですが、ところどころ変更しています。このクラスに、次のような箇所があります。

リスト1 SimpleAttributeModifierでsrc属性を変更する例
//Imageコンポーネントではなく、<img>タグのsrc属性を変更する形式に変更
WebMarkupContainer userImage = new WebMarkupContainer("imageLink");
userImage.add(new SimpleAttributeModifier("src", status.getUser().getProfileImageURL().toString()));

いままでImageコンポーネントを使っていたところが、WebMarkupContainerクラスに変わっています。WebMarkupContainerクラスは、タグをWicketコンポーネントとして扱えるようにするだけのコンポーネントで、それ自体には機能はありません。HTMLファイルに記述されているタグがそのまま出力されます。ここでWebMarkupContainerを使うことで、<img>タグにビヘイビアを追加できるようにしているのです。

SimpleAttributeModifierのコンストラクタに属性名と文字列を渡すことで、タグの属性を変更することができます。属性がHTMLに存在しない場合は属性が追加されます。属性名としてsrcを、新しい属性値としてtwitter4jを使って取り出したProfileImageURLを文字列化したものを渡しています。これにより、タグのsrc属性を変更することができます。

サンプルプログラムを起動し、http://localhost:8080/wicket-sample/pagingにアクセスすると、前よりも画像が速く表示できることが体感できます。HTMLソースを見れば、<img>タグのsrc属性値がTwitterが使っている画像URLになっていることが確認できます。

もっとフレキシブルな属性変更を行う

多くの場合はSimpleAttributeModifierで対応可能ですが、もっと動的に、状況により設定される属性値がかわるようなケースに対応するには、モデルを介して属性値を指定したいところです。

そのためには、SimpleAttributeModifierクラスのスーパークラスであるAttributeModifierクラスを使うとよいでしょう。属性値としてモデルを使用することができます。

また、SimpleAttributeModifierやAttributeModifierは属性値を常に置き換えますが、置き換えるのではなく追加したい場合もあります。その場合には、AttributeModifierクラスを使用してください。

タイムラインをページ指定可能にする

TwitterのAPIには、もともとページ番号を指定してタイムラインを取得するための命令が備わっています。twitter4jもその機能に対応しているので、タイムラインのページングを行うための機能はもとから備わっているわけです。あとは、Wicketのページオブジェクトに表示したいページ番号を指定できればいいのです。

Wicketのページオブジェクトに情報を渡すためには、ページのコンストラクタ引数を利用出来ることは既に説明しました。しかしながら、今回はコンストラクタ引数による情報引き渡しは行いません。コンストラクタ引数による情報引き渡しは、あくまでも「一時的なページ」のために使うべきものです。コンストラクタ引数を使うと、Wicketはページオブジェクトにアクセスするために新しいURLを自動的に割り当てます。このURLは一時的なもので、セッションが切れれば使えなくなります。ページに情報を渡すときには、遷移先ページがセッション中だけ使えればよいのか、URLであとからでもアクセス可能なのかを常に意識すべきです。

今回は、表示したページをあとからでもアクセス可能にします。そのためには、URLでページを指定することになります。セッションが切れても常にページに情報を引き渡すには、URLを利用するのが一番簡単です。WicketでURLに情報を付与したり、付与された情報を引き出すためのクラスが、PageParametersクラスです。

PageParametersを使って情報を受け取る

Pageクラスには、引数としてPageParametersオブジェクトだけを受け取るものがあらかじめ用意されています。WicketはURLにリクエストパラメータが埋め込まれていると、それらをPageParametersオブジェクトに変換し、PageParametersを引数に受け取るコンストラクタを呼び出します。

PageParametersでは、リクエストパラメータとして渡されるキーと値をJavaのMapのような処理で扱うことができます。キーを指定してgetString()メソッドを呼び出すことで、パラメータ値を取得することができます。また、getAsInteger()やgetAsDoube()のように数値として取得するためのメソッドや、getAsTime()やgetAsDuration()のように、Wicket独自の時間型オブジェクトとして取り出すメソッド、getAsEnum()のようにenum型に変換するメソッドが用意されているため、文字列をオブジェクト化する手間を若干ながら省くこともできます。

サンプルプログラムでは、getAsInteger()メソッドを使ってURLからページ番号を取り出しています。ページパラメータにページ番号が指定されていないケースもありえますので、デフォルト値として1を指定することで、指定がなければ1ページ目を表示するように配慮しています。

リスト2 パラメータ値を取得する
this.currentPageNumber = parameters.getAsInteger("page", 1);

タイムライン取得時にページを指定する

twitter4jにはあらかじめページを指定するためのAPIが備わっています。実はこのAPIは、前回までのサンプルでも使用していました。Twitterタイムラインを取得する処理は次のようなものでした。

リスト3 MyTimelineクラスでのタイムライン取得処理
return twitter.getFriendsTimeline(new Paging(1, ITEMS_PER_PAGE));

getFriendTimeline()メソッドに渡しているPagingオブジェクトが、取得するページを指定するためのオブジェクトです。Pagingコンストラクタの第1引数がページ番号です。いままでは固定値として1を指定していたために、常に1ページ目が表示されていたのです。第2引数は1ページに表示するステータス件数です。ITEMS_PER_PAGE定数は20と定義していますので、1ページ20件として1ページ目を取得する、という処理になります。

さきほどリクエストパラメータからページ番号を取得してcurrentPageNumberフィールドに格納しましたので、次のように変更するだけで、パラメータで指定したページが表示されます。

リスト4 ページ番号を指定してタイムラインを取得する
return twitter.getFriendsTimeline(new Paging(currentPageNumber, ITEMS_PER_PAGE));

これで、表示ページ番号を指定できるページとなりました。

ページング用リンクを作る

URLでページを指定できるようになりましたので、あとは前ページと次ページに移動するためのリンクがあればいいでしょう。

前ページと次ページ表示用リンクは組み合わせて使うものです。1つ1つをページに追加するのではなく、⁠前後リンクコンポーネント」としてまとめて扱えた方が便利ですし、再利用性も高そうです。今回は、⁠page」というリクエストパラメータを扱えるページであればどのクラスであろうと使える、再利用可能な前後リンクを作ります。

パネルは条件付きのページ

Wicketにはパネルという再利用性を高めるためのコンポーネントがあります。パネルはページと同じように、自分専用のHTMLファイルを持つことができます。実際のところ、パネルではHTMLのどの部分を使うかを指定する必要がある点と、ページに追加することによってだけ表示できる2点を除けば、ページと同じように使うことができます。

すべてのパネルはPanelクラスのサブクラスです。Panelを継承するだけで、そのクラスはWicketによってパネルとして扱われます。

パネルには対となるHTMLファイルが必要です。HTMLファイルの扱いはページの場合と同じで、クラスパス上の同じ位置に、拡張子を除いて同じ名前のHTMLファイルがあれば、それがパネル用のHTMLファイルとなります。

そのほか、プロパティファイルの扱いについてもページと同じように扱うことが可能です。

サンプルプログラムのjp.gihyo.wicket.component.PagingLinkクラスを見てください。このクラスはPanelを継承しているため、パネルとして動作します。対となるHTMLファイルとプロパティファイルもあります。3つのファイルが組み合わさって、1つのパネルを構成しています。

パネルのためにHTMLを切り取る

パネルは、イメージとしては、HTMLページの一部分だけを切り取って別コンポーネントとしたものです。既にHTMLがあって、その一部分だけをコンポーネントとして使いたい、というケースです。パネルにはこのようなケースに対応できるように作られています。

パネルは対となるHTMLファイルの中身全体を使用するわけではありません。パネルが使うのは、HTMLファイル内で<wicket:panel>と</wicket:panel>で囲まれた範囲だけです。それ以外の場所は、単に無視され、最初からなかったかのように扱われます。

もし大きなHTMLファイルの一部分だけを抜き出してパネル化したいのであれば、HTMLファイルをまるごとコピーして、使いたい部分を<wicket:panel>で囲むだけです。

サンプルプログラムのPagingLink.htmlでは、2つの<a>タグだけが<wicket:panel>で囲まれています。前リンクと次リンクです。それ以外の場所は<body>や<html>タグを含めて、すべて無視されます。

パネルにコンポーネントを追加する

パネルは使用するHTML部分が全体ではなく一部に限定される点を除けばページと同じようなものですので、作り方も変わりません。PageLink.htmlをページだと思えばよいのです。

リスト5 PagingLink.htmlのパネル化部分
<a href="#" wicket:id="prevLink"><span wicket:id="prevLabel">前へ</span></a> <a href="#" wicket:id="nextLink"><span wicket:id="nextLabel">後へ</span></a>

prevLinkとnextLinkという2つの<a>タグと、リンク文字列を埋め込むための<span>タグを組み合わせたパネルです。

対となるPagingLink.javaでは、この2つのリンクを組み立てます。

まず、リンクで遷移する先となるページクラスを、コンストラクタ引数として受け取ります。また、現在のページ番号が分からないと「前のページ番号」「次のページ番号」が分からないので、これもコンストラクタで受け取ります。ただし、ページ番号についてはモデルとして受け取るようにします。

リスト6 PagingLinkコンストラクタ
public PagingLink(String id, Class<? extends Page> targetPageClass, IModel<Integer> pageNumberModel) {
  super(id);

  if(targetPageClass == null) throw new IllegalArgumentException("targetPageClass is missing.");
  if(pageNumberModel == null) throw new IllegalArgumentException("pageNumberModel is missing.");
  this.targetPageClass = targetPageClass;
  this.pageNumberModel = pageNumberModel;
  construct();
}

ページ番号をモデルとして受け取るのは、⁠現在のページ」という情報は可変だからです。現在のページは次々と変わります。もし数値として受け取ってしまうと、最初に受け取ったページ番号で固定されてしまいます。Wicketでは、値そのものを直接受け取るのではなく、モデルとして値を受け取ることで、変化する値をオブジェクト内に保持することができます。必要なときにモデルのgetObject()を呼び出せば、その時の最新値が取得できるのです。

パネルにコンポーネントを追加する

コンストラクタで必要な情報を受けとれば、あとはパネルを組み立てるだけです。2つのリンクを追加する必要があります。両者のプログラムはほとんど同じですので、コード例としては前への移動リンクのみを載せます。両方を見たい場合はサンプルプログラムを参照してください。

リスト7 前への移動リンクの組み立て
Label prevLabel = new Label("prevLabel", new ResourceModel("prev"));

Link<Void> prevLink = new Link<Void>("prevLink") {
  @Override
  public void onClick() {
    setResponsePage(targetPageClass, new PageParameters("page=" + (pageNumberModel.getObject() + 1)));
  }
  
  @Override
  public boolean isEnabled() {
    Integer number = pageNumberModel.getObject();
    return number != null && number > 0;
  }
};
add(prevLink);
prevLink.add(prevLabel);

まずprevLabelというLabelコンポーネントは、リンクとして表示される文字列を作ります。ResourceModelを使えば、ページやパネルと対になっているプロパティファイルから、指定キーを使って文字列を取得することができます。この例では「prev」というキーを使ってPagingLink.properties内を検索します。

prevLinkはLinkコンポーネントの匿名サブクラスです。Linkコンポーネントは前回までに既に使用しています。onClick()メソッドをオーバーライドして、クリックされたら行う動作をプログラムしています。ここでは、コンストラクタで受け取っているページへと遷移するためにsetResponsePage()を呼び出しています。setResponsePage()はMyTimelineページを再表示する際にも使用しました。今回は遷移先クラスだけではなく、PageParametersも一緒に引数として渡しています。

PageParametersはキーと値の組み合わせですので、同じくキーと値の組み合わせであるMapを引数として組み立てることができます。しかし、1つのパラメータのためにいちいちMapを作るのも面倒ですので、もう1つの作り方である、⁠key=value」といった文字列から作成する方法を利用しました。

リスト8 文字列によるPageParameters組み立て
new PageParameters("key1=value1&key2=value2");

上記のように「key=value」の組み合わせを&記号でつなげることで、複数のパラメータにも使うことができます[1]⁠。

PageParameterの値として、コンストラクタで受け取った、現在ページを取得するモデルを使用しています。モデルのgetObject()で取得したページ番号にプラス1することで、次のページへのパラメータとしているのです。

リンクを無効にする

onClick()だけをプログラムすれば実際に遷移することができますが、もう少し配慮したいところです。1ページ目で、さらに若いページ番号への移動リンクをクリックできるのはおかしいでしょう。そのため、コンポーネントが有効かどうかをチェックするためのメソッドisEnable()メソッドをオーバーライドします。

Wicketはプッシュではなくプルによって動くことを思い出してください。コンポーネントが有効かどうかはコンポーネントの属性の一部であり、コンポーネント自身が状況を判断して決定すべきものです。

今回のサンプルプログラムでは、前ページへの移動リンクでは「ページ番号が指定されているかどうか⁠⁠、次ページへの移動リンクでは「ページが1よりも大きいかどうか」で有効無効を判断しています。Twitterではページ番号が若いほど新しいページですので、次ページへの移動リンクは「現在のページ-1」ページへと移動します。数値が1よりも大きくないと、1引くと最初のページである1ページよりも小さい値になってしまうので、無効にしているのです。

前ページの方は、再利用性を考慮しています。今回のプログラムではpageNumberModel.getObject()が1よりも小さい値を返す可能性はありません。しかし外部から与えられるモデルがどのような値を返すかは、コンポーネントからは判断できません。そのためにpageNumberModel.getObject()がnullを返したり1未満の値を返したときには、前ページリンク自体を無効にしました。

パネルをページに追加する

2つのリンクを組み合わせたパネルが完成しました。一度完成してしまえば、パネルは1つのコンポーネントとして使うことができます。パネルをページで表示するには、いままでLinkやLabelをページに追加したように、PagingLink「コンポーネント」というものがあるかのように扱えばよいだけです。

今回のパネルを適用する先のHTMLは、次のような単純な<div>要素です。

リスト9 パネルを適用する先のHTML
<div class="paging" wicket:id="paging">前へ 後へ</div>

タグにパネルを適用すると、そのタグの内側に、パネル用HTMLの内容が転記されます。上記のHTMLでは「前へ 後へ」と書かれた部分に、Paging.html内のパネル部分が転記されるのです。つまり、<div>の内側に<a>タグが2つ転記されます。

パネルをページに適用するプログラムは次のものです。

リスト10 PagingLinkをページに追加する
add(new PagingLink("paging", PagingTimeline.class, new AbstractReadOnlyModel<Integer>() {
  @Override
  public Integer getObject() {
      return getCurrentPage();
  }
}));

上記のとおり、PagingLinkというコンポーネントがあるかのように、ただadd()メソッドに渡しているだけです。第2引数のモデルは現在ページを返すよう、AbstractReadOnlyModelを使ってgetObject()が呼ばれるたびに現在ページを取得するようにプログラムしています。

パネルはコンポーネント作成のための機構

PagingLinkを見てわかるように、使う側から見ると、パネルはコンポーネントのように見えます。実際、Wicketにあらかじめ含まれているコンポーネントのうちのいくつかは、実際はパネルです。

パネルはページとほぼ同じ機能をもつため、さまざまなことを実現できます。パネルに別のパネルを追加することすらもできます。ページで使うことのできるあらゆることは、パネルにも適用することができます。パネルはそれ単独で動く部品なのです。パネルを上手に取り入れることで、ページのプログラムは複数の部品に分解されて、シンプルになっていきます。

リンクをAjaxで処理する

今回でTwitterタイムラインはページング可能となりました。しかしまだ、お気に入りリンクやリプライリンクをクリックする度にいちいちページ全体が再描画されてしまう問題が残っています。例えばfavリンクをクリックした場合は、クリックしたステータス行だけが更新されるようにしたいところです。

次回は、Wicketの組み込みAjax機能を使って、タイムライン上のリンクをAjax化します。

おすすめ記事

記事・ニュース一覧