JSF をめぐる話題

メニュー

初めに

 ここでは順不同に、JSF に関する話題を書いていきます。事の成り行き上、まとまりに欠けることはご了解ください。また、新しい項目に気が付いた場合には、随時内容を増やしまた修正していきたいと思います。

 なんといっても機能豊富なページングの実装に関する説明から始めます。

ページングの実装(複合コンポーネントの利用方法)

 ページングの機能は、その画面を定義する xhtml ファイルと、その画面専用の管理 Bean の2つから構成されます。この画面定義は、複合(合成)コンポーネントと呼ばれる技術となります。一方の管理 Bean は通常の Java クラスです。以下では順にページング機能の実装方法を説明していきます。

  xhtml ファイルは pagingPart.xhtml とし、管理 Bean のクラス名は PagingModel とします。ページングの外観は ここ を参照してください。先頭ページや最終ページなどで使用しているアイコンは Bootstrap のものを使用しています。この部分は適宜読み替えてください。

ページング用 xhtml


 第1ステップは、複合コンポーネントの作成になります。 KnowledgeDB プロジェクトを選択した状態で、メニューの「ファイル」→「新規ファイル」を選択してください。

 「カテゴリ」から「JavaServer Faces」を、「ファイル・タイプ」から「JSF合成コンポーネント」を選択して、「次へ」をクリックしてください。



 「ファイル名」に pagingPart と入力し、「場所」や「フォルダ」はデフォルトのままでかまいません。「終了」をクリックしてください。

 上記の「場所」と「フォルダ」で指定した位置に、 pagingPart.xhtml ファイルができています。


 第2ステップは、 xhtml ファイルを編集します。そのソース(部分)を以下に示します。

                    
    
1   <cc:interface>
2       <cc:attribute name="pagingModel" />
3   </cc:interface>

    
4   <cc:implementation>
5       <div class="text-center #{cc.attrs.pagingModel.isValid() ? '' : 'hidden'}">
6           <a jsfc="h:link" type="button" outcome="#{cc.attrs.pagingModel.firstUrl}" class="btn btn-default #{cc.attrs.pagingModel.isMode(1) ? '' : 'hidden'}" >
                <span class="glyphicon glyphicon-fast-backward"></span>
            </a>
7           <a jsfc="h:link" type="button" outcome="#{cc.attrs.pagingModel.prevUrl}" class="btn btn-default #{cc.attrs.pagingModel.isMode(2) ? '' : 'hidden'}" >
                <span class="glyphicon glyphicon-step-backward"></span>
            </a>
8           <span jsfc="ui:repeat" var="id" value="#{cc.attrs.pagingModel.numbers}" style="margin: 5px;font-size: 1.5em;" >
                <a jsfc="h:link" type="text" outcome="#{cc.attrs.pagingModel.getNumberUrl(id)}" value="#{id.toString()}" class="#{id == cc.attrs.pagingModel.currentIndex + 1 ? 'text-danger' : 'text-primary'}"></a>
            </span>
9           <a jsfc="h:link" type="button" outcome="#{cc.attrs.pagingModel.nextUrl}" class="btn btn-default #{cc.attrs.pagingModel.isMode(4) ? '' : 'hidden'}" >
                <span class="glyphicon glyphicon-step-forward"></span>
            </a>
