SeamFramework.org Community Documentation

- 前のページ
- 次のページ

## 第7章 対話とワークスペースの管理

本章では、 そろそろ Seam の対話モデルについて詳細に理解していくことにします。

事のはじまりは、 3 つの思いつきが融合した結果、 Seam 「対話」の概念となったことです。

- ワークスペース という思いつき、 これは 2002 年にビクトリア州政府 (オーストラリア) のプロジェクトで思いつきました。 このプロジェクトで、私は Struts の上にワークスペース管理を実装せざるを得なくなりました。2 度と繰り返したいとは思わないような経験でした。

- 楽観的セマンティクスで動作するアプリケーショントランザクションという思いつきに加え、 ステートレスなアーキテクチャをベースとする既存のフレームワークでは拡張された永続コンテキストの効率的な管理は実現できないことを実感した事実でした。 (Hibernate チームは `LazyInitializationException` に対する非難は聞き飽きていましたし、 実際にはこれは Hibernate に問題があるのではなく、 むしろ Spring フレームワークや J2EE における従来の stateless session facade (anti) パターンなどステートレスアーキテクチャでサポートされる極端に限定的な永続コンテキストモデルに問題があったのです。)

- ワークフロータスクという思いつき

こうした思いつきを統一しフレームワークで強力なサポートを提供することで、 以前よりすっきりしたコードでより豊かで効率的なアプリケーションをビルドできるパワフルな構成概念を得ました。

## 7.1. Seam の対話モデル

これまでに見た例は、以下の規則に従う非常に単純な対話モデルを利用します。

- JSF 要求ライフサイクルである、 要求値の適用、 プロセスの妥当性検証、 モデル値の更新、 アプリケーションの呼び出し、 応答のレンダリングの各フェーズの期間、 常にアクティブな対話コンテキストがあります。

- JSF 要求ライフサイクルであるビューの復元フェーズの終わりで、 Seam はそれまでの長期対話コンテキストの復元を試みます。 存在しなければ、 Seam は新しい一時的な対話コンテキストを生成します。

- `@Begin` メソッドが出てくると、 一時的な対話コンテキストは長期対話に昇格します。

- `@End` メソッドが出てくると、 どのような長期対話コンテキストでも一時的な対話に降格されます。

- JSF 要求ライフサイクルである応答のレンダリングのフェーズの終わりで、 Seam は長期対話コンテキストの内容を記憶するか、 一時的な対話コンテキストの内容を破棄します。

- どのような faces 要求 (JSF ポストバック) でも対話コンテキストを伝播します。 デフォルトでは、 non-faces 要求 (例えば、 GET 要求) は対話コンテキストを伝播しませんが、 これについての詳細は下記を参照してください。

- JSF 要求ライフサイクルがリダイレクトによって短縮される場合、 Seam は透過的に現在の対話コンテキストを保存および復元します (その対話が `@End (beforeRedirect=true)` で既に終了されていない限り)。

Seam は透過的に対話コンテキスト (一時的な対話コンテキストを含む) を JSF ポストパックおよびリダイレクト全体に伝播します。 特に何もしなければ 非 faces 要求 (たとえば GET 要求など) は対話コンテキストを伝播せず新しい一時的な対話内で処理されます。 常にとは限りませんが、 これが通常求められる動作になります。

non-faces 要求全体に Seam 対話を伝播させたい場合、 要求パラメータとして Seam 対話 ID (conversation id) を明示的にコード化する必要があります。

    <a href="main.jsf?#{manager.conversationIdParameter}=#{conversation.id}"

    >Continue</a

    >

JSF のようにする場合には以下のようにします。

    <h:outputLink value="main.jsf">

        <f:param name="#{manager.conversationIdParameter}" value="#{conversation.id}"/>

        <h:outputText value="Continue"/>

    </h:outputLink

    >

Seam タグライブラリを使用する場合、 以下は等価です。

    <h:outputLink value="main.jsf">

        <s:conversationId/>

        <h:outputText value="Continue"/>

    </h:outputLink

    >

ポストバック用の対話コンテキストの伝播を無効にしたい場合は、 同様のトリックが使えます。

    <h:commandLink action="main" value="Exit">

        <f:param name="conversationPropagation" value="none"/>

    </h:commandLink

    >

Seam タグライブラリを使用する場合、 以下は等価です。

    <h:commandLink action="main" value="Exit">

        <s:conversationPropagation type="none"/>

    </h:commandLink

    >

対話コンテキストの伝播を無効にすることと、 対話を終了することとは全く異なることですので注意してください。

