ページナビゲーション

本質的には、Tapestryアプリケーションは複数の関連するページが連携して動作するというものです。 ある程度、各ページ自身がアプリケーションに似ています。 (To some degree, each page is like an application unto itself.)

どのリクエストも特定のページに向けられます。リクエストは次のふたつの形でやってきます:

  • あるページ内のあるコンポーネントへ向けられる コンポーネントイベント リクエスト。それは、そのコンポーネント内でイベントを発生します。
  • あるページへ向けられる レンダー リクエスト。そのページのHTMLマークアップがクライアントに返されます。

このコンポーネントイベントリクエストとレンダーリクエストのふたつに分けるという方式は Tapestry 5 で新たに取り入れられました。 これはいくつかの点でPortlet仕様の考えに基づいていて、リクエストをふたつの種類に区別することで ブラウザの戻るボタンや再読み込みボタンにまつわる伝統的なWebアプリケーションの問題の多くを軽減することができます。

コンポーネントイベントリクエスト

ハイパーリンク(ActionLink)のクリック またはフォーム(Form)のサブミットによってコンポーネントイベントリクエストは発生します。

どちらの場合においても、イベントハンドラメソッドから返される値によってブラウザへのレスポンスが制御されます。

コンポーネントイベントリクエストのURLから、ページ名とそのページ内のコンポーネントid、そのコンポーネント上で発生したイベントの名前(通常これは "action" です)を特定します。 さらに、コンポーネントイベントリクエストには追加のコンテキスト情報が含まれているかもしれません。コンテキスト情報はイベントハンドラメソッドに渡されます。

このURLからはアプリケーションの内部構造が少しだけ見えます。時間が経つにつれてアプリケーションは成長しメンテナンスされるので、コンポーネントのidは変更されるかもしれません。 これはコンポーネントイベントリクエストのURLをブックマークすべきではないということです。幸い、ユーザーにはそうする機会は滅多に無いでしょう(以下を見てください)。

Nullレスポンス

イベントハンドラメソッドが値を返さないかnullを返した場合、現在のページ(コンポーネントを含んでいるページ)がレスポンスをレンダリングします。

現在のページのレンダーリクエストURLを生成し、クライアントサイドリダイレクトとしてクライアントへ送信します。 クライアントのブラウザは自動的に新しいリクエストを出し、ページが表示されます。

ブラウザ内には新しく生成されたコンテンツが表示されているでしょう。加えて、ブラウザのアドレスバーにあるのはレンダーリクエストのURLです。 レンダーリクエストURLはより短く、URL中に含まれるアプリケーションの構造もより少なくなっています(例えば、コンポーネントidやイベントの種類は含んでいません)。 ユーザーはレンダーリクエストURLをブックマークすることができます。コンポーネントイベントリクエストURLは一時的なもので、アプリケーションの使用中しか意味を成しません。 コンポーネントイベントリクエストURLは後のセッションでは利用することができないということです。

Stringレスポンス

文字列が返された場合、それはページの論理名として扱われます(ページの完全限定クラス名ではありません)。 他の場所と同じように、ページ名は大文字小文字を区別しません。

繰り返しになりますが、レンダーリクエストURLが生成されクライアントリダイレクトとして送信されます。

Classレスポンス

Classを返すと、それはページのクラスとして扱われます。リファクタリングのためには、イベントハンドラからページクラスを返す方がページ名を返すよりも安全です。

他の型の戻り値と同様に、レンダーリクエストURLを生成してクライアントリダイレクトを送信します。

Pageレスポンス

ページ名やページクラスではなく、ページのインスタンスを返すこともできます。

ページのインスタンスはInjectPageアノテーションを使ってインジェクトすることができます。

ページのインスタンスを返す前に、そのページに何かしらの設定をすることもあるでしょう(例は後述します)。

ページ内のコンポーネントを返すこともできますが、実行時に警告が出ます。

Linkレスポンス

イベントハンドラメソッドはLinkインスタンスを直接返すこともできます。 そのLinkインスタンスはURLに変換され、クライアントリダイレクトとして送信されます。

