JSF 2.0 Ajaxで画面遷移に少し深く突っ込む

JSF 2.0ではAjax利用時も通常の画面全体のサブミットとなんら変わることなく処理が出来るというのは以前かるく書いた。
http://d.hatena.ne.jp/shin/20091115/p3


普通にアクションベースでWebアプリを書いているとAjaxと画面遷移は面倒だということがわかるはず。汎用的にやろうと戻り値を見て画面遷移用なのか、画面書き換えようなのか処理する部分を書かないといけない。そうするとクライアントサイドも面倒だが、Ajaxと画面全体の呼び出しと通るルーチンが異なってしまってバグも生み出しやすい。


JSF 2.0はその辺を全部自動でやってくれる。Ajaxによる画面の部分書き換えなのか、Ajaxによる画面全体の更新なのか、指定したURLそのものを取りに生かせるかはまったく意識せずに行える。


以下サンプルを乗せてみる。つぶやこうとしたときにログインしていなければログイン画面に飛ばすという処理。そこでログインするとまたもとの画面に戻ってきて処理が続行できる。一度ログインしていれば次からはログイン画面が開くことが無い。

ソースコード

メイン画面のテンプレート

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:f="http://java.sun.com/jsf/core">
    <head jsfc="h:head">
        <title>メイン画面</title>
        <h:outputScript name="jsf.js" library="javax.faces" />

    </head>
    <body>
        <p>メイン画面</p>
        <form jsfc="h:form">
            <input id="inputText" jsfc="h:inputText" value="#{mainView.テキスト}"/>
            <input type="submit" jsfc="h:commandButton" value="つぶやく" action="#{mainView.送信}">
                <f:ajax execute="inputText"  render="inputText outputList"/>
            </input>
            <h:panelGroup id="outputList">
                <ul >
                    <ui:repeat  value="#{mainView.一覧}" var="item">
                        <li>#{item}</li>
                    </ui:repeat>
                </ul>
            </h:panelGroup>
        </form>
    </body>
</html>


メイン画面の処理コード。Sessionスコープ。ログイン用の管理されたBeanを注入している。

package managed;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ManagedProperty;
import javax.faces.bean.SessionScoped;


@ManagedBean
@SessionScoped
public class MainView implements Serializable{
    //JSF 2.0はアノテーションで注入できる
    @ManagedProperty("#{login}")
    private Login login;

    private String テキスト;
    private List<String> 一覧 = new ArrayList<String>();


    public String 送信(){
        if(login.isログイン() == false){
            login.set元の画面("index");//戻ってくる画面をセット
            return "login?faces-redirect=true";
        }

        String text = String.format("[%tF %<tT] %s" ,new Date() ,テキスト);
        一覧.add(0 ,text);//先頭に入れる

        テキスト = "";//空っぽにする
        return null;//画面遷移なし
    }


    //--------------------------------------------------------------
    //以下アクセサメソッド
    public void setテキスト(String テキスト) {
        this.テキスト = テキスト;
    }

    public String getテキスト() {
        return テキスト;
    }

    public List<String> get一覧() {
        return 一覧;
    }

    public void setLogin(Login login) {
        this.login = login;
    }

}

ログイン画面のテンプレート。入力項目はマッピングしていないのでダミー。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
    <head>
        <title>ログイン</title>
    </head>
    <body>
        <p>ログイン</p>
        <form jsfc="h:form">
            ID<input value=""/><br/>
            PASSWORD<input value=""/><br/>
            <h:commandButton value="ログイン" action="#{login.action}"/>
        </form>
    </body>
</html>


ログインまわりを処理する管理されたBean。Sessionスコープ。ログインは常に成功で。

package managed;

import java.io.Serializable;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;

@ManagedBean
@SessionScoped
public class Login implements Serializable{

    private String 元の画面;
    private boolean ログイン;
    

    public String action(){
        ログイン = true;

        String location  = 元の画面 + "?faces-redirect=true";

        元の画面 = "";
        return location;
    }

    
    //---------------------------------------------------
    //以下アクセサメソッド
    public void set元の画面(String 元の画面) {
        this.元の画面 = 元の画面;
    }

    public boolean isログイン() {
        return ログイン;
    }
}

実行

実行すると初期画面が表示されている。入力して送信すると・・・
http://shin.cside.com/diary/2010/0627-01.png


ログインしていないのでログイン画面が表示される。
http://shin.cside.com/diary/2010/0627-02.png


ログインすると戻ってくる。先ほど入力した文字はそのまま。
http://shin.cside.com/diary/2010/0627-03.png


ボタンを押すと今度は下に追加されて、入力欄がクリアされる。
http://shin.cside.com/diary/2010/0627-04.png


さらに追加すると上に追加されているのがわかる。
http://shin.cside.com/diary/2010/0627-05.png


ということで、ログインしているかどうかのチェック等は今回はinvoke applicationフェーズ(つまり通常のロジック)でやってるけど、フィルタでやるのではなくAOPやリスナーあたりでやるのが望ましい。画面全体のsubmitを主体とするならフィルタは楽で良いけど、Ajax等がからむと面倒なことになる。


このサンプルではリダイレクト指定しているのでURLがかわっているのがわかると思う。

今回は入力した内容を保持しておきたかったのでメイン画面がセッションスコープにしてある。細かくスコープ管理したい場合、フラッシュスコープ(Railsであるようなリダイレクト後まで有効なやつ。JSF 2.0で標準装備)でログイン画面に渡して、ログイン画面内ではViewスコープで保持というのがベストだろうか。

Ajaxを多用する場合基本的に1画面に1:1で対応するようにViewScope/RequestScopeで作成しておいて、画面遷移は全てGET(今回のようにリダイレクト含む)、画面間に受け渡しにFlashスコープというのは画面の流れがわかっている場合はありかと。

特に画面遷移はすべてGETで動くというのはURLがわかりやすいという利点がある。JSF 2.0ではGETパラメータの受け取りやポストバックではない画面遷移ができるのでこの辺はうんまくやっていきたいところ。