`conversationPropagation` 要求パラメータ または `<s:conversationPropagation>` タグは、 対話の開始と終了、あるいはネストされた対話の開始にも使用することができます。

    <h:commandLink action="main" value="Exit">

        <s:conversationPropagation type="end"/>

    </h:commandLink

    >

    <h:commandLink action="main" value="Select Child">

        <s:conversationPropagation type="nested"/>

    </h:commandLink

    >

    <h:commandLink action="main" value="Select Hotel">

        <s:conversationPropagation type="begin"/>

    </h:commandLink

    >

    <h:commandLink action="main" value="Select Hotel">

        <s:conversationPropagation type="join"/>

    </h:commandLink

    >

この対話モデルは、 マルチウィンドウ操作に関して正常に動作するアプリケーションの構築を容易してくれます。 多くのアプリケーションにとって必要なものはこれだけです。 複雑なアプリケーションのなかには以下の追加要件の両方あるいはどちらかを必要とするものがあります。

- 対話には、 連続的に実行したり同時に実行することもある多くの小さな単位のユーザーインタラクションも含まれます。 より小さい ネストされた対話 (nested conversation) には単独の対話状態セットがあり、 また外側の対話状態へのアクセスもあります。

- ユーザーは同じブラウザのウィンドウ内でいくつもの対話を切り換えることができます。 この機能がワークスペース管理と呼ばれるものです。

## 7.2. ネストされた対話

ネストされた対話は既存の対話のスコープ内で `@Begin(nested=true)` とマークされたメソッドを呼び出すことによって作成されます。 ネストされた対話はそれ自体の対話コンテキストを持っていますが、 外側の対話のコンテキストから値を読み取ることができます。 オブジェクトは参照により取得されるため外側の対話のコンテキストはネストされた対話内では読み取り専用となりますが、 オブジェクト自体への変更はその外側のコンテキストに反映されます。

- 対話をネストするとオリジナルの対話または外側の対話のコンテキストに積み重ねられるコンテキストを初期化します。 外側の対話が親とみなされます。

- ネストされた対話のコンテキストに直接セットされるまたはアウトジェクトされる値はすべて親となる対話のコンテキスト内にアクセス可能なオブジェクトに影響は与えません。

- 対話コンテキストからのコンテキスト検索やインジェクションはまず現在の対話コンテキストにある値を検索し、 値がみつかならないと対話がネストされている場合はその対話スタックに進みます。 このあとわかるように、 この動作は上書き可能です。

次に `@End` が出てくると、 対話スタックから「ポップ(Pop)」されることで、 その取り出された対話は破棄されます。 対話は任意の深さにネストすることができます。

特定のユーザーアクティビティ (ワークスペース管理や戻るボタン) により、 内側の対話が終了する前に外側の対話が開始させることができます。 この場合、 同じ外側の対話に属する複数の並列ネスト対話を持つことができます。 ネストされた対話が終了する前に外側の対話が終了すると、 Seam はネストされた対話コンテキストを外側のコンテキストと共にすべて破棄します。

対話スタックの最下位にある対話がルートの対話になります。 この対話を破棄すると常に派生した対話はすべて破棄されます。 `@End(root=true)` を指定すると宣言的にこれを行うことができます。

対話は継続可能な状態と見なすことができます。 ネストされた対話により、 ユーザーとの交信でのさまざまなポイントにおいてアプリケーションは一貫した継続可能な状態を捕らえることができるようになります。 従って、 戻るボタンを押すことやワークスペース管理に対して正しい動作を保証します。

前述した通り、 現在ネストされている対話の親となる対話にコンポーネントが存在する場合、 このネストされている対話は同じインスタンスを使用します。ときには、 親となる対話内に存在するコンポーネントインスタンスがその子となる対話からは見えなくなるように、 ネストされるそれぞれの対話内に別々のインスタンスを持たせると便利なことがあります。 コンポーネントに `@PerNestedConversation` アノテーションを付けるとこれを行うことができます。

## 7.3. GET 要求を使って対話を開始する

ページが non-faces 要求 (例、 HTTP GET 要求) 経由でアクセスされる場合、 JSF は起動されるアクションリスナーを全く定義しません。 ユーザーがそのページをブックマークする、あるいは `<h:outputLink>` からそのページに行き着く場合などに発生します。

ページがアクセスされたら直ちに対話を開始したい場合があります。 JSF アクションメソッドがないため、 アクションに `@Begin` アノテーションを付けるという普通の方法では問題を解決することができません。

このページが状態をコンテキスト変数にフェッチする必要がある場合、 さらなる問題が発生します。 すでに、2 つの問題解決方法を見てきました。 Seam コンポーネントにその状態が保持される場合、 `@Create` メソッドでその状態をフェッチできます。 保持されていなければ、 コンテキスト変数に対して `@Factory` メソッドを定義することができます。

これらのオプションがうまくいかない場合、 Seam では `pages.xml` ファイルに ページアクション を定義することができます。

    <pages>

        <page view-id="/messageList.jsp" action="#{messageManager.list}"/>

        ...

    </pages

    >