10          <a jsfc="h:link" type="button" outcome="#{cc.attrs.pagingModel.lastUrl}" class="btn btn-default #{cc.attrs.pagingModel.isMode(8) ? '' : 'hidden'}" >
                <span class="glyphicon glyphicon-fast-forward"></span>
            </a>
        </div>
    </cc:implementation>

                

 かなり複雑と思います。特に、管理 Bean である PagingModel との間を何度も行き来しないと理解が困難と思います。以下に簡単ですが、説明します。
  xhtml は2つの部分からなります。cc:interface と cc:implementation とです。 cc:interface はこの xhtml を外部から利用するインターフェースとなります。ここではペアとなって動作する管理 Bean クラスの名前を pagingModel のパラメータ名で受け渡しすること宣言しています。

 一方 cc:implementation は実装の定義になります。全体を div タグで囲っています(5行目)。PagingModel の isValid メソッドを呼び出して、 div タグの表示/非表示を制御しています。1件もデータが存在しない場合は、ページングに関係する表示を一切表示させないためです。

 ここでページングの実際の画面を見てください。全体は、次の5つの部分に分かれています。先頭ページに移動するボタン・1つ前ページに移動するボタン・数字を直接クリックして移動するエリア・1つ後ページに移動するボタン・最終ページに移動するボタンです。 div タグの内容は、その5つの部分に対応しています。

 6行目の説明です。 a タグをどのように動的に作成しているかが肝になります。 outcome="#{cc.attrs.pagingModel.firstUrl}" により、PageingModel に先頭ページの URL を計算させています。 #{cc.attrs.pagingModel.isMode(1) ? '' : 'hidden'}" の部分で、先頭ページへ移動するボタンが有効かどうかを調べています。 PagingModel の設定により、先頭ページへの移動を無効にする機能があるため、その状態を調べています。 class="glyphicon glyphicon-fast-backward" は Bootstrap の該当するアイコンを指定しています。

 この行が理解できれば、7、9、10行目は同様に理解できます。

 8行目は少しばかり複雑です。この行では直接数字をクリックして遷移することを可能にしています。この処理を行う上で必要な情報は次のものになります。データの総件数・1ページの表示データ数・現在表示しているページ番号・データ総件数があまりに多い場合に直接選択できる数字をいくつまで表示するか、の4つとなります。こうした設定値はすべて PagingModel が管理するようになっています。以下のPagingModel のソースと見比べてください。
 問題があるとすると、 style 属性で直接マージンやフォントサイズを指定していることです。ここはより柔軟性を持たせた方がよいかもしれません。

ページング用 Bean クラス

 ページング用の管理 Bean クラス PagingModel のソース(部分)を次に示します。

                    
public class PagingModel extends BaseBean {
    /*
     ページングで使用するナビゲーション用のボタンとリンク
    FIRST   先頭ページに移動するボタン
    PREV    前頁に移動するボタン
    NEXT    次頁に移動するボタン
    LAST    最終ページに移動するボタン
    NUMBER  ページ番号で直接移動するページ番号
    ALL     全てのナビゲーション用ボタンとリンクを有効にする
    */
    public static final int FIRST = 0x01;
    public static final int PREV = 0x02;
    public static final int NEXT = 0x04;
    public static final int LAST = 0x08;
    public static final int NUMBER = 0x10;
    public static final int ALL = FIRST | PREV | NEXT | LAST | NUMBER;
    
    /*
    現在のナビゲーションボタンとリンク
    デフォルトではALL
    */
    private int mode = PagingModel.ALL;
    
    /*
    データの総数
    */
    private int totalItems = 0;
    
    /*
    現在表示しているページのインデックス(ゼロベース)
    */
    private int currentIndex = 0;
    
    /*
    1ページに表示するデータの件数
    デフォルトでは10件
    */
    private int pageItems = 10;
    
    /*
    表示モードにNUMBERが含まれている場合に、何件のページ番号を表示するか
    デフォルトではページ番号は10件
    */
    private int numberItems = 10;    

    /*
    リンク先のURL
    */
    private String url = "";
    
    /*
    ページ番号をGETで指定するためのパラメータ名
    paramがpageであった場合、?page=0のような書式でURLが呼び出される
    デフォルトのパラメータ名は「"page"」
    */
    private String param = "page";
    
    /*
    GETのパラメータで渡されたページ番号にバインドする変数
    */
    private int page = 0;
    
