会話スコープとAjax

Converasationオブジェクトによる手動スコープ制御を実現する会話スコープ(@ConversationScoped)。
タブブラウザにも対処できる優れものなんですが、制約があります。

それは、URLに特定のクエリパラメータを含めないと会話スコープが有効にならないということです。

以前の会話スコープのサンプルを使ってこのことを確かめてみます。

Java
package sandbox.action;

import java.io.Serializable;
import javax.enterprise.context.Conversation;
import javax.enterprise.context.ConversationScoped;
import javax.inject.Inject;
import javax.inject.Named;

/**
 * @author jabaraster
 */
@Named
@ConversationScoped
public class ConversationAction implements Serializable {
    private static final long serialVersionUID = 8562666832318519767L;

    private int               counter;

    @Inject
    private Conversation      conversation;

    /**
     * 
     */
    public void begin() {
        if (this.conversation.isTransient()) {
            this.conversation.begin();
        }
    }

    /**
     * 
     */
    public void countUp() {
        this.counter++;
    }

    /**
     * 
     */
    public void end() {
        if (!this.conversation.isTransient()) {
            this.conversation.end();
        }
    }

    /**
     * @return the counter
     */
    public int getCounter() {
        return this.counter;
    }

    /**
     * @return
     */
    public boolean isTransient() {
        return this.conversation.isTransient();
    }
}
XHTML
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
>
<h:head>
    <meta charset="UTF-8" />
    <title>会話スコープ</title>
</h:head>
<h:body>
<h:form>
    <ul>
        <li><h:commandButton action="#{conversationAction.begin}" value="会話スコープの開始" /></li>
        <li><h:commandButton action="#{conversationAction.countUp}" value="カウントアップ" /></li>
        <li><h:commandButton action="#{conversationAction.end}" value="会話スコープの終了" /></li>
    </ul>
    <hr/>
    <h:panelGroup id="msg">
        #{conversationAction.counter}<br/>
        会話中: #{!conversationAction.transient}<br/>
    </h:panelGroup>
</h:form>
</h:body>
</html>
初期表示

初期表示ではXHTMLをリクエストします。この状態では当然ながらURLにはクエリパラメータはありません。

会話スコープの開始

「会話スコープの開始」ボタンを押して会話スコープを開始します。

結果のURLには、まだクエリストリングは付いていません。しかしサーバ側では会話スコープが開始されていて、次のリクエストからはクエリストリングが付与されます。
ソースを見るとそのことが確認できます。
ちょっと見にくいですが、formタグのaction属性を見ると、「?cid=8」というクエリストリングが付与されていることが確認出来ます。

以降のリクエストではこのクエリストリングがURLに含まれるため、サーバ側では会話スコープにあるBeansを取り出せる、というわけです。「カウントアップ」を押すとその様子が分かります。

Ajaxとの組み合わせ

さて本題です。
会話スコープの挙動をここまで見てきましたが、Ajaxと組み合わせたときには注意が必要になります。
それは「会話スコープをAjaxで開始あるいは終了したら、h:formタグを再描画する必要がある」ということです。

先ほどのサンプルをAjax化して確認してみましょう。
まずは最小限の範囲のみ再描画するようにAjax化してみます。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
>
<h:head>
    <meta charset="UTF-8" />
    <title>会話スコープ</title>
</h:head>
<h:body>
<h:form>
    <f:ajax render="msg">
        <ul>
            <li><h:commandButton action="#{conversationAction.begin}" value="会話スコープの開始" /></li>
            <li><h:commandButton action="#{conversationAction.countUp}" value="カウントアップ" /></li>
            <li><h:commandButton action="#{conversationAction.end}" value="会話スコープの終了" /></li>
        </ul>
        <hr/>
        <h:panelGroup id="msg">
            #{conversationAction.counter}<br/>
            会話中: #{!conversationAction.transient}<br/>
        </h:panelGroup>
    </f:ajax>
</h:form>
</h:body>
</html>

h:formの直下にf:ajaxを置き、renderにmsgを設定しています。

ところがこれはうまく動きません。具体的には、「会話スコープの開始」を押した後に何度「カウントアップ」を押してもカウントアップしないばかりか、会話スコープ自体が認識されないのです。

これはformタグを再描画していないため、action属性が書き換わっていないことが原因です。
action属性のURLにいつまでたってもクエリストリングが付与されないため、サーバ側は会話スコープ中にあることを認識出来ないのです。

この現象への対処は当然ながらh:formを再描画対象に加えることです。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
>
<h:head>
    <meta charset="UTF-8" />
    <title>会話スコープ</title>
</h:head>
<h:body>
<h:form>
    <f:ajax render="@form">
        <ul>
            <li><h:commandButton action="#{conversationAction.begin}" value="会話スコープの開始" /></li>
            <li><h:commandButton action="#{conversationAction.countUp}" value="カウントアップ" /></li>
            <li><h:commandButton action="#{conversationAction.end}" value="会話スコープの終了" /></li>
        </ul>
        <hr/>
        <h:panelGroup id="msg">
            #{conversationAction.counter}<br/>
            会話中: #{!conversationAction.transient}<br/>
        </h:panelGroup>
    </f:ajax>
</h:form>
</h:body>
</html>

f:ajaxのrender属性を@formに変えました。これでうまく動作します。

ただAjaxを使う理由は再描画の範囲を狭めることにあると思うので、常にh:form全体を再描画するのは避けたいです。
今回であればボタン毎に再描画する範囲を変えるという手が使えます。カウントアップするときは会話スコープに手を加えないのでh:formを再描画する必要がないですから。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
>
<h:head>
    <meta charset="UTF-8" />
    <title>会話スコープ</title>
</h:head>
<h:body>
<h:form>
    <ul>
        <li><h:commandButton action="#{conversationAction.begin}" value="会話スコープの開始">
            <f:ajax render="@form"/>
        </h:commandButton></li>
        <li><h:commandButton action="#{conversationAction.countUp}" value="カウントアップ">
            <f:ajax render="msg"/>
           </h:commandButton></li>
        <li><h:commandButton action="#{conversationAction.end}" value="会話スコープの終了">
            <f:ajax render="@form"/>
        </h:commandButton></li>
    </ul>
    <hr/>
    <h:panelGroup id="msg">
        #{conversationAction.counter}<br/>
        会話中: #{!conversationAction.transient}<br/>
    </h:panelGroup>
</h:form>
</h:body>
</html>

面倒ではありますが、これで必要最小限の範囲の再描画が実現出来ました。ほんとに面倒ですけど・・・

会話スコープの難しさ

会話スコープは便利なのですが、調べるにつれて難しいところもあることが分かってきました。
今回はAjaxと組み合わせたときにh:form全体を再描画せざるを得ない、という問題でしたが、他にも

  • 画面表示と同時に会話スコープを開始したい
  • 会話スコープを終了しつつ自画面を再度表示したい
  • スコープがタイムアウトしたときのエラー処理

などの課題があります。

JSFといい会話スコープといい、課題山積み。