ページがレンダリングされるようとするときは常に、 応答のレンダリングフェーズの冒頭でこのアクションメソッドが呼び出されます。 ページアクションが null 以外の結果を返す場合、 Seam は適切な JSF および Seam ナビゲーションの規則を処理するので、 まったく異なるページがレンダリングさることになるかもしれません。

ページのレンダリング前に行いたいことが対話の開始だけなら、 組み込みアクションメソッドを次のように使用できます。

    <pages>

        <page view-id="/messageList.jsp" action="#{conversation.begin}"/>

        ...

    </pages

    >

また、 この組み込みアクションは JSF コントロールからも呼び出すことができ、 同様に `#{conversation.end}` を使って対話を終了することができます。

既存の対話にジョインするあるいはネストされる対話を開始する、 ページフローまたはアトミック対話を開始するためにさらに制御が必要な場合は、 `<begin-conversation>` 要素を使用してください。

    <pages>

        <page view-id="/messageList.jsp">

           <begin-conversation nested="true" pageflow="AddItem"/>

        <page>

        ...

    </pages

    >

また、 `<end-conversation>` 要素もあります。

    <pages>

        <page view-id="/home.jsp">

           <end-conversation/>

        <page>

        ...

    </pages

    >

1 番目の問題を解決するには、 現在 5 つのオプションから選択できます。

- `@Create` メソッドに `@Begin` アノテーションを追加する

- `@Factory` メソッドに `@Begin` アノテーションを追加する

- Seam ページアクションメソッドに `@Begin` アノテーションを追加する

- `pages.xml` で `<begin-conversation>` を使用する

- `#{conversation.begin}` を Seam ページアクションメソッドとして使用する

## 7.4. 長期対話の要求

あるページでは、長期対話のコンテキストしか持っていません。そのようなページを "保護する" ための一つの方法は、そのページをレンダリングする必要条件として長期対話を必要とすることです。幸いなことに、Seam はこの要件を強制するための組み込みのメカニズムを持っています。

Seam のページ記述子では、次のように `conversation-required` 属性を使用することで描画されるページで、現在の対話が長期対話 （もしくはネストされた対話） でなければならないと指定することができます。

    <page view-id="/book.xhtml" conversation-required="true"/>

## 注意

唯一の欠点は、 どの 長期対話が必要とされるかを指定する組み込みの方法が無いことです。ページアクションの中で、特定の値が対話に存在しているかどうかを2重にチェックすることにより、この基本的な認可を当てにすることができます。

Seam がこのページが長期対話が存在しないところで、要求されていると決定する際に、次のようなアクションが取られます。

- `org.jboss.seam.noConversation` と呼ばれるコンテキスト依存のイベントが作り出されます。

- `org.jboss.seam.NoConversation` というバンドルキーを使って、警告ステータスのメッセージが登録されます。

- 定義されていれば、ユーザーは他のページにリダイレクトされます。

他のページは次のように、 Seam ページ記述子で `<pages>` 要素に `no-conversation-view-id` 属性に定義されます。

    <pages no-conversation-view-id="/main.xhtml"/>

現時点では、アプリケーション全体に対してそのようなページは一つだけ定義することができます。

## 7.5. `<s:link>` と `<s:button>` の使いかた

JSF コマンドリンクは常に JavaScript でフォームサブミットを行います。 これによりウェブブラウザの「新しいウィンドウで開く」または「新しいタブで開く」機能を動作させなくしてしまいます。 プレーンの JFS でこの機能が必要な場合は、 `<h:outputLink>` を使用する必要があります。 ただし、 `<h:outputLink>` には重要な制限が 2 つあります。

- JSF はアクションリスナーを `<h:outputLink>` につなげる方法を提供していません。

- 実際にフォームサブミットがないため、 JSF は選択された `DataModel` の列を伝播しません。

Seam は 1 番目の問題解決に対しては ページアクションという概念を提供していますが、 これは 2 番目の問題についてはまったく役に立ちません。 これについては要求パラメータを渡すという RESTful の方法を使ってサーバー側で選択されたオブジェクトの再問い合わせを行うことで回避可能でしょう。 Seam ブログサンプルアプリケーションなどの場合には、 実際にこれが最適の手段となります。 RESTful スタイルはサーバー側の状態を必要としないためブックマーク機能をサポートします。 ブックマークはあまり必要ないなど、これ以外の場合は、 `@DataModel` および `@DataModelSelection` を使用すると非常に便利で透過的になります。

この機能を補ってさらに対話伝播の管理をより簡略化するために、 Seam は `<s:link>` JSF タグを提供しています。

このリンクは JSF ビュー ID だけを指定することができます。

    <s:link view="/login.xhtml" value="Login"/>