    private String sql = "";
}                        

                

 ソースに付けてあるコメントをそのまま残しました。

 ページングの実装は、全件検索ではなく、必要なページ部分だけを毎回検索することで行います。そのため検索に使用する SQL 文も PagingModel クラスで管理しています。

 さして複雑なメソッドはありませんが、ここではページ番号を直接クリックしてリンクを作成する場合に使用する、そのページ番号の集合を返すメソッドを以下に示します。例えば、検索データが25件あり、1ページに10件のデータを表示した場合は、{ 1, 2, 3 } となるリストを返すメソッドになります。

                    
    public List<Integer>getNumbers()
    {
1        if (totalItems <= 0 || pageItems <= 0 || numberItems <= 0 || (mode & NUMBER) == 0)
2           return new ArrayList<Integer>();
        
3       int pages = totalItems / pageItems;
4       if (totalItems % pageItems != 0)
5           ++pages;
        
6       int start, current = currentIndex + 1, half = numberItems / 2;
7       if (current <= half)
8           start = 1;
9       else if (current + half > pages)
10          start = pages - numberItems + 1;
        else
11          start = current - half;
 
12      List<Integer> values = new ArrayList<>();
13      for (int i = 0; start <= pages && i <= numberItems - 1; ++i, ++start)
14          values.add(start);
        
15      return values;
    }

                

 1、2行目はページ番号でリンクする必要がない場合です。データの総数がゼロ件の場合、1ページに表示するデータがゼロ件の場合、直接数字でリンクするそのリンク数がゼロ件の場合、そして直接数字でリンクするモードが不要の場合は、空のリストを返しています。

 3~5行目は全ページ数の計算です。5行目はページに端数がある場合の処理。

 6~11行目は、開始するページ番号を計算しています。 current が現在のページ番号です。 half は1ページに表示するデータ件数の半分です。 start は計算により開始ページ番号を決定しています。

 12~14行目は、リンク用のページ番号を計算している部分です。

ページングの利用方法

 このようにして用意したページング用の xhtml と PagingModel クラスをどのようにして利用するか、簡単なのは PagingModel です。 ここ にあるように、利用したい管理 Bean クラスを PagingModel クラスから派生するだけです。
 あまり無いとは思いますが、1画面にページングが2か所に必要になる場合は、継承では実現できません。その場合は PagingModel のインスタンス変数を2つ用意して、移譲を使って実装することになると思います。工夫してください。

  pagingPart.xhtml を利用している knowledgeList.xhtml のソース(部分)を以下に示します。


1 <html xmlns:ez="http://xmlns.jcp.org/jsf/composite/ezcomp">
    …
2   <ez:pagingPart pagingModel="#{knowledgeListBean}" />
3   <f:metadata>
4       <f:viewParam name="page" value="#{knowledgeListBean.page}"/>
5       <f:event type="preRenderView" listener="#{knowledgeListBean.preRender}"/>
    </f:metadata>
    …
</html>    

                

 この部分をページングを必要とするタグの位置にそのまま記述するだけです。

 1行目は、 ez: 名前空間の定義を読み込んでいます。実際にはこれ以外の読み込みも必要ですので、適宜追加してください。

 2行目は、pagingPart.xhtml の cc:interface において管理 Bean クラスの名前は paingModel で受け渡すことにしましたので、 knowledgeListBean を渡します。 knowledgeListBean クラスは PagingModel クラスから派生しましたので、ページングに必要な機能はすべて実装済みです。

 4行目は、knowledgeList.xhtml が呼び出された際の、 GET パラメータの page にバインドする用のメソッドを指定しています。ここで問題があるとすると、 PagingModel ではパラメータの名前を変更可能にしてあります。しかし name="page" としてありますから、パラメータの名前は page 固定になっていることです。ここにも EL 式を記述して、 PagingModel のもつパラメータ名を動的に設定すれば、より柔軟性が高まります。

 5行目は、knowledgeList.xhtml が呼び出された際の初期画面を構築しています。 GET パラメータの page の値により画面を作成しているわけです。

 次に、preRender メソッドを見てみましょう。

                    
    public void preRender()
    {
1       int p = getPage();
2       if (p > 0)
        {
3           --p;
4           setCurrentIndex(p);
            
5           int count = getPageItems();
6           knowledges = select(knowledgeDao.findPage(p * count, getPageItems(), getSql()));
        }
    }             

                

 GET で指定されたページ番号を知り(1行目)、0ベースのインデックスに変更し(3行目)、全件数を調べ(5行目)、該当するページのデータを検索しています(6行目)。今気が付きましたが、2つ目の getPageItems 呼び出しは不要ですね。ソースはこのまま修正しないで載せておきます。


 ブラウザに返される HTML のページングに関係する部分を次にしめします。1ページから3ページまである場合になります。


 1つ目の a タグは、先頭ページに移動する用、2つ目は1つ前に移動するため用、3つ目から5つ目までは、それぞれ1ページ目・2ページ目・3ページ目に移動する用、6つ目は1つ後に移動するため用、最後のリンクは最終ページに移動する用になります。URL の後に page パラメータで移動するページ番号が指定してあります。サーバではこのパラメータを読み取って、正しくページを構成することになります。

  head タグにも jsfc="h:head" 属性を習慣として付けるようにしましょう。通常では問題となることはありませんが、AJAX を使用した場合、以下のようなエラーが発生します。

    「1 つまたは複数のリソースに 'head' のターゲットがありますが、'head' コンポーネントがビューで定義されていません。」

  <head jsfc="h:head"> としていれば、このエラーは発生しません。