ComponentResourcesオブジェクトにはアクションやページのLinkオブジェクトを生成するメソッドがあり、 ComponentResourcesオブジェクトをページ(やコンポーネント)にインジェクトしてそのメソッドを使用することができます。 (実際にはそのメソッドはComponentResourcesCommonで定義されています)

ストリームレスポンス

イベントハンドラメソッドはStreamResponseオブジェクトを返すこともできます。 StreamResponseオブジェクトはクライアントのブラウザに直接送信するストリームをカプセル化したものです。 これは、例えば画像やPDFを生成しそれをクライアントに提供するコンポーネントに役立ちます。

URLレスポンス

URLオブジェクトは外部URLへのクライアントリダイレクトとして処理されます。

Objectレスポンス

その他の種類のオブジェクトをイベントハンドラメソッドから返すとエラーとなります。

ページレンダーリクエスト

レンダーリクエストはその構造も振る舞いもコンポーネントイベントリクエストに比べて簡単です。 最も簡単なものではURLは単にページの論理名となります。

ページは アクティベーションコンテキスト を持っていることがあります。アクティベーションコンテキストはページの状態の永続化情報を表しています。 実際的な話としては、たいていアクティベーションコンテキストはデータベースに永続化されたオブジェクトのidです。

ページがアクティベーションコンテキストを持っている場合、その値はURLのパスに追加されます。

全てのページがアクティベーションコンテキストを持つわけではありません。

アクティベーションコンテキストはレンダーリクエストのリンクが生成されるときに明示的にセットされます(PageLinkコンポーネントはこのためにcontextパラメータを持っています)。 明示的なアクティベーションコンテキストが与えられなかった場合は、ページ自身にアクティベーションコンテキストの問い合わせを行います。

この問い合わせはイベントの発生によって行われます。そのイベント名は "passivate" です(すぐ後に出てきますが、これに対応する "activate" イベントもあります)。 このイベントのハンドラメソッドの戻り値がコンテキストとなります。例:

public class ProductDetail
{
  private Product product;
  
  . . .
  
  long onPassivate() { return product.getId(); }
}

アクティベーションコンテキストは複数の値から成ることもあります。その場合はメソッドの戻り値は配列かListです。

メモ: tapestry-hibernate を使っていて、そしてパッシベートしたいコンテキストがHibernateのエンティティである場合、 エンティティのidではなくエンティティ自体をそのまま使うことができます。 エンティティのidを取り出しURLに埋め込み、またそれを復元し "activate" イベントハンドラメソッドに渡す処理をTapestryが自動で行います。

ページアクティベーション

ページレンダーリクエストがやってくると、ページがレンダリングされる前にアクティベートされます。

アクティベーションは次のふたつの目的に役立ちます:

  • URL内にエンコードされたデータ(上で述べたアクティベーションコンテキスト)からページの内部状態を復元することを可能にします。
  • ページへのアクセスの正当性を検証する粗い方法を提供します。

後者の場合、検証はユーザーの身元とアクセス権に関係しているのが一般的です。 特定のユーザーしかアクセスできないページがある場合、そのアクセスを検証するためにページのアクティベートイベントハンドラを使うことができます。

ページのアクティベートイベントハンドラはパッシベードハンドラと逆のものです:

  . . .
  
  void onActivate(long productId)
  {
     product = productDAO.getById(productId);
  }
  
  . . .

関連事項: ページがレンダリングされるときには、たいてい多くのコンポーネントイベントリクエストURL(リンクやフォーム)が含まれているでしょう。 それらリンクやフォームのコンポーネントイベントリクエスト もまた ページをアクティベートするところから開始し、その他の処理はその後で実行されます。 これにより同じアクティベーションコンテキストを含むリクエストが途切れること無く繋がります。

これと同じことを、ページの状態を永続化することである程度はできます。 しかし、それにはセッションが有効である必要があり、ブックマーク可能ではありません。

アクティベートイベントハンドラも値を返すことができ、その値はコンポーネントイベントリクエストの場合と同じように扱われます。 これは典型的にはアクセスの正当性を検証する際に使われます。

ページナビゲーションパターン

アクションリンクとコンテキスト、ページコンテキストは様々な方法で組み合わせることができます。