あるいは、 アクションメソッドを指定することができます (この場合、 アクションの結果は最終的なページを確定する)。

    <s:link action="#{login.logout}" value="Logout"/>

JSF ビュー ID とアクションメソッドの両方を指定すると、 アクションメソッドが null 以外の結果を返さない限り「ビュー」が使用されます。

    <s:link view="/loggedOut.xhtml"  action="#{login.logout}" value="Logout"/>

リンクは `<h:dataTable>` 内で使用する `DataModel` の選択列を自動的に伝播します。

    <s:link view="/hotel.xhtml" action="#{hotelSearch.selectHotel}" value="#{hotel.name}"/>

既存の対話のスコープを残しておくことができます。

    <s:link view="/main.xhtml" propagation="none"/>

対話を開始、 終了、 またはネストすることができます。

    <s:link action="#{issueEditor.viewComment}" propagation="nest"/>

リンクが対話を開始すると、 使用されるページプローを指定することもできます。

    <s:link action="#{documentEditor.getDocument}" propagation="begin"

            pageflow="EditDocument"/>

`taskInstance` 属性を jBPM タスクリストを使用する場合に利用できます。

    <s:link action="#{documentApproval.approveOrReject}" taskInstance="#{task}"/>

(上記の例は DVD ストアデモアプリケーションを参照してください。)

最後に、 ボタンとしてレンダリングされる「リンク」が必要な場合は `<s:button>` を使用します。

    <s:button action="#{login.logout}" value="Logout"/>

## 7.6. 成功のメッセージ

動作に対して成功したか失敗したかをユーザーに示すメッセージを表示するのは非常に一般的です。 これには、 JSF `FacesMessage` を使うと便利です。 残念ながら、 成功のアクションはブラウザリダイレクトを要することが多く、 JSF はリダイレクト全体に faces のメッセージは伝播しません。 このためプレーン JSF で成功のメッセージを表示するのはかなり困難になります。

組み込みの対話スコープ Seam コンポーネントである `facesMessages` がこの問題を解決してくれます。 (Seam リダイレクトフィルタをインストールしておく必要があります。)

    @Name("editDocumentAction")

    @Stateless

    public class EditDocumentBean implements EditDocument {

        @In EntityManager em;

        @In Document document;

        @In FacesMessages facesMessages;

        public String update() {

            em.merge(document);

            facesMessages.add("Document updated");

        }

    }

`facesMessages` に追加されるメッセージはすべてすぐ次のフェースである応答レンダリングフェーズで現在の対話に対して使用されます。 これは Seam がリダイレクト全体に一時的な対話コンテキストを維持するので長期実行の対話がない場合でも機能します。

JSF EL 式を faces メッセージサマリーに含ませることもできます。

    facesMessages.add("Document #{document.title} was updated");

たとえば、 通常の方法でメッセージを表示することができます。

    <h:messages globalOnly="true"/>

## 7.7. ナチュラル対話の ID

永続オブジェクトを処理する対話を作業する場合は標準のサロゲート対話 ID ではなくそのオブジェクトのナチュラルキーを使用する方が望ましい場合があります。

既存の対話に容易にリダイレクト

ユーザーが同じ操作を 2 回要求する場合は既存の対話にリダイレクトすると便利な場合があります。 例をあげてみます。 「 「ebay で両親へのクリスマスプレゼントを獲得しそのアイテムの支払いプロセスが途中まで進んでいます。 ここでこのアイテムが直接両親に配達されるようにしようと思います。 支払い詳細は入力しましたが両親の明確な住所を思い出すことができません。 住所を検索しようとして誤って同じブラウザウィンドウを使ってしまいました。 もう一度さっきのアイテムの支払いに戻る必要があります。」 」

ナチュラル対話では既存の対話にユーザーを再度参加させて中断したところからまた始めさせることが非常に簡単です。 対話 ID として itemId を使い payForItem の対話に再度参加させるだけです。

ユーザーフレンドリーな URL

個人的には操作可能な階層と (URL を編集しながら操作可能) 意味のある URL (この Wiki に見られるようにランダムな ID での識別を行わない) で構成されているように見えますが、 アプリケーションユーザーにとってはフレンドリーな URL はそれほど重要ではない場合もあるでしょう。

ナチュラル対話では、 ホテル予約システム (あるいはどのようなアプリケーションであれ) を構築している場合に `http://seam-hotels/book.seam?hotel=BestWesternAntwerpen` (ドメインモデルでマッピングを行うパラメータ `hotel` は何であれユニークでなければならない) のような URL を生成し、 URL 書き換えでこれを容易に http://seam-hotels/book/BestWesternAntwerpen に変換することができます。

かなり改善されたはずです。

## 7.8. ナチュラル対話を作成する