JSF のマッピングを変更する方法

  JSF のマッピングは構成ファイルにある web.xml で指定します。通常は以下のようになっています。

                     
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>/faces/*</url-pattern>
    </servlet-mapping>

                 

 もし拡張子が xhtml のものだけに変更したい場合、以下のようにすればよいです。

                     
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>

                 

h:commandButton タグの利用方法

  h:commandButton の disabled が初期状態で true のコンポーネントは、jQuery などを使用してブラウザ上で動的に false に変更したとしても submit 動作をしません。そのため、ボタンの有効/無効の表示を変更する必要がある場合は、2つのボタンを用意して表示を切り替える必要があります。サンプルは ここ を参照してください。

 この解決法はアド・ホックな対応と思えるのですが、より優れた方法が思いつきませんでした。

ポストバックかどうかを判定する方法

 管理 Bean クラスのメソッドで以下のコードで可能です。


    FacesContext.getCurrentInstance().isPostback()                        

                 

状態遷移の方法

  xhtml ファイルに直接遷移先を記述する場合、type 属性を submit にし、 action に遷移先を指定します。


    <input jsfc="h:commandButton" id="addButton" type="submit" value="新 規" action="newPage.xhtml" />

                 

 管理 Bean クラスで遷移先を指定する場合は、以下のようにします。


    <input jsfc="h:commandButton" id="readButtonOn" type="submit" value="検 索" action="#{knowledgeListBean.search()}" />
    
    
    public String search()
    {
        return "newPage.xhtml";
    }                  

                 

 また こちら も参照してください。

現在の処理フェーズを知る方法

  JSF の処理は Web サーバにおいて、6つのフェーズで処理が行われています。現在どの処理フェーズかを知るには、管理 Bean のメソッドで、以下のようにします。


    PhaseId phase = FacesContext.getCurrentInstance().getCurrentPhaseId();                         

                 

  PhaseId オブジェクトに関しては、必要に応じて調べてください。

読み取り専用の h:inputText の注意点

 読み取り専用 readonly=true 属性の付いた <h:inputText> の場合に value 属性を指定しても、ポストバック時に setter メソッドが呼び出されません。ブラウザ上の jQuery で値を設定したとしてもその値は Web サーバで無視されます。 readonly 属性を true にしたうえで、その値を管理 Bean のメソッドで処理したい場合は注意が必要になります。

コメントを記述する方法

  xhtml ファイルで HTML の通常のコメント(<!-- -->)を記述すると、EL 式の部分はコメントにならないのでエラーとなります。EL 式も含めてコメントにするには以下の2つの方法があります。

 その1。 <!-- -->の代わりに、<ui:remove> </ui:remove> タグを使用します。

 その2。 web.xml ファイルに以下のように facelets.SKIP_COMMENTS を記述することができます。すると、<!-- -->がコメントとして認識されます。

                     
    <context-param>
        <param-name>facelets.SKIP_COMMENTS</param-name>
        <param-value>true</param-value>
    </context-param>

                 

コンボボックスが変更された場合にポストバックする方法

  xhtml ファイルに、以下のように定義します。


    <select jsfc="h:selectOneMenu" id="hans" value="#{addInfoWithPersonBean.hanCode}" onchange="submit()" valueChangeListener="#{addInfoWithPersonBean.hanChanged}" >
        <option jsfc="f:selectItems" var="han" value="#{addInfoWithPersonBean.hans}" itemValue="#{han.code}" itemLabel="#{han.getCodeName()}" ></option>
    </select>

                 

  onchange 属性に submit() を指定します。 valueChangeListener に呼び出される管理 Bean のメソッドを記述します。

 また こちら も参照してください。

JSF で table タグを利用する方法

  こちら を参照してください。

h:messages タグの利用方法

  こちら を参照してください。

日付の書式を設定する方法

  こちら を参照してください。

select option タグを利用する方法

  こちら を参照してください。

GET パラメータを知る方法

  こちら こちら を参照してください。

セッションを無効にする方法

  こちら を参照してください。

初期画面表示の処理を指定する方法

  こちら こちら を参照してください。

 以下ではまとまった説明をします。初期画面用の処理を指定するには、 JSF のバージョンにより次の方法があります。

 ・JSF 2.0 以降の方法

                     
    <f:metadata>
         <f:event type="preRenderView" listener="#{myBean.preRender}"/>
    </f:metadata>                        

                 

 この例では、管理 Bean の preRender メソッドが呼び出されます。但し、再描画の度に呼び出されることに注意が必要です。

 ・JSF 2.2 以降の方法

                     
    <f:metadata>
        <f:viewAction action="#{myBean.init()}"/>
    </f:metadata>   

                 

 この例では、管理 Bean の init メソッドが呼び出されます。但しこちらば、初期化時に1度だけ呼び出されます。再描画の度に呼び出すようにしたい場合は、次のようにします。

                     
    <f:metadata>
        <f:viewAction action="#{myBean.init()}" onPostback="true" />
    </f:metadata>   

                 

 このメソッドは JSF のフェーズのうち INVOKE_APPLICATION で呼び出されますが、その呼び出しフェーズを APPLY_REQUEST_VALUES, PROCESS_VALIDATIONS, UPDATE_MODEL_VALUES に変更することができます。

                     
    <f:metadata>
        <f:viewAction action="#{myBean.init()}" phase="APPLY_REQUEST_VALUES" />
    </f:metadata>   

                 

 さらに、呼び出したメソッドから URL を返すことで、画面遷移することができます。

                     
   public String init() {
        return "newPage.html";
   }                         

                 

AJAX との連携での注意点

  AJAX と連携して動的にコンポーネントを変更する場合、その要素に対して HTML の head タグで jQuery を設定しておいても、jQuery のイベントは発生しません。

 その場合は、AJAX で更新する内容自体に JQuery の本体となる <script></script> の部分を含めてしまうことが必要です。

GET による画面遷移の方法

  h:link タグと h:button タグの outcome 属性に遷移先を指定すると GET による遷移となります。一方、 h:commandLink タグと h:commandButton タグを使用すると、POST による遷移となります。ただし、管理 Bean のメソッドから GET 遷移を指定することができます。以下の呼び出しがあった場合を考えます。

                     
    <input jsfc="h:commandButton" type="submit" value="削 除" action="#{myBean.remove()}" />

                 

  remove メソッドが以下のように実装された場合、コメントにあるように遷移の方法が異なります。

                     
    public String remove()
    {
        return "remove.xhtml";                                            // デフォルトのPOSTによる画面遷移
    }

    public String remove()
    {
        return "remove.xhtml?faces-redirect=true";                        // GETによる画面遷移
    }                

                 

 通常 POST による遷移の場合、サーバ上の処理が成功した場合は POST-REDIRECT-GET となる遷移を行うのが原則です。適宜使い分けてください。