Web應用程式的開發與傳統的單機程式開發在本質上存在著太多的差異,Web應用程式開發人員至今不可避免的必須處理HTTP的細節,而 HTTP無狀態的(stateless)本質,與傳統應用程式必須維持程式運行過程中的資訊有明顯的違背,再則Web應用程式面對網站上不同的使用者同時 的存取,其執行緒安全問題以及資料驗證、轉換處理等問題,又是複雜且難以解決的。
另一方面,本質上是靜態的HTML與本質上是動態的應用程式又是一項違背,這造成不可避免的,處理網頁設計的美術人員與程式設計人員,必須被彼 此加入至視圖元件中的邏輯互相干擾,即便一些視圖呈現邏輯以標籤的方式呈現,試圖展現對網頁設計美術人員的親切,但它終究必須牽涉到相關的流程邏輯。
有很多方案試著解決種種的困境,而各自的著眼點各不相同,有的從程式設計人員的角度來解決,有的從網頁設計人員的角度來解決,各種的框架被提 出,所造成的是各種不統一的標籤與框架,為了促進產能的整合開發環境(IDE)難以整合這些標籤與框架,另一方面,開發人員的學習負擔也不斷的加重,他們 必須一人瞭解多個角色的工作。
JavaServer Faces 的提出在試圖解決這個問題,它試圖在不同的角度上提供網頁設計人員、應用程式設計人員、元件開發人員解決方案,讓不同技術的人員可以彼此合作又不互相干 擾,它綜合了各家廠商現有的技術特點,由Java Community Process(JCP)團隊研擬出來的一套標準,並在2004年三月發表了JavaServer Faces 1.0實作成果。
從網頁設計人員的角度來看,JavaServer Faces提供了一套像是新版本的HTML標籤,但它不是靜態的,而是動態的,可以與後端的動態程式結合,但網頁設計人員不需要理會後端的動態部份,網頁 設計人員甚至不太需要接觸JSTL這類的標籤,也可以動態的展現資料(像是動態的查詢表格內容),JavaServer Faces提供標準的標籤,這可以與網頁編輯程式結合在一起,另一方面,JavaServer Faces也允許您自訂標籤。
從應用程式設計人員的角度來看,JavaServer Faces提供一個與傳統應用程式開發相類似的模型(當然因某些本質上的差異,模型還是稍有不同),他們可以基於事件驅動來開發程式,不必關切HTTP的 處理細節,如果必須處理一些視覺元件的屬性的話,他們也可以直接在整合開發環境上拖拉這些元件,點選設定元件的屬性,JavaServer Faces甚至還為應用程式設計人員處理了物件與字串(HTTP傳送本質上就是字串)間不匹配的轉換問題。
從UI元件開發人員的角度來看,他們可以設計通用的UI元件,讓應用程式的開發產能提高,就如同在設計Swing元件等,UI開發人員可以獨立開發,只要定義好相關的屬性選項來調整細節,而不用受到網頁設計人員或應用程式設計人員的干擾。
三個角色的知識領域原則上可以互不干擾,根據您的角色,您只要瞭解其中一個知識領域,就可以運用JavaServer Faces,其它角色的知識領域您可以不用瞭解太多細節。
當然,就其中一個角色單獨來看,JavaServer Faces隱藏了許多細節,若要全盤瞭解,其實JavaServer Faces是複雜的,每一個處理的環境都值得深入探討,所以學習JavaServer Faces時,您要選擇的是通盤瞭解,還是從使用的角度來瞭解,這就決定了您學習時所要花費的心力。
要使用JSF,首先您要先取得JavaServer Faces參考實作(JavaServer Faces Reference Implementation),在將來,JSF會與Container整合在一起,屆時您只要下載支援的Container,就可以使用JSF的功能。
請至 JSF 官方網站的 下載區 下載參考實作,在下載壓縮檔並解壓縮之後,將其 lib 目錄下的 jar 檔案複製至您的Web應用程式的/WEB-INF/lib目錄下,另外您還需要 jstl.jar 與 standard.jar 檔案,這些檔案您可以在 sample 目錄下,解壓縮當中的一個範例,在它的/WEB-INF/lib目錄下找到,將之一併複製至您的Web應用程式的/WEB-INF/lib目錄下,您總共 需要以下的檔案:
* jsf-impl.jar
* jsf-api.jar
* commons-digester.jar
* commons-collections.jar
* commons-beanutils.jar
* jstl.jar
* standard.jar
接下來配置Web應用程式的web.xml,使用JSF時,所有的請求都透過FacesServlet來處理,您可以如下定義:
"1.0" encoding="ISO-8859-1"?>
"http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
JSF Demo
JSF Demo
Faces Servlet
javax.faces.webapp.FacesServlet
1
Faces Servlet
*.faces
index.html
在上面的定義中,我們將所有.faces的請求交由FaceServlet來處理,FaceServlet會喚起相對的.jsp網頁,例如請求是/index.faces的話,則實際上會喚起/index.jsp網頁,完成以上的配置,您就可以開始使用JSF了。
現在可以開發一個簡單的程式了,我們將設計一個簡單的登入程式,使用者送出名稱,之後由程式顯示使用者名稱及歡迎訊息。
先看看應用程式開發人員要作些什麼事,我們撰寫一個簡單的JavaBean:
package onlyfun.caterpillar;
public class UserBean {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
這個Bean將儲存使用者的名稱,編譯好之後放置在/WEB-INF/classes下。
接下來設計頁面流程,我們將先顯示一個登入網頁/pages/index.jsp,使用者填入名稱並送出表單,之後在/pages/welcome.jsp中顯示Bean中的使用者名稱與歡迎訊息。
為了讓JSF知道我們所設計的Bean以及頁面流程,我們定義一個/WEB-INF/faces-config.xml:
"1.0"?>
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"
"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
/pages/index.jsp
case>
login
/pages/welcome.jsp
case>
user
onlyfun.caterpillar.UserBean
session
在
在
接下來要告訴網頁設計人員的資訊是,他們可以使用的Bean名稱,即
首先網頁設計人員撰寫index.jsp網頁:
<%@taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@page contentType="text/html;charset=Big5"%>
第一個JSF程式
請輸入您的名稱
名稱: "#{user.name}"/>
"送出" action="login"/>
我們使用了JSF的core與html標籤庫,core是有關於UI元件的處理,而html則是有關於HTML的進階標籤。
html標籤庫中幾乎都是與HTML標籤相關的進階標籤,
網頁設計人員不必理會表單傳送之後要作些什麼,他只要設計好歡迎頁面就好了:
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@page contentType="text/html;charset=Big5"%>
第一個JSF程式
"#{user.name}"/> 您好!
歡迎使用 JavaServer Faces!
這個頁面沒什麼需要解釋的了,如您所看到的,在網頁上沒有程式邏輯,網頁設計人員所作的就是遵照頁面流程,使用相關名稱取出資料,而不用擔心實際上程式是如何運作的。
接下來啟動Container,連接上您的應用程式網址,例如:http://localhost:8080/jsfDemo/pages/index.faces,填入名稱並送出表單,您的歡迎頁面就會顯示了。
三、 簡單的導航 Navigation在 第一個JSF程式 中,我們簡單的定義了頁面的流程由 index.jsp 到 welcome.jsp,接下來我們擴充程式,讓它可以根據使用者輸入的名稱與密碼是否正確,決定要顯示歡迎訊息或是將使用者送回原頁面進行重新登入。
首先我們修改一下UserBean:
package onlyfun.caterpillar;
public class UserBean {
private String name;
private String password;
private String errMessage;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
public void setErrMessage(String errMessage) {
this.errMessage = errMessage;
}
public String getErrMessage() {
return errMessage;
}
public String verify() {
if(!name.equals("justin") ||
!password.equals("123456")) {
errMessage = "名稱或密碼錯誤";
return "failure";
}
else {
return "success";
}
}
}
在UserBean中,我們增加了密碼與錯誤訊息屬性,在verify()方法中,我們檢查使用者名稱與密碼,它傳回一個字串,"failure"表示登入錯誤,並會設定錯誤訊息,而"success"表示登入正確,這個傳回的字串將決定頁面的流程。
接下來我們修改一下 faces-config.xml 中的頁面流程定義:
"1.0"?>
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"
"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
/pages/index.jsp
case>
success
/pages/welcome.jsp
case>
case>
failure
/pages/index.jsp
case>
user
onlyfun.caterpillar.UserBean
session
根據上面的定義,當傳回的字串是"success"時,將前往 welcome.jsp,如果是"failure"的話,將送回 index.jsp。
接下來告訴網頁設計人員Bean名稱與相關屬性,以及決定頁面流程的verify名稱,我們修改 index.jsp 如下:
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@page contentType="text/html;charset=Big5"%>
第一個JSF程式
請輸入您的名稱
"#{user.errMessage}"/>
名稱: "#{user.name}"/>
密碼: "#{user.password}"/>
"送出"
action="#{user.verify}"/>
當要根據verify運行結果來決定頁面流程時,action屬性中使用 JSF Expression Language "#{user.verify}",如此JSF就知道必須根據verify傳回的結果來導航頁面。
在JSF中是根據faces-config.xml中
....
/pages/index.jsp
case>
success
/pages/welcome.jsp
case>
case>
failure
/pages/index.jsp
case>
....
對於JSF,每一個視圖(View)都有一個獨特的識別(identifier),稱之為View ID,在JSF中的View ID是從Web應用程式的環境相對路徑開始計算,設定時都是以/作為開頭,如果您請求時的路徑是/pages/index.faces,則JSF會將副檔 名改為/pages/index.jsp,以此作為view-id。
在
您還可以在
....
/pages/index.jsp
case>
#{user.verify}
success
/pages/welcome.jsp
case>
....
....
在導航時,預設都是使用forward的方式,您可以在
....
/pages/index.jsp
case>
success
/pages/welcome.jsp
case>
....
....
您的來源網頁可能是某個特定模組,例如在/admin/下的頁面,您可以在
....
/admin/*
case>
#{user.verify}
success
/pages/welcome.jsp
case>
....
....
在上面的設定中,只要來源網頁是從/admin來的,都可以開始測試接下來的
....
/*
case>
....
....
或者是這樣:
....
*
case>
....
....
JSF Expression Language 搭配 JSF 標籤來使用,是用來存取資料物件的一個簡易語言。
JSF EL是以#開始,將變數或運算式放置在
#{someBeanName}
變數名稱可以是faces-config.xml中定義的名稱,如果是Bean的話,可以透過使用 '.' 運算子來存取它的屬性,例如:
...
"#{userBean.name}"/>
...
在JSF標籤的屬性上," 與 " (或'與')之間如果含有EL,則會加以運算,您也可以這麼使用它:
...
名稱, 年齡:"#{userBean.name}, #{userBean.age}"/>
...
一個執行的結果可能是這樣顯示的:
名稱, 年齡:Justin, 29
EL的變數名也可以程式執行過程中所宣告的名稱,或是JSF EL預設的隱含物件,例如下面的程式使用param隱含物件來取得使用者輸入的參數:
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@page contentType="text/html; charset=Big5"%>
您好, "#{param.name}"/>
param是JSF EL預設的隱含物件變數,它代表request所有參數的集合,實際是一個java.util.Map型態物件,JSF所提供的隱含物件,大致上對應於JSP隱含物件, 不過JSF隱含物件移除了pageScope與pageContext,而增加了facesContext與view,它們分別對應於 javax.faces.context.FacesContext與javax.faces.component.UIViewRoot。
對於Map型態物件,我們可以使用 '.' 運算子指定key值來取出對應的value,也可以使用 [ 與 ] 來指定,例如:
...
您好, "#{param['name']}"/>
...
在 [ 與 ] 之間,也可以放置其它的變數值,例如:
...
"#{someBean.someMap[user.name]}"/>
...
如果變數是List型態或陣列的話,則可以在 [] 中指定索引,例如:
....
"#{someBean.someList[0]}"/>
"#{someBean.someArray[1]}"/>
"#{someBean.someListOrArray[user.age]}"/>
....
您也可以指定字面常數,對於true、false、字串、數字,JSF EL會嘗試進行轉換,例如:
....
"#{true}"/>
....
"#{'This is a test'}"/>
....
如果要輸出字串,必須以單引號 ' 或雙引數 " 括住,如此才不會被認為是變數名稱。
在宣告變數名稱時,要留意不可與JSF的保留字或關鍵字同名,例如不可取以下這些名稱:
true false null div mod and or not eq ne lt gt le ge instanceof empty
使用EL,您可以直接實行一些算術運算、邏輯運算與關係運算,其使用就如同在一般常見的程式語言中之運算。
算術運算子有:加法 (+), 減法 (-), 乘法 (*), 除法 (/ or div) 與餘除 (% or mod) 。下面是算術運算的一些例子:
運算式 | 結果 |
---|---|
#{1} | 1 |
#{1 + 2} | 3 |
#{1.2 + 2.3} | 3.5 |
#{1.2E4 + 1.4} | 12001.4 |
#{-4 - 2} | -6 |
#{21 * 2} | 42 |
#{3/4} | 0.75 |
#{3 div 4} | 0.75,除法 |
#{3/0} | Infinity |
#{10%4} | 2 |
#{10 mod 4} | 2,也是餘除 |
#{(1==2) ? 3 : 4} | 4 |
如同在Java語法一樣 ( expression ? result1 : result2)是個三元運算,expression為true顯示result1,false顯示result2。
邏輯運算有:and(或&&)、or(或!!)、not(或!)。一些例子為:
運算式 | 結果 |
---|---|
#{true and false} | false |
#{true or false} | true |
#{not true} | false |
關係運算有:小於Less-than (< or lt)、大於Greater-than (> or gt)、小於或等於Less-than-or-equal (<= or le)、大於或等於Greater-than-or-equal (>= or ge)、等於Equal (== or eq)、不等於Not Equal (!= or ne),由英文名稱可以得到lt、gt等運算子之縮寫詞,以下是Tomcat的一些例子:
運算式 | 結果 |
---|---|
#{1 < 2} | true |
#{1 lt 2} | true |
#{1 > (4/2)} | false |
#{1 > (4/2)} | false |
#{4.0 >= 3} | true |
#{4.0 ge 3} | true |
#{4 <= 3} | false |
#{4 le 3} | false |
#{100.0 == 100} | true |
#{100.0 eq 100} | true |
#{(10*10) != 100} | false |
#{(10*10) ne 100} | false |
左邊是運算子的使用方式,右邊的是運算結果,關係運算也可以用來比較字元或字串,按字典順序來決定比較結果,例如:
運算式 | 結果 |
---|---|
#{'a' < 'b'} | true |
#{'hip' > 'hit'} | false |
#{'4' > 3} | true |
EL運算子的執行優先順序與Java運算子對應,如果有疑慮的話,也可以使用括號()來自行決定先後順序。
六、 國際化訊息
JSF的國際化(Internnationalization)訊息處理是基於Java對國際化的支援,您可以在一個訊息資源檔中統一管理訊息資源,資源檔的名稱是.properties,而內容是名稱與值的配對,例如:
titleText=JSF Demo
hintText=Please input your name and password
nameText=name
passText=password
commandText=Submit
資源檔名稱由basename加上語言與地區來組成,例如:
* basename.properties
* basename_en.properties
* basename_zh_TW.properties
沒有指定語言與地區的basename是預設的資源檔名稱,JSF會根據瀏覽器送來的Accept-Language header中的內容來決定該使用哪一個資源檔名稱,例如:
Accept-Language: zh_TW, en-US, en
如果瀏覽器送來這些header,則預設會使用繁體中文,接著是美式英文,再來是英文語系,如果找不到對應的訊息資源檔,則會使用預設的訊息資源檔。
由於訊息資源檔必須是ISO-8859-1編碼,所以對於非西方語系的處理,必須先將之轉換為Java Unicode Escape格式,例如您可以先在訊息資源檔中寫下以下的內容:
titleText=JSF示範
hintText=請輸入名稱與密碼
nameText=名稱
passText=密碼
commandText=送出
然後使用JDK的工具程式native2ascii來轉換,例如:
native2ascii -encoding Big5 messages_zh_TW.txt messages_zh_TW.properties
轉換後的內容會如下:
titleText=JSF/u793a/u7bc4
hintText=/u8acb/u8f38/u5165/u540d/u7a31/u8207/u5bc6/u78bc
nameText=/u540d/u7a31
passText=/u5bc6/u78bc
commandText=/u9001/u51fa
接下來您可以使用
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@page contentType="text/html;charset=UTF8"%>
"messages" var="msgs"/>
"#{msgs.titleText}"/>
"#{msgs.hintText}"/>
"#{msgs.nameText}"/>:
"#{user.name}"/>
"#{msgs.passText}"/>:
"#{user.password}"/>
"#{msgs.commandText}"
actionListener="#{user.verify}"
action="#{user.outcome}"/>
如此一來,如果您的瀏覽器預設接受zh_TW語系的話,則頁面上就可以顯示中文,否則預設將以英文顯示,也就是messages.properties的內容,為了能顯示多國語系,我們設定網頁編碼為UTF8。
"zh_TW">
"messages" var="msgs"/>
直接指定以上的話,則會使用繁體中文來顯示,JSF會根據
"en">
"messages" var="msgs"/>
您也可以在faces-config.xml中設定語系,例如:
<default-locale>endefault-locale>
zh_TW
.....
在
當然,如果您可以提供一個選項讓使用者選擇自己的語系會是更好的方式,例如根據user這個Bean的locale屬性來決定頁面語系:
"#{user.locale}">
"messages" var="msgs"/>
在頁面中設定一個表單,可以讓使用者選擇語系,例如設定單選鈕:
"#{user.locale}">
"zh_TW"
itemLabel="#{msgs.zh_TWText}"/>
"en"
itemLabel="#{msgs.enText}"/>
JSF使用 JavaBean 來達到程式邏輯與視圖分離的目的,在JSF中的Bean其角色是屬於Backing Bean,又稱之為Glue Bean,其作用是在真正的業務邏輯Bean及UI元件之間搭起橋樑,在Backing Bean中會呼叫業務邏輯Bean處理使用者的請求,或者是將業務處理結果放置其中,等待UI元件取出當中的值並顯示結果給使用者。
JSF將Bean的管理集中在faces-config.xml中,一個例子如下:
....
user
onlyfun.caterpillar.UserBean
session
....
這個例子我們在 第一個JSF程式 看過,
"#{user.name}"/>
您還可以將存活範圍設定為none,當設定為none時會在需要的時候生成一個新的Bean,例如您在一個method中想要生成一個臨時的Bean,就可以將之設定為none。
在JSF頁面上要取得Bean的屬性,是使用 JSF表示語言 (Expression Language),要注意到的是,JSF表示語言是寫成 #{expression},而 JSP表示語言 是寫成 ${expression},因為表示層可能是使用JSP,所以必須特別區分,另外要注意的是,JSF的標籤上之屬性設定時,只接受JSF表示語言。
八、 Beans 的組態與設定JSF預設會讀取faces-config.xml中關於Bean的定義,如果想要自行設置定義檔的名稱,我們是在web.xml中提供javax.faces.CONFIG_FILES參數,例如:
javax.faces.CONFIG_FILES
/WEB-INF/beans.xml
...
定義檔可以有多個,中間以 "," 區隔,例如:
/WEB-INF/navigation.xml,/WEB-INF/beans.xml
一個Bean最基本要定義Bean的名稱、類別與存活範圍,例如:
....
user
onlyfun.caterpillar.UserBean
session
....
如果要在其它類別中取得Bean物件,則可以先取得javax.faces.context.FacesContext,它代表了JSF目前的 執行環境物件,接著嘗試取得javax.faces.el.ValueBinding物件,從中取得指定的Bean物件,例如:
FacesContext context = FacesContext.getCurrentInstance();
ValueBinding binding =
context.getApplication().createValueBinding("#{user}");
UserBean user = (UserBean) binding.getValue(context);
如果只是要嘗試取得Bean的某個屬性,則可以如下:
FacesContext context = FacesContext.getCurrentInstance();
ValueBinding binding =
context.getApplication().createValueBinding(
"#{user.name}");
String name = (String) binding.getValue(context);
如果有必要在啟始Bean時,自動設置屬性的初始值,則可以如下設定:
....
user
onlyfun.caterpillar.UserBean
session
name
caterpillar
password
123456
....
如果要設定屬性為 null 值,則可以使用
....
name
<null-value/>
password
<null-value/>
....
當然,您的屬性不一定是字串值,也許會是int、float、boolean等等型態,您可以設定
型態 | 轉換 |
---|---|
short、int、long、float、double、byte,或相應的Wrapper類別 | 嘗試使用Wrapper的valueOf()進行轉換,如果沒有設置,則設為0 |
boolean 或 Boolean | 嘗試使用Boolean.valueOf()進行轉換,如果沒有設置,則設為false |
char 或 Character | 取設置的第一個字元,如果沒有設置,則設為0 |
String 或 Object | 即設定的字串值,如果沒有設定,則為空字串new String("") |
您也可以將其它產生的Bean設定給另一個Bean的屬性,例如:
....
user
onlyfun.caterpillar.UserBean
session
other
onlyfun.caterpillar.OtherBean
session
user
#{user}
....
在上面的設定中,在OtherBean中的user屬性,接受一個UserBean型態的物件,我們設定為前一個名稱為user的UserBean物件。
九、 Beans 上的 List, Map....
someBean
onlyfun.caterpillar.SomeBean
session
someProperty
java.lang.Integer
1
2
3
....
這是一個設定接受List型態的屬性,我們使用
設定Map的話,則是使用
....
someBean
onlyfun.caterpillar.SomeBean
session
someProperty
java.lang.Integer
someKey1
100
someKey2
200
....
由於Map物件是以key-value對的方式來存入,所以我們在每一個
您也可以直接像設定Bean一樣,設定一個List或Map物件,例如在JSF附的範例中,有這樣的設定:
....
Special expense item types
specialTypes
java.util.TreeMap
application
java.lang.Integer
Presentation Material
100
Software
101
Balloons
102
....
而範例中另一個設定List的例子如下:
....
statusStrings
java.util.ArrayList
request
<null-value/>
Open
Submitted
Accepted
Rejected
....
十、 標準轉換器
Web應用程式與瀏覽器之間是使用HTTP進行溝通,所有傳送的資料基本上都是字串文字,而Java應用程式本身基本上則是物件,所以物件資料必須經由轉換傳送給瀏覽器,而瀏覽器送來的資料也必須轉換為物件才能使用。
JSF定義了一系列標準的轉換器(Converter),對於基本資料型態(primitive type)或是其Wrapper類別,JSF會使用javax.faces.Boolean、javax.faces.Byte、 javax.faces.Character、javax.faces.Double、javax.faces.Float、 javax.faces.Integer、javax.faces.Long、javax.faces.Short等自動進行轉換,對於 BigDecimal、BigInteger,則會使用javax.faces.BigDecimal、javax.faces.BigInteger自 動進行轉換。
至於DateTime、Number,我們可以使用
來看個簡單的例子,首先我們定義一個簡單的Bean:
package onlyfun.caterpillar;
import java.util.Date;
public class UserBean {
private Date date = new Date();
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
}
這個Bean的屬性接受Date型態的參數,按理來說,接收到HTTP傳來的資料中若有相關的日期資訊,我們必須剖析這個資訊,再轉換為Date物件,然而我們可以使用JSF的標準轉換器來協助這項工作,例如:
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@page contentType="text/html;charset=Big5"%>
轉換器示範
設定的日期是:
"#{user.date}">
"dd/MM/yyyy"/>
"dateField" value="#{user.date}">
"dd/MM/yyyy"/>
for="dateField" style="color:red"/>
"送出" action="show"/>
在
假設faces-config.xml是這樣定義的:
"1.0"?>
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"
"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
/*
case>
show
/pages/index.jsp
case>
user
onlyfun.caterpillar.UserBean
session
首次連上頁面時顯示的畫面如下:
如您所看到的,轉換器自動依pattern設定的樣式將Date物件格式化了,當您依格式輸入資料並送出後,轉換器也會自動將您輸入的資料轉換為Date物件,如果轉換時發生錯誤,則會出現以下的訊息:
您還可以參考 Using the Standard Converters 這篇文章中有關於標準轉換器的說明。
十一、 自訂轉換器public Object getAsObject(FacesContext context,
UIComponent component,
String str);
public String getAsString(FacesContext context,
UIComponent component,
Object obj);
簡單的說,第一個方法會接收從客戶端經由HTTP傳來的字串資料,您在第一個方法中將之轉換為您的自訂物件,這個自訂物件將會自動設定給您指定的Bean物件;第二個方法就是將從您的Bean物件得到的物件轉換為字串,如此才能藉由HTTP傳回給客戶端。
直接以一個簡單的例子來作說明,假設您有一個User類別:
package onlyfun.caterpillar;
public class User {
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
這個User類別是我們轉換器的目標物件,而您有一個GuestBean類別:
package onlyfun.caterpillar;
public class GuestBean {
private User user;
public void setUser(User user) {
this.user = user;
}
public User getUser() {
return user;
}
}
這個Bean上的屬性直接傳回或接受User型態的參數,我們來實作一個簡單的轉換器,為HTTP字串與User物件進行轉換:
package onlyfun.caterpillar;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
public class UserConverter implements Converter {
public Object getAsObject(FacesContext context,
UIComponent component,
String str)
throws ConverterException {
String[] strs = str.split(",");
User user = new User();
try {
user.setFirstName(strs[0]);
user.setLastName(strs[1]);
}
catch(Exception e) {
// 轉換錯誤,簡單的丟出例外
throw new ConverterException();
}
return user;
}
public String getAsString(FacesContext context,
UIComponent component,
Object obj)
throws ConverterException {
String firstName = ((User) obj).getFirstName();
String lastName = ((User) obj).getLastName();
return firstName + "," + lastName;
}
}
實作完成這個轉換器,我們要告訴JSF這件事,這是在faces-config.xml中完成註冊:
"1.0"?>
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"
"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
/*
case>
show
/pages/index.jsp
case>
onlyfun.caterpillar.User
onlyfun.caterpillar.UserConverter
guest
onlyfun.caterpillar.GuestBean
session
註冊轉換器時,需提供轉換器識別(Converter ID)與轉換器類別,接下來要在JSF頁面中使用轉換器的話,就是指定所要使用的轉換器識別,例如:
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@page contentType="text/html;charset=Big5"%>
自訂轉換器
Guest名稱是:
"#{guest.user}"
converter="onlyfun.caterpillar.User"/>
"userField"
value="#{guest.user}"
converter="onlyfun.caterpillar.User"/>
for="userField" style="color:red"/>
"送出" action="show"/>
您也可以
"userField" value="#{guest.user}">
"onlyfun.caterpillar.User"/>
除了向JSF註冊轉換器之外,還有一個方式可以不用註冊,就是直接在Bean上提供一個取得轉換器的方法,例如:
package onlyfun.caterpillar;
import javax.faces.convert.Converter;
public class GuestBean {
private User user;
private Converter converter = new UserConverter();
public void setUser(User user) {
this.user = user;
}
public User getUser() {
return user;
}
public Converter getConverter() {
return converter;
}
}
之後可以直接結合 JSF Expression Language 來指定轉換器:
"userField"
value="#{guest.user}"
converter="#{guest.converter}"/>
十二、 標準驗證器
當應用程式要求使用者輸入資料時,必然考慮到使用者輸入資料之正確性,對於使用者的輸入必須進行檢驗,檢驗必要的兩種驗證是語法檢驗(Synatic Validation)與語意檢驗(Semantic Validation)。
語法檢驗是要檢查使用者輸入的資料是否合乎我們所要求的格式,最基本的就是檢查使用者是否填入了欄位值,或是欄位值的長度、大小值等等是否符合 要求。語意檢驗是在語法檢驗之後,在格式符合需求之後,我們進一步驗證使用者輸入的資料語意上是否正確,例如檢查使用者的名稱與密碼是否匹配。
在 簡單的導航 (Navigation) 中,我們對使用者名稱與密碼檢查是否匹配,這是語意檢驗,我們可以使用JSF所提供的標準驗證器,為其加入語法檢驗,例如:
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@page contentType="text/html;charset=Big5"%>
驗證器示範
"table" style="color:red"/>
請輸入您的名稱
"#{user.errMessage}"/>
名稱: "#{user.name}"
required="true"/>
密碼: "#{user.password}"
required="true">
"6"/>
"送出"
action="#{user.verify}"/>
在
這一次在錯誤訊息的顯示上,我們使用
下面是一個驗證錯誤的訊息顯示:
JSF提供了三種標準驗證器:
十三、 自訂驗證器
您可以自訂自己的驗證器,所需要的是實作javax.faces.validator.Validator介面,例如我們實作一個簡單的密碼驗證器,檢查字元長度,以及密碼中是否包括字元與數字:
package onlyfun.caterpillar;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;
public class PasswordValidator implements Validator {
public void validate(FacesContext context,
UIComponent component,
Object obj)
throws ValidatorException {
String password = (String) obj;
if(password.length() < 6) {
FacesMessage message = new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"字元長度小於6",
"字元長度不得小於6");
throw new ValidatorException(message);
}
if(!password.matches(".+[0-9]+")) {
FacesMessage message = new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"密碼必須包括字元與數字",
"密碼必須是字元加數字所組成");
throw new ValidatorException(message);
}
}
}
您要實作javax.faces.validator.Validator介面中的validate()方法,如果驗證錯誤,則丟出一個 ValidatorException,它接受一個FacesMessage物件,這個物件接受三個參數,分別表示訊息的嚴重程度(INFO、 WARN、ERROR、FATAL)、訊息概述與詳細訊息內容,這些訊息將可以使用
接下來要在faces-config.xml中註冊驗證器的識別(Validater ID),要加入以下的內容:
"1.0"?>
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"
"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
....
onlyfun.caterpillar.Password
onlyfun.caterpillar.PasswordValidator
....
要使用自訂的驗證器,我們可以使用
....
"#{user.password}" required="true">
"onlyfun.caterpillar.Password"/>
....
您也可以讓Bean自行負責驗證的工作,可以在Bean上提供一個驗證方法,這個方法沒有傳回值,並可以接收FacesContext、UIComponent、Object三個參數,例如:
package onlyfun.caterpillar;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.ValidatorException;
public class UserBean {
....
public void validate(FacesContext context,
UIComponent component,
Object obj)
throws ValidatorException {
String password = (String) obj;
if(password.length() < 6) {
FacesMessage message = new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"字元長度小於6",
"字元長度不得小於6");
throw new ValidatorException(message);
}
if(!password.matches(".+[0-9]+")) {
FacesMessage message = new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"密碼必須包括字元與數字",
"密碼必須是字元加數字所組成");
throw new ValidatorException(message);
}
}
}
接著可以在頁面下如下使用驗證器:
.....
"#{user.password}"
required="true"
validator="#{user.validate}"/>
....
在使用標準轉換器或驗證器時,當發生錯誤時,會有一些預設的錯誤訊息顯示,這些訊息可以使用
javax.faces.component.UIInput.CONVERSION=Format Error.
javax.faces.component.UIInput.REQUIRED=Please input your data.
....
javax.faces.component.UIInput.CONVERSION是用來設定當轉換器發現錯誤時顯示的訊息,而 javax.faces.component.UIInput.REQUIRED是在標籤設定了required為true,而使用者沒有在欄位輸入時顯 示的錯誤訊息。
您要在faces-config.xml中告訴JSF您使用的訊息檔案名稱,例如:
"1.0"?>
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"
"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
<default-locale>endefault-locale>
zh_TW
messages
.....
在這邊我們設定了訊息檔案的名稱為messages_xx_YY.properties,其中xx_YY是根據您的Locale來決定,轉換器或驗證器的錯誤訊息如果有設定的話,就使用設定值,如果沒有設定的話,就使用預設值。
驗證器錯誤訊息,除了上面的javax.faces.component.UIInput.REQUIRED之外,還有以下的幾個:
訊息識別 | 預設訊息 | 用於 |
---|---|---|
javax.faces.validator.NOT_IN_RANGE | Validation Error: Specified attribute is not between the expected values of {0} and {1}. | DoubleRangeValidator與LongRangeValidator,{0}與{1}分別代表minimum與maximum所設定的屬性 |
javax.faces.validator.DoubleRangeValidator.MAXIMUM、javax.faces.validator.LongRangeValidator.MAXIMUM | Validation Error: Value is greater than allowable maximum of '{0}'. | DoubleRangeValidator或LongRangeValidator,{0}表示maximum屬性 |
javax.faces.validator.DoubleRangeValidator.MINIMUM、javax.faces.validator.LongRangeValidator.MINIMUM | Validation Error: Value is less than allowable minimum of '{0}'. | DoubleRangeValidator或LongRangeValidator,{0}代表minimum屬性 |
javax.faces.validator.DoubleRangeValidator.TYPE、javax.faces.validator.LongRangeValidator.TYPE | Validation Error: Value is not of the correct type. | DoubleRangeValidator或LongRangeValidator |
javax.faces.validator.LengthValidator.MAXIMUM | Validation Error: Value is greater than allowable maximum of ''{0}''. | LengthValidator,{0}代表maximum |
javax.faces.validator.LengthValidator.MINIMUM | Validation Error: Value is less than allowable minimum of ''{0}''. | LengthValidator,{0}代表minimum屬性 |
在您提供自訂訊息的時候,也可以提供{0}或{1}來設定顯示相對的屬性值,以提供詳細正確的錯誤提示訊息。
訊息的顯示有概述訊息與詳述訊息,如果是詳述訊息,則在識別上加上 "_detail",例如:
javax.faces.component.UIInput.CONVERSION=Error.
javax.faces.component.UIInput.CONVERSION_detail= Detail Error.
....
除了在訊息資源檔中提供訊息,您也可以在程式中使用FacesMessage來提供訊息,例如在 自訂驗證器 中我們就這麼用過:
....
if(password.length() < 6) {
FacesMessage message = new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"字元長度小於6",
"字元長度不得小於6");
throw new ValidatorException(message);
}
....
最好的方法是在訊息資源檔中提供訊息,這麼一來如果我們要修改訊息,就只要修改訊息資源檔的內容,而不用修改程式,來看一個簡單的例子,假設我們的訊息資源檔中有以下的內容:
onlyfun.caterpillar.message1=This is message1.
onlyfun.caterpillar.message2=This is message2 with /{0} and /{1}.
則我們可以在程式中取得訊息資源檔的內容,例如:
package onlyfun.caterpillar;
import java.util.Locale;
import java.util.ResourceBundle;
import javax.faces.context.FacesContext;
improt javax.faces.component.UIComponent;
import javax.faces.application.Application;
import javax.faces.application.FacesMessage;
....
public void xxxMethod(FacesContext context,
UIComponent component,
Object obj) {
// 取得應用程式代表物件
Application application = context.getApplication();
// 取得訊息檔案主名稱
String messageFileName =
application.getMessageBundle();
// 取得當前 Locale 物件
Locale locale = context.getViewRoot().getLocale();
// 取得訊息綁定 ResourceBundle 物件
ResourceBundle rsBundle =
ResourceBundle.getBundle(messageFileName, locale);
String message = rsBundle.getString(
"onlyfun.caterpillar.message1");
FacesMessage facesMessage = new FacesMessage(
FacesMessage.SEVERITY_FATAL, message, message);
....
}
....
....
接下來您可以將FacesMessage物件填入ValidatorException或ConverterException後再丟出, FacesMessage建構時所使用的三個參數是嚴重程度、概述訊息與詳述訊息,嚴重程度有SEVERITY_FATAL、 SEVERITY_ERROR、SEVERITY_WARN與SEVERITY_INFO四種。
如果需要在訊息資源檔中設定{0}、{1}等參數,則可以如下:
....
String message = rsBundle.getString(
"onlyfun.caterpillar.message2");
Object[] params = {"param1", "param2"};
message = java.text.MessageFormat.format(message, params);
FacesMessage facesMessage = new FacesMessage(
FacesMessage.SEVERITY_FATAL, message, message);
....
如此一來,在顯示訊息時,onlyfun.caterpillar.message2的{0}與{1}的位置就會被"param1"與"param2"所取代。
十五、 自訂轉換, 驗證標籤 在 自訂驗證器 中,我們的驗證器只能驗證一種pattern(.+[0-9]+),我們希望可以在JSF頁面上自訂匹配的pattern,然而由於我們使用
....
"#{user.password}" required="true">
"onlyfun.caterpillar.Password"/>
"pattern" value=".+[0-9]+"/>
....
使用
....
public void validate(FacesContext context,
UIComponent component,
Object obj)
throws ValidatorException {
....
String pattern = (String)
component.getAttributes().get("pattern");
....
}
....
您也可以開發自己的一組驗證標籤,並提供相關屬性設定,這需要瞭解JSP Tag Library的撰寫,所以請您先參考 JSP/Servlet 中有關於JSP Tag Library的介紹。
要開發驗證器轉用標籤,您可以直接繼承javax.faces.webapp.ValidatorTag,這個類別可以幫您處理大部份的細節,您所需要的,就是重新定義它的createValidator()方法,我們以改寫自訂驗證器 中的PasswordValidator為例:
package onlyfun.caterpillar;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;
public class PasswordValidator implements Validator {
private String pattern;
public void setPattern(String pattern) {
this.pattern = pattern;
}
public void validate(FacesContext context,
UIComponent component,
Object obj)
throws ValidatorException {
String password = (String) obj;
if(password.length() < 6) {
FacesMessage message = new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"字元長度小於6", "字元長度不得小於6");
throw new ValidatorException(message);
}
if(pattern != null && !password.matches(pattern)) {
FacesMessage message = new FacesMessage(
FacesMessage.SEVERITY_ERROR,
"密碼必須包括字元與數字",
"密碼必須是字元加數字所組成");
throw new ValidatorException(message);
}
}
}
主要的差別是我們提供了pattern屬性,在validate()方法中進行驗證時,是根據我們所設定的pattern屬性,接著我們繼承javax.faces.webapp.ValidatorTag來撰寫自己的驗證標籤:
package onlyfun.caterpillar;
import javax.faces.application.Application;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.webapp.ValidatorTag;
public class PasswordValidatorTag extends ValidatorTag {
private String pattern;
public void setPattern(String pattern) {
this.pattern = pattern;
}
protected Validator createValidator() {
Application application =
FacesContext.getCurrentInstance().
getApplication();
PasswordValidator validator =
(PasswordValidator) application.createValidator(
"onlyfun.caterpillar.Password");
validator.setPattern(pattern);
return validator;
}
}
application.createValidator()方法建立驗證器物件時,是根據在faces-config.xml中註冊驗證器的識別(Validater ID):
"1.0"?>
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"
"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
....
onlyfun.caterpillar.Password
onlyfun.caterpillar.PasswordValidator
....
剩下來的工作,就是佈署tld描述檔了,我們簡單的定義一下:
"1.0" encoding="UTF-8" ?>
"http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
web-jsptaglibrary_2_0.xsd"
version="2.0">
PasswordValidator Tag
1.0
2.0
<short-name>coshort-name>
http://caterpillar.onlyfun.net
PasswordValidator
passwordValidator
onlyfun.caterpillar.PasswordValidatorTag
empty
pattern
true
false
而我們的index.jsp改寫如下:
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="/WEB-INF/taglib.tld" prefix="co" %>
<%@page contentType="text/html;charset=Big5"%>
驗證器示範
"table" style="color:red"/>
請輸入您的名稱
"#{user.errMessage}"/>
名稱: "#{user.name}"
required="true"/>
密碼: "#{user.password}"
required="true">
".+[0-9]+"/>
"送出"
action="#{user.verify}"/>
主要的差別是,我們使用了自己的驗證器標籤:
".+[0-9]+"/>
如果要自訂轉換器標籤,方法也是類似,您要作的是繼承javax.faces.webapp.ConverterTag,並重新定義其createConverter()方法。
十六、 動作事件JSF支援事件處理模型,雖然由於HTTP本身無狀態(stateless)的特性,使得這個模型多少有些地方仍不太相同,但JSF所提供的事件處理模型已足以讓一些傳統GUI程式的設計人員,可以用類似的模型來開發程式。
在 簡單的導航 中,我們根據動作方法(action method)的結果來決定要導向的網頁,一個按鈕繫結至一個方法,這樣的作法實際上即使JSF所提供的簡化的事件處理程序,在按鈕上使用action繫 結至一個動作方法(action method),實際上JSF會為其自動產生一個「預設的ActionListener」來處理事件,並根據其傳回值來決定導向的頁面。
如果您需要使用同一個方法來應付多種事件來源,並想要取得事件來源的相關訊息,您可以讓處理事件的方法接收一個javax.faces.event.ActionEvent事件參數,例如:
package onlyfun.caterpillar;
import javax.faces.event.ActionEvent;
public class UserBean {
private String name;
private String password;
private String errMessage;
private String outcome;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
public void setErrMessage(String errMessage) {
this.errMessage = errMessage;
}
public String getErrMessage() {
return errMessage;
}
public void verify(ActionEvent e) {
if(!name.equals("justin") ||
!password.equals("123456")) {
errMessage = "名稱或密碼錯誤" + e.getSource();
outcome = "failure";
}
else {
outcome = "success";
}
}
public String outcome() {
return outcome;
}
}
在上例中,我們讓verify方法接收一個ActionEvent物件,當使用者按下按鈕,會自動產生ActionEvent物件代表事件來源,我們故意在錯誤訊息之後如上事件來源的字串描述,這樣就可以在顯示錯誤訊息時一併顯示事件來源描述。
為了提供ActionEvent的存取能力,您的index.jsp可以改寫如下:
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@page contentType="text/html;charset=Big5"%>
第一個JSF程式
請輸入您的名稱
"#{user.errMessage}"/>
名稱: "#{user.name}"/>
密碼: "#{user.password}"/>
"送出"
actionListener="#{user.verify}"
action="#{user.outcome}"/>
主要改變的是按鈕上使用了actionListener屬性,這種方法可以使用一個ActionListener,JSF會先檢查是否有指定的 actionListener,然後再檢查是否指定了動作方法並產生預設的ActionListener,並根據其傳回值導航頁面。
如果您要註冊多個ActionListener,例如當使用者按下按鈕時,順便在記錄檔中增加一些記錄訊息,您可以實作javax.faces.event.ActionListener,例如:
package onlyfun.caterpillar;
import javax.faces.event.ActionListener;
....
public class LogHandler implements ActionListener {
public void processAction(ActionEvent e) {
// 處理Log
}
}
package onlyfun.caterpillar;
import javax.faces.event.ActionListener;
....
public class VerifyHandler implements ActionListener {
public void processAction(ActionEvent e) {
// 處理驗證
}
}
這麼一來,您就可以使用
"送出" action="#{user.outcome}">
"onlyfun.caterpillar.LogHandler"/>
"onlyfun.caterpillar.VerifyHandler"/>
十七、 即時事件
所謂的即時事件(Immediate Events),是指JSF視圖元件在取得請求中該取得的值之後,即立即處理指定的事件,而不再進行後續的轉換器處理、驗證器處理、更新模型值等流程。
在JSF的事件模型中會有所謂即時事件,導因於Web應用程式的先天特性不同於GUI程式,所以JSF的事件模式與GUI程式的事件模式仍有相當程度的不同,一個最基本的問題正因為HTTP無狀態的特性,使得Web應用程式天生就無法直接喚起伺服端的特定物件。
所有的物件喚起都是在伺服端執行的,至於該喚起什麼物件,則是依一個基本的流程:
依客戶端傳來的session資料或伺服端上的session資料,回復JSF畫面元件。
JSF畫面元件各自獲得請求中的值屬於自己的值,包括舊的值與新的值。
轉換為物件並進行驗證。
更新Bean或相關的模型值。
執行應用程式相關邏輯。
對先前的請求處理完之後,產生畫面以回應客戶端執行結果。
對於動作事件(Action Event)來說,元件的動作事件是在套用請求值階段就生成ActionEvent物件了,但相關的事件處理並不是馬上進行,ActionEvent會先被排入佇列,然後必須再通過驗證、更新模式值階段,之後才處理佇列中的事件。
這樣的流程對於按下按鈕然後執行後端的應用程式來說不成問題,但有些事件並不需要這樣的流程,例如只影響畫面的事件。
舉個例子來說,在表單中可能有使用者名稱、密碼等欄位,並提供有一個地區選項按鈕,使用者可以在不填下按鈕的情況下,就按下地區選項按鈕,如果 依照正常的流程,則會進行驗證、更新模型值、喚起應用程式等流程,但顯然的,使用者名稱與密碼是空白的,這會引起不必要的錯誤。
您可以設定元件的事件在套用請求值之後立即被處理,並跳過後續的階段,直接進行畫面繪製以回應請求,對於JSF的input與command元件,都有一個immediate屬性可以設定,只要將其設定為true,則指定的事件就成為立即事件。
一個例子如下:
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@page contentType="text/html;charset=UTF8"%>
"#{user.locale}">
"messages" var="msgs"/>
"#{msgs.titleText}"/>
"#{msgs.hintText}"/>
"#{msgs.nameText}"/>:
"#{user.name}"/>
"#{msgs.passText}"/>:
"#{user.password}"/>
"#{msgs.commandText}"
action="#{user.verify}"/>
"#{msgs.Text}"
immediate="true"
actionListener="#{user.changeLocale}"/>
這是一個可以讓使用者決定使用語系的示範,最後一個commandButton元件被設定了immediate屬性,當按下這個按鈕後,JSF 套用請求值之後會立即處理指定的actionListener,而不再進行驗證、更新模型值,簡單的說,就這個程式來說,您在輸入欄位與密碼欄位中填入的 值,不會影響您的user.name與user.password。
基於範例的完整起見,我們列出這個程式Bean物件及faces-config.xml:
package onlyfun.caterpillar;
import javax.faces.event.ActionEvent;
public class UserBean {
private String locale = "en";
private String name;
private String password;
private String errMessage;
public void changeLocale(ActionEvent e) {
if(locale.equals("en"))
locale = "zh_TW";
else
locale = "en";
}
public String getLocale() {
if (locale == null) {
locale = "en";
}
return locale;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
public void setErrMessage(String errMessage) {
this.errMessage = errMessage;
}
public String getErrMessage() {
return errMessage;
}
public String verify() {
if(!name.equals("justin") ||
!password.equals("123456")) {
errMessage = "名稱或密碼錯誤";
return "failure";
}
else {
return "success";
}
}
}
"1.0"?>
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"
"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
/pages/index.jsp
case>
success
/pages/welcome.jsp
case>
case>
failure
/pages/index.jsp
case>
user
onlyfun.caterpillar.UserBean
session
訊息資源檔的內容則是如下:
titleText=JSF Demo
hintText=Please input your name and password
nameText=name
passText=password
commandText=Submit
Text=/u4e2d/u6587
Text中設定的是「中文」轉換為Java Unicode Escape格式的結果,另一個訊息資源檔的內容則是英文訊息的翻譯而已,其轉換為Java Unicode Escape格式結果如下:
titleText=JSF/u793a/u7bc4
hintText=/u8acb/u8f38/u5165/u540d/u7a31/u8207/u5bc6/u78bc
nameText=/u540d/u7a31
passText=/u5bc6/u78bc
commandText=/u9001/u51fa
Text=English
welcome.jsp就請自行設計了,程式的畫面如下:
如果使用者改變了JSF輸入元件的值後送出表單,就會發生值變事件(Value Change Event),這會丟出一個javax.faces.event.ValueChangeEvent物件,如果您想要處理這個事件,有兩種方式,一是直接 設定JSF輸入元件的valueChangeListener屬性,例如:
"#{user.locale}"
οnchange="this.form.submit();"
valueChangeListener="#{user.changeLocale}">
"zh_TW" itemLabel="Chinese"/>
"en" itemLabel="English"/>
為了模擬GUI中選擇了選單項目之後就立即發生反應,我們在onchange屬性中使用了JavaScript,其作用是在選項項目發生改變之 後,立即送出表單,而不用按下提交按鈕;而valueChangeListener屬性所綁定的user.changeLocale方法必須接受 ValueChangeEvent物件,例如:
package onlyfun.caterpillar;
import javax.faces.event.ValueChangeEvent;
public class UserBean {
private String locale = "en";
private String name;
private String password;
private String errMessage;
public void changeLocale(ValueChangeEvent event) {
if(locale.equals("en"))
locale = "zh_TW";
else
locale = "en";
}
public void setLocale(String locale) {
this.locale = locale;
}
public String getLocale() {
if (locale == null) {
locale = "en";
}
return locale;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
public void setErrMessage(String errMessage) {
this.errMessage = errMessage;
}
public String getErrMessage() {
return errMessage;
}
public String verify() {
if(!name.equals("justin") ||
!password.equals("123456")) {
errMessage = "名稱或密碼錯誤";
return "failure";
}
else {
return "success";
}
}
}
另一個方法是實作javax.faces.event.ValueChangeListener介面,並定義其processValueChange()方法,例如:
package onlyfun.caterpillar;
....
public class SomeListener implements ValueChangeListener {
public void processValueChange(ValueChangeEvent event) {
....
}
....
}
然後在JSF頁面上使用標籤,並設定其type屬性,例如:
{code:borderStyle=solid}
"#{user.locale}"
οnchange="this.form.submit();">
"onlyfun.caterpillar.SomeListener"/>
"zh_TW" itemLabel="Chinese"/>
"en" itemLabel="English"/>
下面這個頁面是對 立即事件 中的範例程式作一個修改,將語言選項改以下拉式選單的選擇方式呈現,這必須配合上面提供的UserBean類別來使用:
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@page contentType="text/html;charset=UTF8"%>
"#{user.locale}">
"messages" var="msgs"/>
"#{msgs.titleText}"/>
"#{user.locale}"
immediate="true"
οnchange="this.form.submit();"
valueChangeListener="#{user.changeLocale}">
"zh_TW"
itemLabel="Chinese"/>
"en"
itemLabel="English"/>
"#{msgs.hintText}"/>
"#{msgs.nameText}"/>:
"#{user.name}"/>
"#{msgs.passText}"/>:
"#{user.password}"/>
"#{msgs.commandText}"
action="#{user.verify}"/>
十九、 Phase 事件
在 即時事件 中我們提到,JSF的請求執行到回應,完整的過程會經過六個階段:
依客戶端傳來的session資料或伺服端上的session資料,回復JSF畫面元件。
JSF畫面元件各自獲得請求中的值屬於自己的值,包括舊的值與新的值。
轉換為物件並進行驗證。
更新Bean或相關的模型值。
執行應用程式相關邏輯。
對先前的請求處理完之後,產生畫面以回應客戶端執行結果。
在每個階段的前後會引發javax.faces.event.PhaseEvent,如果您想嘗試在每個階段的前後捕捉這個事件,以進行一些處 理,則可以實作javax.faces.event.PhaseListener,並向javax.faces.lifecycle.Lifecycle 登記這個Listener,以有適當的時候通知事件的發生。
PhaseListener有三個必須實作的方法getPhaseId()、beforePhase()與afterPhase(),其中getPhaseId()傳回一個PhaseId物件,代表Listener想要被通知的時機,可以設定的時機有:
其中PhaseId.ANY_PHASE指的是任何的階段轉換時,就進行通知;您可以在beforePhase()與afterPhase()中撰寫階段前後撰寫分別想要處理的動作,例如下面這個簡單的類別會列出每個階段的名稱:
package onlyfun.caterpillar;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
public class ShowPhaseListener implements PhaseListener {
public void beforePhase(PhaseEvent event) {
String phaseName = event.getPhaseId().toString();
System.out.println("Before " + phaseName);
}
public void afterPhase(PhaseEvent event) {
String phaseName = event.getPhaseId().toString();
System.out.println("After " + phaseName);
}
public PhaseId getPhaseId() {
return PhaseId.ANY_PHASE;
}
}
撰寫好PhaseListener後,我們可以在faces-config.xml中向Lifecycle進行註冊:
"1.0"?>
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"
"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
onlyfun.caterpillar.ShowPhaseListener
......
您可以使用這個簡單的類別,看看在請求任一個JSF畫面時所顯示的內容,藉此瞭解JSF每個階段的流程變化。
二十、 簡介 JSF 標準標籤其名稱以output作為開頭,作用為輸出指定的訊息或綁定值。
其名稱以input作為開頭,其作用為提供使用者輸入欄位。
其名稱以command作為開頭,其作用為提供命令或連結按鈕。
其名稱以select作為開頭,其作用為提供使用者選項的選取。
包括了form、message、messages、graphicImage等等未分類的標籤。
JSF標準HTML標籤包括了幾個共通的屬性,整理如下:
屬性名稱 | 適用 | 說明 |
---|---|---|
id | 所有元件 | 可指定id名稱,以讓其它標籤或元件參考 |
binding | 所有元件 | 綁定至UIComponent |
rendered | 所有元件 | 是否顯示元件 |
styleClass | 所有元件 | 設定Cascading stylesheet (CSS) |
value | 輸入、輸出、命令元件 | 設定值或綁定至指定的值 |
valueChangeListener | 輸入元件 | 設定值變事件處理者 |
converter | 輸入、輸出元件 | 設定轉換器 |
validator | 輸入元件 | 設定驗證器 |
required | 輸入元件 | 是否驗證必填欄位 |
immediate | 輸入、命令元件 | 是否為立即事件 |
除了共通的屬性之外,您還可以在某些元件上設定標籤HTML 4.01的屬性,像是size、alt、width等屬性,或者是設定DHTML事件屬性,例如onchange、onclick等等。
除了JSF的標準HTML標籤之外,您還需要一些標準核心標籤,這些標籤是獨立於Renderer Kit的,JSF並不限制在HTML輸出表示層,核心標籤可以搭配其它的Renderer Kit來使用。
詳細的HTML標籤或核心標籤的使用與屬性說明可以查詢 Tag Library Documentation 文件
二十一、 輸出類標籤
輸出類的標籤包括了outputLabel、outputLink、outputFormat與 outputText,分別舉例說明如下:
產生
"user" value="#{user.name}"/>
for="user" value="#{user.name}"/>
這會產生像是以下的標籤:
"user" type="text" name="user" value="guest" />
產生 HTML標籤,例如:
"../index.jsp">
"Link to Index"/>
"name" value="MyName"/>
你可搭配
value所指定的內容也可以是JSF EL綁定。
產生指定的文字訊息,可以搭配
"messages" var="msgs"/>
"#{msgs.welcomeText}">
"Hello"/>
"Guest"/>
如果您的messages.properties包括以下的內容:
welcomeText={0}, Your name is {1}.
則{0}與{1}會被取代為
Hello, Your name is Guest.
另一個使用的方法則是:
"{0}, Your name is {1}.">
"Hello"/>
"Guest"/>
簡單的顯示指定的值或綁定的訊息,例如:
"#{user.name}"/>
顯示單行輸入欄位,即輸出 HTML標籤,其type屬性設定為text,例如:
"#{user.name}"/>
顯示多行輸入文字區域,即輸出
"#{user.command}"/>
顯示密碼輸入欄位,即輸出 HTML標籤,其type屬性設定為password,例如:
"#{user.password}"/>
您可以設定redisplay屬性以決定是否要顯示密碼欄位的值,預設是false。
隱藏欄位,即輸出 HTML標籤,其type屬性設定為hidden,隱藏欄位的值用於保留一些訊息於客戶端,以在下一次發送表單時一併送出,例如:
"#{user.hiddenInfo}"/>
命令類標籤包括commandButton與commandLink,其主要作用在於提供一個命令按鈕或連結,以下舉例說明:
顯示一個命令按鈕,即輸出 HTML標籤,其type屬性可以設定為button、submit或reset,預設是submit,按下按鈕會觸發 javax.faces.event.ActionEvent,使用例子如下:
"送出" action="#{user.verify}"/>
您可以設定image屬性,指定圖片的URL,設定了image屬性的話,標籤的type屬性會被設定為image,例如:
"#{msgs.commandText}"
image="images/logowiki.jpg"
action="#{user.verify}"/>
"#{msgs.commandText}"
action="#{user.verify}"/>
產生的HTML輸出範例如下:
如果搭配
"welcome"/>
"locale" value="zh_TW"/>
在視圖上呈現一個核取方塊,例如:
我同意 "#/{user.aggree/}"/>
value所綁定的屬性必須接受與傳回boolean型態。這個元件在網頁上呈現的外觀如下:
這三個標籤的作用,是讓使用者從其所提供的選項中選擇一個項目,所不同的就是其外觀上的差別,例如:
"#{user.education}">
"高中" itemValue="高中"/>
"大學" itemValue="大學"/>
"研究所以上" itemValue="研究所以上"/>
value所綁定的屬性可以接受字串以外的型態或是自訂型態,但記得如果是必須轉換的型態或自訂型態,必須搭配 標準轉換器 或 自訂轉換器 來轉換為物件,
您也可以設定layout屬性,可設定的屬性是lineDirection、pageDirection,預設是lineDirection,也就是由左到右來排列選項,如果設定為pageDirection,則是由上至下排列選項,例如設定為:
"pageDirection"
value="#{user.education}">
"高中" itemValue="高中"/>
"大學" itemValue="大學"/>
"研究所以上" itemValue="研究所以上"/>
則外觀如下:
這三個標籤提供使用者複選項目的功能,一個
"pageDirection"
value="#{user.preferColors}">
"紅" itemValue="false"/>
"黃" itemValue="false"/>
"藍" itemValue="false"/>
value所綁定的屬性必須是陣列或集合(Collection)物件,在這個例子中所使用的是boolean陣列,例如:
package onlyfun.caterpillar;
public class UserBean {
private boolean[] preferColors;
public boolean[] getPreferColors() {
return preferColors;
}
public void setPreferColors(boolean[] preferColors) {
this.preferColors = preferColors;
}
......
}
如果是其它型態的物件,必要時必須搭配轉換器(Converter)進行字串與物件之間的轉換。
下圖是
在Internet Explorer則是這樣的:
選擇類標籤可以搭配"高中"
itemValue="高中"
itemDescription="學歷"
itemDisabled="true"/>
itemLabel屬性設定顯示在網頁上的文字,itemValue設定發送至伺服端時的值,itemDescription 設定文字描述,它只作用於一些工具程式,對HTML沒有什麼影響,itemDisabled設定是否選項是否作用,這些屬性也都可以使用JSF Expression Language來綁定至一個值。
"#{user.sex}"/>
則綁定的Bean上必須提供下面這個方法:
....
public SelectItem getSex() {
return new SelectItem("男");
}
....
如果要一次提供多個選項,則可以使用
"#{user.education}">
"#{user.educationItems}"/>
這個例子中
....
private SelectItem[] educationItems;
public SelectItem[] getEducationItems() {
if(educationItems == null) {
educationItems = new SelectItem[3];
educationItems[0] =
new SelectItem("高中", "高中");
educationItems[1] =
new SelectItem("大學", "大學");
educationItems[2] =
new SelectItem("研究所以上", "研究所以上");
}
return educationItems;
}
....
在這個例子中,SelectItem的第一個建構參數用以設定value,而第二個參數用以設定label,SelectItem還提供有數個建構函式,記得可以參考一下線上API文件。
您也可以提供一個傳回Map物件的方法,Map的key-value會分別作為選項的label-value,例如:
"pageDirection"
value="#{user.preferColors}">
"#{user.preferColorItems}"/>
您要提供下面的程式來搭配上面這個例子:
....
private Map preferColorItems;
public Map getPreferColorItems() {
if(preferColorItems == null) {
preferColorItems = new HashMap();
preferColorItems.put("紅", "Red");
preferColorItems.put("黃", "Yellow");
preferColorItems.put("藍", "Blue");
}
return preferColorItems;
}
....
這個標籤會繪製一個HTML 標籤,value可以指定路徑或圖片URL,路徑可以指定相對路徑或絕對路徑,例如:
"/images/logowiki.jpg"/>
這個標籤可以用來作簡單的元件排版,它會使用HTML表格標籤來繪製表格,並將元件置於其中,主要指定columns屬性,例如設定為 2:
"2">
"Username"/>
"name" value="#{userBean.name}"/>
"Password"/>
"password" value="#{userBean.password}"/>
"submit" action="login"/>
"reset" type="reset"/>
則自動將元件分作 2 個 column來排列,排列出來的樣子如下:
"2">
Username
"name" value="#{userBean.name}"/>
Password
"password" value="#{userBean.password}"/>
"submit" action="login"/>
"reset" type="reset"/>
這個元件用來將數個JSF元件包裝起來,使其看來像是一個元件,例如:
"2">
"Username"/>
"name" value="#{userBean.name}"/>
"Password"/>
"password" value="#{userBean.password}"/>
"submit" action="login"/>
"reset" type="reset"/>
在
很多資料經常使用表格來表現,JSF提供
package onlyfun.caterpillar;
public class UserBean {
private String name;
private String password;
public UserBean() {
}
public UserBean(String name, String password) {
this.name = name;
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
package onlyfun.caterpillar;
import java.util.*;
public class TableBean {
private List userList;
public List getUserList() {
if(userList == null) {
userList = new ArrayList();
userList.add(new UserBean("caterpillar", "123456"));
userList.add(new UserBean("momor", "654321"));
userList.add(new UserBean("becky", "7890"));
}
return userList;
}
}
在TableBean中,我們假設getUserList()方法實際上是從資料庫中查詢出UserBean的內容,之後傳回List物件,若我們的 faces-config.xml如下:
"1.0" encoding="UTF-8"?>
//Sun Microsystems,
Inc.//DTD JavaServer Faces Config 1.0//EN"
"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
tableBean
onlyfun.caterpillar.TableBean
request
userBean
onlyfun.caterpillar.UserBean
request
我們可以如下使用
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
"#{tableBean.userList}" var="user">
"#{user.name}"/>
"#{user.password}"/>
所產生的HTML表格標籤如下:
caterpillar
123456
momor
654321
becky
7890
"#{tableBean.userList}" var="user">
"header">
"Name"/>
"#{user.name}"/>
"footer">
"****"/>
"header">
"Password"/>
"#{user.password}"/>
"footer">
"****"/>
所產生的表格如下所示:
另外,對於表頭、表尾仍至於每一行列,都可以分別設定CSS風格,例如下面這個styles.css摘錄自Core JSF一書:
.orders {
border: thin solid black;
}
.ordersHeader {
text-align: center;
font-style: italic;
color: Snow;
background: Teal;
}
.evenColumn {
height: 25px;
text-align: center;
background: MediumTurquoise;
}
.oddColumn {
text-align: center;
background: PowderBlue;
}
可以在我們的頁面中如下加入:
....
"styles.css" rel="stylesheet" type="text/css"/>
....
"#{tableBean.userList}" var="user"
styleClass="orders"
headerClass="ordersHeader"
rowClasses="evenColumn,oddColumn">
"header">
"Name"/>
"#{user.name}"/>
"footer">
"****"/>
"header">
"Password"/>
"#{user.password}"/>
"footer">
"****"/>
則顯示的表格結果如下:
對於前四種型態,JSF實際上是以javax.faces.model.DataModel加以包裝,DataModel是個抽象類別,其子類別都是位於 javax.faces.model這個package下:
如果您想要對表格資料有更多的控制,您可以直接使用DataModel來設定表格資料,呼叫DataModel的setWrappedObject()方法可以讓您設定對應型態的資料,呼叫getWrappedObject()則可以取回資料,例如:
package onlyfun.caterpillar;
import java.util.*;
import javax.faces.model.DataModel;
import javax.faces.model.ListDataModel;
public class TableBean {
private DataModel model;
private int rowIndex = -1;
public DataModel getUsers() {
if(model == null) {
model = new ListDataModel();
model.setWrappedData(getUserList());
}
return model;
}
private List getUserList() {
List userList = new ArrayList();
userList.add(new UserBean("caterpillar", "123456"));
userList.add(new UserBean("momor", "654321"));
userList.add(new UserBean("becky", "7890"));
return userList;
}
public int getSelectedRowIndex() {
return rowIndex;
}
public String select() {
rowIndex = model.getRowIndex();
return "success";
}
}
在這個Bean中,我們直接設定DataModel?,將userList設定給它,如您所看到的,我們還可以取得DataModel?的各個 變項,在這個例子中,select()將作為點選表格之後的事件處理方法,我們可以藉由DataModel?的getRowIndex ()來取得所點選的是哪一row的資料,例如:
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
"styles.css" rel="stylesheet" type="text/css"/>
"#{tableBean.users}" var="user"
styleClass="orders"
headerClass="ordersHeader"
rowClasses="evenColumn,oddColumn">
"header">
"Name"/>
"#{tableBean.select}">
"#{user.name}"/>
"footer">
"****"/>
"header">
"Password"/>
"#{user.password}"/>
"footer">
"****"/>
Selected Row: "#{tableBean.selectedRowIndex}"/>
DataModel的rowIndex是從0開始計算,當處理ActionEvent時,JSF會逐次遞增rowIndex的值,這讓您可以得知目前正在處理的是哪一個row的資料,一個執行的圖示如下:
二十九、 JSF 生命週期JSF的每個元件基本上都是可替換的,像是轉換器(Converter)、驗證器(Validator)、元件(Component)、繪製器 (Renderer)等等,每個元件都可以替換讓JSF在使用時更有彈性,但相對的所付出的就是元件組合時的複雜性,為此,最基本的,如果您打算自訂一些 JSF元件,那麼您對於JSF處理請求的每個階段必須要有所瞭解。
下圖是JSF處理請求時的每個階段與簡單說明,起始狀態即使用者端發出請求時,終止狀態則相當於繪製器發出回應時:
扣除事件處理,JSF總共必須經過六個階段:
對於選擇的頁面如果是初次瀏覽則建立新的元件樹。如果是會話階段,會從使用者端或伺服器端的資料找尋資料以回復每個元件的狀態並重建元件樹,如果不包括請求參數,則直接跳過接下來的階段直接繪製回應。
每個元件嘗試從到來的請求中找尋自己的參數並更新元件值,在這邊會觸發ActionEvent,這個事件會被排入佇列中,然後在喚起應用程式階段之後才會真正由事件處理者進行處理。
然而對於設定immeduate為true的命令(Commamnd)元件來說,會立即處理事件並跳過之後的階段直接繪製回應,而對於設定immediate為true的輸入(Input)元件,會馬上進行轉換驗證並處理值變事件,之後跳過接下來的階段,直接繪製回應。
進行轉換與驗證處理,如果驗證錯誤,則會跳過之後的階段,直接繪製回應,結果是重新呼叫同一頁繪製結果。
更新每一個與元件綁定的backing bean或模型物件。
處理動作事件,並進行後端應用程式邏輯。
使用繪製器繪製頁面。
如果您只是要「使用」JSF,則您最基本的只需要知道「執行驗證」、「更新模型值」、與「喚起應用程式」這三個階段及中間的事件觸發,JSF參考實作將這三個階段之外的其它階段之複雜性隱藏起來了,您不需要知道這幾個階段的處理細節。
然而如果您要自訂元件,則您還必須知道「回復畫面」、「套用請求值」與「繪製回應」這些階段是如何處理的,這幾個階段相當複雜,所幸的是您可以使用JSF 所提供的框架來進行元件自訂,JSF提供的框架已經很大程度上降低了元件製作的複雜性。
當然,即使JSF框架降低了複雜性,但實際上要處理JSF自訂元件還是很複雜的一件事,在嘗試開發自訂元件之前,您可以先搜尋一些網站,像是 Apache MyFaceshttp://myfaces.apache.org/,看看是不是已經有相關類似的元件已經開發完成,省去您重新自訂元件的氣力。
三十、 概述自訂元件Tag即之前一直在使用的JSF標籤,類似於HTML標籤,JSF標籤主要是方便網頁設計人員進行版面配置與資料呈現的一種方式,實際的處理中,JSF標籤的目的在於設定Component屬性、設定驗證器、設定資料綁定、設定方法綁定等等。
Component的目的在於處理請求,當請求來到伺服端應用程式時,每一個Component都有機會根據自己的client id,從請求中取得屬於自己的值,接著Component可以將這個值作處理,然後設定給綁定的bean。
當請求來到Web應用程式時,HTTP中的字串內容可以轉換為JSF元件所需的值,這個動作稱之為解碼(decode),相對的,將JSF 元件的值轉換為HTTP字串資料並送至客戶端,這個動作稱之為編碼(encode),Component可自己處理編碼、解碼的任務,也可以將之委託給 Renderer來處理。
當您要自訂Component時,您可以繼承UIComonent或其相關的子類別,這要根據您實際要自訂的元件而定,如果您要自訂一個輸出元 件,可以繼承UIOutput,如果要自訂一個輸入元件,則可以繼承UIInput,每一個標準的JSF元件實際上都對應了一個 UIComponent的子類別,下圖為一個大致的類別繼承架構圖:
實際上要自訂一個元件是複雜的一件工作,您首先要學會的是一個完整的自訂元件流程,實際上要自訂一個元件時,您可以參考一下網路上的一些成品,例如 Apache MyFaceshttp://myfaces.apache.org/,接下來後面的幾個主題所要介紹的,將只是一個自訂元件的簡單流程。
Renderer是一個可替換的元件,您的Component可以搭配不同的Renderer,而不用自行擔任繪製回應或解碼的動作,這會讓您的 Component可以重用,當您需要將回應從HTML轉換為其它的媒介時(例如行動電話網路),則只要替換Renderer就可以了,這是一個好處,或 者您可以簡單的替換掉一個Renderer,就可以將原先簡單的HTML回應,替換為有JavaScript功能的Renderer。
當您開始接觸自訂元件時,您會開始接觸到JSF的框架(Framework),也許有幾個類別會是您經常接觸的:
自訂Component所要繼承的父類別,但通常,您是繼承其子類別,例如UIInput、UIOutput等等。
自訂JSF標籤所要繼承的父類別,繼承它可以幫您省去許多JSF標籤處理的細節。
包括了JSF相關的請求資訊,您可以透過它取得請求物件或請求參數,或者是 javax.faces.application.Application物件。
包括了一個應用程式所共享的資訊,像是locale、驗證器、轉換器等等,您可以透過一些 工廠方法 取得相關的資訊。
Component可以自己負責將物件資料編碼為HTML文件或其它的輸出文件,也可以將這個任務委託給 Renderer,這邊先介紹的是讓Component自己負責編碼的動作。
這邊著重的是介紹完成自訂元件所必須的流程,所以我們不設計太複雜的元件,這邊將完成以下的元件,這個元件會有一個輸入文字欄位以及一個送出按鈕:
您要繼承UIComponent或其子類別來自訂Component,由於文字欄位是一個輸入欄位,為了方便,您可以繼承UIInput類別,這可以讓您省去一些處理細節的功夫,在繼承UIComponent或其子類別後,與編碼相關的主要有三個方法:
其中encodeChildren()是在包括子元件時必須定義,Component如果它的 getRendersChildren()方法傳回true時會呼叫encodeChildren()方法,預設上, getRendersChildren()方法傳回false。
由於我們的自訂元件相當簡單,所以將編碼的動作寫在encodeBegin()或是encodeEnd()都可以,我們這邊是定義encodeBegin ()方法:
package onlyfun.caterpillar;
import java.io.IOException;
import java.util.Map;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
public class UITextWithCmd extends UIInput {
private static final String TEXT = ".text";
private static final String CMD = ".cmd";
public UITextWithCmd() {
setRendererType(null);
}
public void encodeBegin(FacesContext context)
throws IOException {
ResponseWriter writer = context.getResponseWriter();
String clientId = getClientId(context);
encodeTextField(writer, clientId);
encodeCommand(writer, clientId);
}
public void decode(FacesContext context) {
// .....
}
private void encodeTextField(ResponseWriter writer,
String clientId) throws IOException {
writer.startElement("input", this);
writer.writeAttribute("name", clientId + TEXT, null);
Object value = getValue();
if(value != null) {
writer.writeAttribute("value",
value.toString(), null);
}
String size = (String) getAttributes().get("size");
if(size != null) {
writer.writeAttribute("size", size, null);
}
writer.endElement("input");
}
private void encodeCommand(ResponseWriter writer,
String clientId) throws IOException {
writer.startElement("input", this);
writer.writeAttribute("type", "submit", null);
writer.writeAttribute("name", clientId + CMD, null);
writer.writeAttribute("value", "submit", null);
writer.endElement("input");
}
}
在encodeBegin()方法中,我們取得ResponseWriter物件,這個物件可以協助您輸出HTML標籤、屬性等,我們使用 getClientId()取得元件的id,這個id是每個元件的唯一識別,預設上如果您沒有指定,則JSF會自動為您產生id值。
接著我們分別對輸入文字欄位及送出鈕作HTML標籤輸出,在輸出時,我們將name屬性設成clientId與一個字串值的結合(即TEXT或CMD),這是為了方便在解碼時,取得對應name屬性的請求值。
在encodeTextField中我們有呼叫getValue()方法,這個方法是從UIOutput繼承下來的,getValue() 方法可以取得Component的設定值,這個值可能是靜態的屬性設定值,也可能是JSF Expression的綁定值,預設會先從元件的屬性設定值開始找尋,如果找不到,再從綁定值(ValueBinding物件)中找尋,元件的屬性值或綁 定值的設定,是在定義Tag時要作的事。
編碥的部份總結來說,是取得Component的值並作適當的HTML標籤輸出,再來我們看看解碼的部份,這是定義在decode()方法中,將下面的內容加入至上面的類別定義中:
....
public void decode(FacesContext context) {
Map reqParaMap = context.getExternalContext().
getRequestParameterMap();
String clientId = getClientId(context);
String submittedValue =
(String) reqParaMap.get(clientId + TEXT);
setSubmittedValue(submittedValue);
setValid(true);
}
....
我們必須先取得RequestParameterMap,這個Map物件中填入了所有客戶端傳來的請求參數, Component在這個方法中有機會查詢這些請求參數中,是否有自己所想要取得的資料,記得我們之前解碼時,是將輸入欄位的name屬性解碼為 client id加上一個字串值(即TEXT設定的值),所以這時,我們嘗試從RequestParameterMap中取得這個請求值。
取得請求值之後,您可以將資料藉由setSumittedValue()設定給綁定的bean,最後呼叫setValid()方法,這個方法設定為 true時,表示元件正確的獲得自己的值,沒有任何的錯誤發生。
由於我們先不使用Renderer,所以在建構函式中,我們設定RendererType為null,表示我們不使用Renderer進行解碼輸出:
public UITextWithCmd() {
setRendererType(null);
}
在我們的例子中,我們都是處理字串物件,所以這邊不需要轉換器,如果您需要使用轉換器,可以呼叫setConverter()方法加以設定,在不使用 Renderer的時候,Component要設定轉換器來自行進行字串與物件的轉換。
三十二、 元件標籤完成Component的自訂,接下來要設定一個自訂Tag與之對應,自訂Tag的目的,在於設定 Component屬性,取得Componenty型態,取得Renderer型態值等;屬性的設定包括了設定靜態值、設定綁定值、設定驗證器等等。
要自訂與Component對應的Tag,您可以繼承UIComponentTag,例如:
package onlyfun.caterpillar;
import javax.faces.application.Application;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;
import javax.faces.webapp.UIComponentTag;
public class TextWithCmdTag extends UIComponentTag {
private String size;
private String value;
public String getComponentType() {
return "onlyfun.caterpillar.TextWithCmd";
}
public String getRendererType() {
return null;
}
public void setProperties(UIComponent component) {
super.setProperties(component);
setStringProperty(component, "size", size);
setStringProperty(component, "value", value);
}
private void setStringProperty(UIComponent component,
String attrName, String attrValue) {
if(attrValue == null)
return;
if(isValueReference(attrValue)) {
FacesContext context =
FacesContext.getCurrentInstance();
Application application =
context.getApplication();
ValueBinding binding =
application.createValueBinding(attrValue);
component.setValueBinding(attrName, binding);
}
else {
component.getAttributes().
put(attrName, attrValue);
}
}
public void release() {
super.release();
size = null;
value = null;
}
public String getSize() {
return size;
}
public void setSize(String size) {
this.size = size;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
首先看到這兩個方法:
public String getComponentType() {
return "onlyfun.caterpillar.TextWithCmd";
}
public String getRendererType() {
return null;
}
由於我們的Component目前不使用Renderer,所以getRendererType()傳回null值,而 getComponentType()在於讓JSF取得這個Tag所對應的Component,所傳回的值在faces-config.xml中要有定 義,例如:
....
onlyfun.caterpillar.TextWithCmd
onlyfun.caterpillar.UITextWithCmd
....
藉由faces-config.xml中的定義,JSF可以得知 onlyfun.caterpillar.TextWithCmd的真正類別,而這樣的定義方式很顯然的,您可以隨時換掉
在設定Component屬性值時,可以由component.getAttributes()取得Map物件,並將標籤屬性值存入Map 中,這個Map物件可以在對應的Component中使用getAttributes()取得,例如在上一個主題中的UITextWithCmd中可以如 下取得存入Map的size屬性:
package onlyfun.caterpillar;
import java.io.IOException;
import java.util.Map;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
public class UITextWithCmd extends UIInput {
....
private void encodeTextField(ResponseWriter writer,
String clientId) throws IOException {
....
String size = (String) getAttributes().get("size");
if(size != null) {
writer.writeAttribute("size", size, null);
}
.....
}
....
}
可以使用isValueReference()來測試是否為JSF Expression Language的綁定語法,如果是的話,則我們必須建立ValueBinding物件,並設定值綁定:
....
private void setStringProperty(UIComponent component,
String attrName, String attrValue) {
if(attrValue == null)
return;
if(isValueReference(attrValue)) {
FacesContext context =
FacesContext.getCurrentInstance();
Application application =
context.getApplication();
ValueBinding binding =
application.createValueBinding(attrValue);
component.setValueBinding(attrName, binding);
}
else {
component.getAttributes().
put(attrName, attrValue);
}
}
....
如果是value屬性,記得在上一個主題中我們提過,從UIOutput繼承下來的getValue()方法可以取得 Component的value設定值,這個值可能是靜態的屬性設定值,也可能是JSF Expression的綁定值,預設會先從元件的屬性設定值開始找尋,如果找不到,再從綁定值(ValueBinding物件)中找尋。
最後,我們必須提供自訂Tag的tld檔:
"1.0" encoding="UTF-8"?>
"2.0"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=
"http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd">
1.0
2.0
<short-name>textcmdshort-name>
http://caterpillar.onlyfun.net/textcmd
textcmd
onlyfun.caterpillar.TextWithCmdTag
empty
size
value
true
三十三 、使用自訂元件
在Component與Tag自訂完成後,這邊來看看如何使用它們,首先定義faces-config.xml:
"1.0" encoding="UTF-8"?>
//Sun Microsystems,
Inc.//DTD JavaServer Faces Config 1.0//EN"
"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
onlyfun.caterpillar.TextWithCmd
onlyfun.caterpillar.UITextWithCmd
someBean
onlyfun.caterpillar.SomeBean
session
我們所撰寫的SomeBean測試類別如下:
package onlyfun.caterpillar;
public class SomeBean {
private String data;
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
這邊寫一個簡單的網頁來測試一下我們撰寫的自訂元件:
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="/WEB-INF/textcmd.tld" prefix="oc" %>
"styles.css" rel="stylesheet" type="text/css"/>
Input data: "10"
value="#{someBean.data}"/>
"#{someBean.data}"/>
三十四、 自訂 Renderer
Component可以將解碼、編碼的動作交給Renderer,這讓您的表現層技術可以輕易的抽換,我們可以將之前的自訂元件的解碼、編碼動作移 出至 Renderer,不過由於我們之前設計的Component是個很簡單的元件,事實上,如果只是要新增一個Command在輸入欄位旁邊,我們並不需要 大費周章的自訂一個新的元件,我們可以直接為輸入欄位更換一個自訂的Renderer。
要自訂一個Renderer,您要繼承javax.faces.render.Renderer,我們的自訂Renderer如下:
package onlyfun.caterpillar;
import java.io.IOException;
import java.util.Map;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.Renderer;
public class TextCmdRenderer extends Renderer {
private static final String TEXT = ".text";
private static final String CMD = ".cmd";
public void encodeBegin(FacesContext context,
UIComponent component) throws IOException {
ResponseWriter writer = context.getResponseWriter();
String clientId = component.getClientId(context);
encodeTextField(component, writer, clientId);
encodeCommand(component, writer, clientId);
}
public void decode(FacesContext context,
UIComponent component) {
Map reqParaMap = context.getExternalContext().
getRequestParameterMap();
String clientId = component.getClientId(context);
String submittedValue =
(String) reqParaMap.get(clientId + TEXT);
((EditableValueHolder) component).setSubmittedValue(
submittedValue);
((EditableValueHolder) component).setValid(true);
}
private void encodeTextField(UIComponent component,
ResponseWriter writer, String clientId)
throws IOException {
writer.startElement("input", component);
writer.writeAttribute("name", clientId + TEXT, null);
Object value = ((UIInput) component).getValue();
if(value != null) {
writer.writeAttribute("value",
alue.toString(), null);
}
String size =
(String) component.getAttributes().get("size");
if(size != null) {
writer.writeAttribute("size", size, null);
}
writer.endElement("input");
}
private void encodeCommand(UIComponent component,
ResponseWriter writer,
String clientId) throws IOException {
writer.startElement("input", component);
writer.writeAttribute("type", "submit", null);
writer.writeAttribute("name", clientId + CMD, null);
writer.writeAttribute("value", "submit", null);
writer.endElement("input");
}
}
這個自訂的Renderer其解碼、編碼過程,與之前直接在Component中進行解碼或編碼過程是類似的,所不同的是在解碼與編碼的方法上,多了 UIComponent參數,代表所代理繪製的Component。
接下來在自訂Tag上,我們的TextWithCmdTag與之前主題所介紹的沒什麼差別,只不過在getComponentType()與 getRendererType()方法上要修改一下:
package onlyfun.caterpillar;
import javax.faces.application.Application;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;
import javax.faces.webapp.UIComponentTag;
public class TextWithCmdTag extends UIComponentTag {
private String size;
private String value;
public String getComponentType() {
return "javax.faces.Input";
}
public String getRendererType() {
return "onlyfun.caterpillar.TextCmd";
}
.....
}
getComponentType()取得的是"javax.faces.Input",它實際上對應至UIInput類別,而 getRendererType()取回的是"onlyfun.caterpillar.TextCmd",這會在faces-config.xml中定 義,以對應至實際的Renderer類別:
....
javax.faces.Input
onlyfun.caterpillar.TextCmd
onlyfun.caterpillar.TextCmdRenderer
....
為Component定義一個Renderer,必須由component family與renderer type共同定義,這並不難理解,因為一個Component可以搭配不同的Renderer,但它是屬於同一個component family,例如UIInput就是屬於javax.faces.Input這個元件家族,而我們為它定義一個新的Renderer。
接下未完成的範例可以取之前主題介紹過的,我們雖然沒有自訂元件,但我們為UIInput置換了一個新的Renderer,這個Renderer會在輸入欄位上加入一個按鈕。
如果您堅持使用之前自訂的UITextWithCmd,則可以如下修改:
package onlyfun.caterpillar;
import javax.faces.component.UIInput;
public class UITextWithCmd extends UIInput {
public UITextWithCmd() {
setRendererType("onlyfun.caterpillar.TextCmd");
}
}
我們只是單純的繼承UIInput,然後使用setRendererType()設定"onlyfun.caterpillar.TextCmd",但並沒有為元件加入什麼行為,看來什麼事都沒有作,但事實上這是因為繼承了UIInput,它為我們處理了大多數的細節。
接下來同樣的,設定自訂Tag:
package onlyfun.caterpillar;
import javax.faces.application.Application;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;
import javax.faces.webapp.UIComponentTag;
public class TextWithCmdTag extends UIComponentTag {
private String size;
private String value;
public String getComponentType() {
return "onlyfun.caterpillar.TextWithCmd";
}
public String getRendererType() {
return "onlyfun.caterpillar.TextCmd";
}
.....
}
要使用自訂的Component,記得要在faces-config.xml中再加入:
....
onlyfun.caterpillar.TextWithCmd
onlyfun.caterpillar.UITextWithCmd
...