ナチュラル対話は `pages.xml` で定義されます。

      <conversation name="PlaceBid"

                      parameter-name="auctionId"

                      parameter-value="#{auction.auctionId}"/>

上記の定義でまず判るのは対話に名前があることです。 この場合 `PlaceBid` になります。 名前は特定の指定対話を固有に識別するため `page` 定義により参加する指定対話の識別に使用されます。

次の属性 `parameter-name` はデフォルトの対話 ID パラメータの代わりにナチュラル対話の ID を含む要求パラメータを定義します。 この例では `parameter-name` は `auctionId` になります。 つまり、 ページの URL 内に出現する `cid=123` のような対話パラメータの代わりに `auctionId=765432` を含むようになります。

上記の設定の最後の属性 `parameter-value` は対話 ID として使用するナチュラルビジネスキーの値の評価に使用される EL 式を定義します。 この例では対話 ID は現在範囲内の `auction` のプライマリーキー値になります。

次に指定対話に参加するページを定義します。 `page` 定義の `conversation` 属性を指定してこれを行います。

      <page view-id="/bid.xhtml" conversation="PlaceBid" login-required="true">

          <navigation from-action="#{bidAction.confirmBid}"

    >

              <rule if-outcome="success">

                  <redirect view-id="/auction.xhtml">

                      <param name="id" value="#{bidAction.bid.auction.auctionId}"/>

                  </redirect>

              </rule

    >

          </navigation>

      </page

    >

## 7.9. ナチュラル対話にリダイレクトする

ナチュラル対話を開始またはリダイレクトする場合、 ナチュラル対話名を指定するいくつかのオプションがあります。 次のページ定義をまず見てみることにします。

      <page view-id="/auction.xhtml">

        <param name="id" value="#{auctionDetail.selectedAuctionId}"/>

        <navigation from-action="#{bidAction.placeBid}">

          <redirect view-id="/bid.xhtml"/>

        </navigation>

      </page

    >

auction ビューからアクション `#{bidAction.placeBid}` を呼び出していること (これらのサンプルはすべて Seam にある seamBay から取得している)、 前述のようにナチュラル対話 `PlaceBid` で設定される `/bid.xhtml` にリダイレクトしていることがわかります。 アクションメソッドの宣言は以下のようになります。

       @Begin(join = true)

       public void placeBid()

名前が付けられた対話が `<page/>` 要素に指定される場合、 その名前の対話へのリダイレクトはアクションメソッドが完全に呼び出された後でナビゲーションルールの一部として発生します。 既存の対話にリダイレクトする場合はアクションメソッドが呼び出される前にリダイレクトが発生する必要があるため問題となってきます。 したがってアクションが呼び出される場合は対話名を指定する必要があります。 `s:conversationName` タグを使って行うのがその方法のひとつとなります。

      <h:commandButton id="placeBidWithAmount" styleClass="placeBid" action="#{bidAction.placeBid}">

        <s:conversationName value="PlaceBid"/>

      </h:commandButton

    >

もうひとつの方法は `s:link` または `s:button` のどちらかを使用するときに `conversationName` を指定する方法です。

      <s:link value="Place Bid" action="#{bidAction.placeBid}" conversationName="PlaceBid"/>

## 7.10. ワークスペースの管理

ワークスペース管理は、1 つのウィンドウの中で対話を「切り換える」能力です。 Seam はワークスペース管理を Java コードのレベルで完全に透過的にします。 ワークスペース管理を可能にするために、必要なすべては以下の通りです。

- それぞれのビュー ID (JSF または Seam ナビゲーションルールを使用する場合) またはページノード (jPDL ページフロー) に詳細のテキストを入力します。 この詳細テキストはワークスペース切り替えによってユーザーに表示されます。

- ページの中に 1 つ以上の標準ワークスペース切り替え JSP または facelets の断片を含ませます。 標準の断片はドロップダウンメニュー、 対話のリスト、 ブレッドクラム (breadcrumbs) を通じてワークスペース管理をサポートします。

### 7.10.1. ワークスペース管理と JSF ナビゲーション

JSF または Seam ナビゲーションルールを使用する場合、 Seam は対話の現在の `view-id` を復元してその対話に切り替えます。 ワークスペースの記述的なテキストは `pages.xml` と呼ばれるファイルで定義され、 Seam はこのファイルが `WEB-INF` ディレクトリ内の `faces-config.xml` のすぐ次に配置されていることを期待します。

    <pages>

        <page view-id="/main.xhtml">

            <description

    >Search hotels: #{hotelBooking.searchString}</description>

        </page>

        <page view-id="/hotel.xhtml">

            <description

    >View hotel: #{hotel.name}</description>

        </page>

        <page view-id="/book.xhtml">

            <description

    >Book hotel: #{hotel.name}</description>

        </page>

        <page view-id="/confirm.xhtml">

            <description

    >Confirm: #{booking.description}</description>

        </page>

        </pages

    >