商品カタログページの概念を用いて、典型的なマスター/詳細リレーションシップを取り上げてみます。 この例では、ProductListingページが商品リストでProductDetailsページが特定の商品の詳細を表示します。

コンポーネントイベントリクエスト / 永続化データ

このパターンでは、ProductListingページがアクションイベントを使用しProductDetailsページが永続化フィールドを使用します。

ProductListing.html:

  <t:loop source="products" value="product">
    <a t:type="actionlink" t:id="select" context="product.id">${product.name}</a>
  </t:loop>

ProductListing.java:

  @InjectPage
  private ProductDetails details;
  
  Object onActionFromSelect(long productId)
  {
    details.setProductId(productId);
    
    return details;
  }

ProductDetails.java:

  @Inject
  private ProductDAO dao;
  
  private Product product;
  
  @Persist
  private long productId;
  
  public void setProductId(long productId) { this.productId = productId; }
  
  void onActivate()
  {
    product = dao.getById(productId);
  }

これは最小限の方法で、おそらくプロトタイプとしては十分でしょう。

ユーザーがリンクをクリックしたとき、最初のコンポーネントイベントリクエストURLは "http://.../productlisting.select/99" のようになっていて、 その後のレンダーリクエストURLは "http://.../productdetails" のようになっているでしょう。製品id("99")はレンダーリクエストURLに現れないことに注目してください。

これにはいくつかの小さな欠陥があります:

  • セッションを必要とします(リクエストをまたいで_productIdフィールドを格納するために)。
  • 有効な製品idがセットされてからでないとProductDetailsページへのアクセスは失敗します。
  • URLは特定の製品を表していません; もしユーザーがこのURLをブックマークし後でまたやってくると、前項の状況(有効な製品idが無い)が起こります。

コンポーネントイベントリクエスト / 永続化データ

[訳注:原文ではこのセクションのタイトルが前セクションと全く同じ "Component Event Requests / Persistent Data" なのでその通り訳してあるが、"Component Event Requests / Activation Context" か何かの間違いと思われる]

前の例をProductListingページを変更せずに改善することができます。 パッシベーション/アクティベーションコンテキストを用いてセッションの使用を避け、リンクをブックマーク可能とします。

ProductDetails.java:

  @Inject
  private ProductDAO dao;
  
  private Product product;
  
  private long productId;
  
  public void setProductId(long productId) { productId = productId; }
  
  void onActivate(long productId)
  {
    this.productId = productId;
    
    product = dao.getById(productId);
  }
  
  long onPassivate() { return productId; }

この変更でレンダーリクエストURLは製品idを含んだ "http://.../productdetails/99" というURLとなります。

これには、ProductListingのonActionFromSelectメソッド内の型安全なJavaコードでページ間の接続が行われるという利点があります。 リンクのクリックによってサーバとの間に二往復の通信が発生するという欠点もあります。

レンダーのみのリクエスト

これがマスター/詳細リレーションシップにおける最も一般的な形です。

ProductListing.html:

  <t:loop source="products" value="product">
    <a t:type="pagelink" page="productdetails" context="product.id">${product.name}</a>
  </t:loop>

ProductListing.java:

リンクをサポートするコードは不要です。

ProductDetails.java:

  @Inject
  private ProductDAO dao;
  
  private Product product;
  
  private long productId;
   
  void onActivate(long productId)
  {
    this.productId = productId;
    
    product = dao.getById(productId);
  }
  
  long onPassivate() { return productId; }

setProductId()メソッドは不要となりました。

制限

アプリケーションのワークフローが拡張されていくと、ページアクティベーションコンテキスト以外にはリクエストをまたいでデータを永続的に保存する合理的な方法が無いことにあなたは気づくでしょう。 例えば、ユーザーがProductDetailsページから関連ページを巡回しProductDetailsへ戻ってくることができるようにする場合、 ページからページへそしてまた別のページへと製品idを回し続ける必要が生じます。

ある点では永続値はより意味のあるものです(At some point, persistent values make more sense)。 近い将来、製品idのような永続データをクエリパラメータ(やフォームのhiddenフィールド)に自動的にエンコードするクライアント側での永続化方法を用意します。