このファイルが期待する場所になくても Seam アプリケーションは正常に動作を続行します。 動作しない機能はワークスペースの切り替え機能のみです。

### 7.10.2. ワークスペース管理と jPDL ページフロー

jPDL ページフロー定義を使う場合、 Seam は現在の jBPM のプロセス状態を復元することによって対話に切り替えます。 同じ `view-id` に現在の `<page>` に応じて異なる詳細を持たせることができるためこれはより柔軟なモデルになります。 詳細テキストは `<page>` ノードで定義されます。

    <pageflow-definition name="shopping">

       <start-state name="start">

          <transition to="browse"/>

       </start-state>

       <page name="browse" view-id="/browse.xhtml">

          <description

    >DVD Search: #{search.searchPattern}</description>

          <transition to="browse"/>

          <transition name="checkout" to="checkout"/>

       </page>

       <page name="checkout" view-id="/checkout.xhtml">

          <description

    >Purchase: $#{cart.total}</description>

          <transition to="checkout"/>

          <transition name="complete" to="complete"/>

       </page>

       <page name="complete" view-id="/complete.xhtml">

          <end-conversation />

       </page>

    </pageflow-definition

    >

### 7.10.3. 対話切り替え

次の断片を JSP または facelets のページに含ませて、 現在の対話またはその他いずれのアプリケーションのページにも切り替えられるドロップダウンメニューを取得します。

    <h:selectOneMenu value="#{switcher.conversationIdOrOutcome}">

        <f:selectItem itemLabel="Find Issues" itemValue="findIssue"/>

        <f:selectItem itemLabel="Create Issue" itemValue="editIssue"/>

        <f:selectItems value="#{switcher.selectItems}"/>

    </h:selectOneMenu>

    <h:commandButton action="#{switcher.select}" value="Switch"/>

この例では、 ユーザーに新しい対話を開始させる 2 つの追加アイテムに加えて、 各対話のためのアイテムを含むメニューがあります。

詳細を持つ対話 (`pages.xml` で指定) のみがドロップダウンメニューに含まれます。

### 7.10.4. 対話一覧

対話一覧は対話切り替えに非常によく似ていますが、 表形式で表示される点が異なります。

    <h:dataTable value="#{conversationList}" var="entry"

            rendered="#{not empty conversationList}">

        <h:column>

            <f:facet name="header"

    >Workspace</f:facet>

            <h:commandLink action="#{entry.select}" value="#{entry.description}"/>

            <h:outputText value="[current]" rendered="#{entry.current}"/>

        </h:column>

        <h:column>

            <f:facet name="header"

    >Activity</f:facet>

            <h:outputText value="#{entry.startDatetime}">

                <f:convertDateTime type="time" pattern="hh:mm a"/>

            </h:outputText>

            <h:outputText value=" - "/>

            <h:outputText value="#{entry.lastDatetime}">

                <f:convertDateTime type="time" pattern="hh:mm a"/>

            </h:outputText>

        </h:column>

        <h:column>

            <f:facet name="header"

    >Action</f:facet>

            <h:commandButton action="#{entry.select}" value="#{msg.Switch}"/>

            <h:commandButton action="#{entry.destroy}" value="#{msg.Destroy}"/>

        </h:column>

    </h:dataTable

    >

恐らく、 多くの方が独自のアプリケーションに合うようカスタマイズを希望するだろうと思います。

詳細を持つ対話のみがこの一覧に含まれます。

対話一覧によりユーザーがワークスペースを破壊することができるので留意してください。

### 7.10.5. ブレッドクラム (Breadcrumbs)

ブレッドクラムは、 ネストされた対話モデルを使うアプリケーションで役に立ちます。 ブレッドクラムは、 現在の対話スタック内の対話へのリンクの一覧になります。

    <ui:repeat value="#{conversationStack}" var="entry">

        <h:outputText value=" | "/>

        <h:commandLink value="#{entry.description}" action="#{entry.select}"/>

    </ui:repeat

## 7.11. 対話型コンポーネントと JSF コンポーネントのバインディング

重要ではありませんが、 JSF コンポーネントへのバインディングの保持には使用できないという制限が対話型コンポーネントにはあります。 (一般的には、 アプリケーション論理からビューに対する強い依存関係を作ってしまうため、 絶対的に必要でない限り一般的にはこの JSF の機能は使用しないようにした方がよいでしょう。) postback 要求で、 Seam 対話コンテキストが復元される前、 ビューの復元フェーズ中にコンポーネントのバインディングは更新されます。

これを回避するには、 イベントスコープコンポーネントを使ってコンポーネントバインディングを格納し、 それを必要とする対話スコープコンポーネントにインジェクトします。

    @Name("grid")

    @Scope(ScopeType.EVENT)

    public class Grid

    {

        private HtmlPanelGrid htmlPanelGrid;

        // getters and setters

        ...

    }

    @Name("gridEditor")

    @Scope(ScopeType.CONVERSATION)

    public class GridEditor

    {

        @In(required=false)

        private Grid grid;

        ...

    }

また、 対話スコープのコンポーネントを JSF コントロールのバインド先となるイベントスコープのコンポーネントにはインジェクトできません。 これには `facesMessages` のようなコンポーネント内での Seam 構築が含まれます。

代わりに 暗黙的な `uiComponent` ハンドルで JSF コンポーネントツリーにアクセスすることができます。 次の例では反復の間にデータテーブルを戻す `UIData` コンポーネントの `getRowIndex()` にアクセスして、現在の列番号を出力します。

    <h:dataTable id="lineItemTable" var="lineItem" value="#{orderHome.lineItems}">

       <h:column>

          Row: #{uiComponent['lineItemTable'].rowIndex}

       </h:column>

       ...

    </h:dataTable

    >

JSF UI のコンポーネントはこのマップ内のクライアント識別子で使用可能になります。

## 7.12. 対話的コンポーネントへの並列コール

Seam コンポーネントへの並列コールに関する全般は 項4.1.10. 「同時並行処理モデル」 でご覧ください。 ここでは並列が発生する最も一般的な状況について説明しています (AJAX 要求から対話的コンポーネントにアクセスする)。 クライアントで発生したイベントの制御に Ajax クライアントライブラリが提供すべきオプションについて説明してから RichFaces で提供されるオプションについて見ていきます。

対話的コンポーネントでは実際の並列アクセスは許可されないので、 Seam は要求を連続的に処理するようそれぞれを待ち行列に入れます。 これにより各要求は決定論的に実行されるようになります。 ただし、 シンプルなキューはあまりよくありません。 まず、 メソッドがなんらかの理由で完了に長時間かかっている場合、 クライアントが要求を生成するたび繰り返してそれを実行するのはよくありません (サービス妨害攻撃の可能性)。 また、 ユーザーに対するステータスのクィックアップデートに AJAX が頻繁に使用されるためアクションを長時間実行し続けるのは実用的ではありません。

したがって、 長時間実行の対話の内側で作業する場合は Seam は一定期間アクションイベントを待ち行列に入れます (並列要求タイムアウト)。 時間通りにイベントを処理できないと一時的な対話を作成してユーザーに状況を知らせるメッセージを出力します。 このため、 AJAX イベントでサーバーを溢れさせないようにすることが非常に重要となります。

components.xml で並列要求のタイムアウトにミリ秒単位で適当なデフォルトを設定することができます。

    <core:manager concurrent-request-timeout="500" />

また、 ページごとに並列要求のタイムアウトを微調整することもできます。

    <page view-id="/book.xhtml"

             conversation-required="true"

             login-required="true"

             concurrent-request-timeout="2000" />

ここまではユーザーに対して連続的に出現する AJAX 要求について説明してきました。 クライアントはサーバーにイベントが発生したことを伝え、 その結果に応じてページの一部を表示します。 この方法は AJAX が軽量である場合には最適となります (1 コラム内の数字の合計を計算するなど呼び出されるメソッドがシンプルである場合)。 しかし、 計算に 1 分を要するような複雑な計算が必要な場合はどうでしょう。

重量のある計算にはポーリングベースの方法を使用してください。 クライアントがサーバーに AJAX 要求を送信し、 これによりサーバーで非同期にアクションが実行され (クライアントへの応答が即時となる)、 クライアントは次にサーバーに更新をポーリングします。 それぞれのアクションがすべて実行する (いずれもタイムアウトしない) ことが重要となるような長時間実行のアクションがある場合に適した方法となります。

### 7.12.1. 対話的 AJAX アプリケーションを設計する

まず、 簡単な「連続」要求を使用するのかどうか、 あるいはポーリングの方法を使用するのかどうかを決める必要があります。

「連続」要求を選択する場合は要求が完了する長さを推測する必要があります。 並列要求のタイムアウトに比べ十分に短い時間で完了するかどうか推測してみます。 不十分な場合はこのページの並列要求のタイムアウトを変更します (前述の通り)。 サーバーを要求で溢れさせないようにするためクライアント側で行列待ちをさせた方がよいでしょう。 イベントが頻繁に発生し (入力フィールドの keypress や onblur など) クライアントの即時更新が優先事項ではない場合はクライアント側で要求の遅延をセットしてください。 要求遅延の作業を行う場合、 イベントがサーバー側でも行列待ちできるようにすることを考慮に入れてください。

最後に、 クライアントライブラリは未完了の重複要求を最新のものを残してすべて停止するオプションを提供することができます。

ポールスタイルのデザインを使用する場合は細かな調整があまり必要ありません。 アクションメソッド `@Asynchronous` をマークしてポーリングの間隔を決定するだけです。

    int total;

    // This method is called when an event occurs on the client

    // It takes a really long time to execute

    @Asynchronous

    public void calculateTotal() {

       total = someReallyComplicatedCalculation();

    }

    // This method is called as the result of the poll

    // It's very quick to execute

    public int getTotal() {

       return total;

    }

### 7.12.2. エラーを処理する

並列要求を対話的コンポーネントに対して行列待ちに入れるよう十分注意をしてアプリケーションを設定しても、 サーバー側がオーバーロードとなり要求処理にかかる時間が `concurrent-request-timeout` を越えるまでに全要求を処理できなくなる危険性があります。 こうした場合、 Seam は `pages.xml` で処理が可能な `ConcurrentRequestTimeoutException` を送出します。 HTTP 503 エラーの送信をお勧めします。

       <exception class="org.jboss.seam.ConcurrentRequestTimeoutException" log-level="trace">

          <http-error error-code="503" />

       </exception

    >

## 503 Service Unavailable (HTTP/1.1 RFC)

サーバーは現在、 一時的な過負荷またはメンテナンスのため要求を処理することができません。 しばらく待つと緩和されるであろう一時的な状態であることを指します。

代わりにエラーページにリダイレクトすることができます。

    <exception class="org.jboss.seam.ConcurrentRequestTimeoutException" log-level="trace">

       <end-conversation/>

       <redirect view-id="/error.xhtml">

          <message

    >The server is too busy to process your request, please try again later</message>

       </redirect>

    </exception

    >

ICEfaces、 RichFaces Ajax、 Seam Remoting はいずれも HTTP エラーのコードを処理することができます。 Seam Remoting は HTTP エラーを示すダイアログボックスをポップアップします。 ICEfaces はその接続ステータスのコンポーネントでエラーを示します。 RichFaces はユーザー定義が可能なコールバックを提供することで HTTP エラー処理にほぼ完全に対応します。 たとえば、 ユーザーにエラーメッセージを表示するには、

    <script type="text/javascript">
       A4J.AJAX.onError = function(req,status,message) {
          alert("An error occurred");
       };
    </script
    >

エラーコードの代わりに、セッションタイムアウトなどでサーバーがビューの有効期限切れを伝えて来た場合、このシナリオを取り扱うために、 RichFaces の別のコールバック関数を使用します。

    <script type="text/javascript">
       A4J.AJAX.onExpired = function(loc,message) {
          alert("View expired");
       };
    </script
    >

もしくは、 RichFaces はエラーを取り扱うことができます。この場合、ユーザーは "View state could't be restored - reload page?" というプロンプトを表示されることになるでしょう。アプリケーションのリソース群で次のメッセージキーを設定することで、このメッセージを国際的にカスタマイズする事ができます。

    AJAX_VIEW_EXPIRED=View expired. Please reload the page.

### 7.12.3. RichFaces (Ajax4jsf)

RichFaces (Ajax4jsf) は Seam で使用される最も一般的な Ajax ライブラリであり、 上述したような制御をすべて提供します。

- `eventsQueue` — イベントが配置されるキューを提供します。 すべてのイベントは待ち行列に配置され、 要求は連続的にサーバーに送信されます。 サーバーへの要求の実行に時間がかかる可能性がある場合に便利です (例えば、複雑な計算、 遅いソースからの情報の読み出し)。 これを使用することによりサーバーが要求で溢れないようにすることができます。

- `ignoreDupResponses` — より新しい「同様の」要求が既にキューにある場合は要求によって生成される応答を無視します。 ignoreDupResponses="true" はサーバー側での要求の処理を 取り消すわけではありません。 クライアント側で不必要な更新を避けるだけです。

  このオプションは複数の並列要求が作成されるのを許可するため、 Seam の対話で使用する際は注意が必要です。

- `requestDelay` — 要求がキューに残っている時間を定義します (ミリ秒単位)。 要求がこの時間を過ぎても処理されないと要求は送信されるか (応答が受信されているかどうかにかかわらず) 破棄 (より新しい同様のイベントがキューにある場合) されます。

  このオプションは複数の並列要求が作成されるのを許可するため、 Seam の対話で使用する場合は注意が必要です。 アクションが実行に要する時間よりセットした遅延 (並列要求のタイムアウトと併用) の方が長いことを確認する必要があります。

- `<a:poll reRender="total" interval="1000" />` — サーバーにポーリングを行い必要に応じてエリアを表示します。

- 前のページ第6章 イベント、インタセプタ、例外処理
- 上に戻る
- ホーム
- 次のページ第8章 ページフローとビジネスプロセス