JavaServer Faces(JSF)提供可擴充的組件模型,開發人員可以建立可重用的組件,使用這些自訂群組件提高開發效率和降低開發成本。雖然對於定製和重用而言 JSF 的組件模型非常強大,但是開發人員普遍認為開發 JSF 自訂群組件並不容易,因為通常至少需要熟悉 JSF encode/decode 和 state holder 的內部機制並覆蓋相應的方法(參考“懷疑論者的 JSF: JSF 組件開發”所述),如 encodeBegine()、decode()、saveState() 和 restoreState() 等,對於開發複雜的自訂群組件,甚至需要深入理解更多的介面,如 NamingContainer、StateHolder、EditableValueHolder 和 ActionSource 等介面。
然而,重用 JSF 標準組件的功能可以極大地簡化自訂群組件的開發,尤其對於自訂複合組件更是如此。在大部分情況下,我們可以重用 JSF 架構已經提供的標準渲染器、狀態管理、事件監聽器、轉換器和驗證器。已有的文章或書籍對如何重用這些標準功能涉及很少,本文基於重用的策略提出快速開發 JSF 自訂複合組件的原則和技巧。
本文首先總結了 JSF 組件開發的通用原則,然後通過一個例子(Value Scroller 自訂複合組件)的開發詳解說明了哪些標準功能可以重用以及如何重用,以達到簡化 JSF 自訂複合組件開發的目的。原則與技巧
開發 JSF 自訂複合組件主要有兩個原則,一方面強調重用已有的標準組件;另一方面如何確保自訂群組件易於重用。
1.儘可能的重用標準組件的功能和實現
傳統的自訂複合組件開發建議完全覆蓋實現 encode/decode 邏輯,但這樣做耗費時間而且容易出錯。毫無疑問,我們可以通過重用標準組件的渲染器等機制減少甚至根本不用自行編寫這部分代碼。另外,為了實現靈活的配置和使用,自訂複合組件通常需要提供很多屬性,我們需要寫很多代碼來處理這些屬性的讀寫和狀態管理。實際上,我們可以簡單地把自訂複合組件的屬性傳遞給它自身包含的標準組件,由已有的標準代碼去處理這些屬性,而不用重複寫這些代碼。
2.清晰地分離組件類、標籤類和模型類
JSF 的組件模型建議在組件類、標籤類和模型類之間有明確清晰的責任分配,以便於重用和擴充。組件類不應該依賴於 javax.faces.component.html 包,因為組件類不僅可以用於 HTML,而且還應該可以重用於其它標記語言(如 WML)。也就是說組件類不應該直接引用 javax.faces.component.html 包內的 HTML 元件。例如,在你的組件類中建立一個 HtmlCommandButton 的執行個體是不可取的,你應該考慮用 javax.faces.component 包中的 UICommand 。另一方面,如果你希望你的模型類可以重用於不同的 Web 架構,那麼你的模型類就不應該依賴於 JSF 的任何包,即模型類只表示業務對象而不包含任何使用者介面相關的組件、資料和狀態。
基於這些原則,對比傳統方式和本文介紹的技巧,我們可以發現基於重用的開發策略會極大簡化 JSF 自訂複合組件的編寫。開發 JSF 自訂群組件通常需要如下 3 個步驟(參考“懷疑論者的 JSF: JSF 組件開發”)。
1.擴充 UIComponent
·傳統方式:建立一個類,擴充 UIComponent,儲存組件狀態,在 faces-config.xml 中註冊組件
重用技巧:
·選擇 UIPanel 作為版面配置容器,重用標準組件作為複合組件的子組件。
·實現內部動作監聽器。
2.定義渲染器或者內聯實現它
傳統方式:覆蓋實現 encode/decode,在 faces-config.xml 中註冊渲染器。
重用技巧:重用標準渲染器類型。
3.建立自訂標籤,繼承 UIComponentTag
傳統方式:返回渲染器類型和組件類型,設定 JSF 運算式屬性
重用技巧:傳遞屬性值給作為子組件的標準組件。樣本概述
我們通過一個自訂複合組件 Value Scroller 的開發步驟說明如何運用多種技巧重用標準組件的功能和實現,達到簡化開發易於重用的目的。 Value Scroller 可以讓你通過點擊增值或減值按鈕來輸入數值,而不用手工鍵入,如 圖 1 所示。這個樣本只包含最基本的功能,如只支援整型數值輸入,但對於本文要介紹的內容已經足夠了。
圖 1. 測試頁面中的 Value Scroller
圖 2 說明了 Value Scroller 的基本類結構,遵循 MVC 模式。組件類 ValueScroller 擴充了 UIPanel,作為控制器(Controller)負責與使用者的互動。標籤類 ValueScrollerTag 繼承了 UIComponentTag, 作為視圖(View)處理頁面顯示。與 Value Scroller 綁定的值對象作為模型(Model)儲存使用者鍵入的數值。
圖 2. Value Scroller 的類結構
在後面章節中,本文將結合 Value Scroller 樣本說明如何應用前面提到的原則和技巧快速開發 JSF 自訂複合組件。 選擇 UIPanel 作為容器
建立 JSF 自訂複合組件的第一步就是要選擇一個標準組件類進行擴充。通常我們會考慮將這個組件類作為容器,在其中嵌入子組件,從而構成複合組件。這裡選擇繼承 UIPanel 作為 Value Scroller 的容器,以 Grid 的方式渲染產生頁面,並且其中包含一個 UIInput 和兩個 UICommand,分別作為數值輸入框和加減值按鈕,如 清單 1 所示:
清單 1. 擴充類 UIPanel
public class ValueScroller extends UIPanel {
/**
* The default constructor
*
*/
public ValueScroller() {
super();
addChildrenAndFaces();
}
}
作為 Value Scroller 子組件的那些標準組件將在 addChildrenAndFaces 方法中加入版面配置容器之中。重用標準渲染器類型
接著,我們開始建立 Value Scroller 的子組件,並且實現渲染器的功能。按照傳統方式,必須覆蓋 UIComponent 的 encodeBegin() 和 decode() 方法,但是,如果我們開發的複合組件只是由多個標準組件構成,我們完全可以將不依賴於特定標記語言的標準組件基類加入到自訂群組件中,並且為每個標準組件設定一個標準的渲染器類型,就可以完成複合組件要實現的渲染器功能。重用標準組件渲染器類型好處在於兩方面:減少開發的工作量和可能出錯的機會,對於 JSF 初學者尤為重要;不用實現與特定標記語言相關的 encode/decode 邏輯,使組件類更易於重用。
“JavaServer Faces 實戰” 這本書列出了 JSF 規範提供的標準渲染器類型。
表 1. JSF 標準渲染器
從 表 1 可以看出,一個組件基類通常對應於多個渲染器類型(如果使用 HTML 作為標記語言,即對應於多個 HTML 元素),因為組件基類只定義了通用的資料和行為。比如說,UICommand 有兩個 HTML 子類 HtmlCommandButton 和 HtmlCommandLink,分別對應於渲染器類型 javax.faces.Link 和 javax.faces.Button 。當我們想在一個複合組件內部包含一個連結時,只需要建立一個 UICommand 執行個體,並將其渲染器類型設定為 javax.faces.Link,而不用從頭覆蓋實現 encodeBegin() 和 decode() 方法。清單 2 列出了 Value Scroller 中的子組件如何在組件類 ValueScroller 中被建立,以及渲染器等屬性如何被設定。
清單 2. 重用標準渲染器建立自訂複合組件
private static final String PANEL_GRID_RENDERER = "javax.faces.Grid";
private static final String INPUT_TEXT_RENDERER = "javax.faces.Text";
private static final String COMMAND_LINK_RENDERER = "javax.faces.Link";
private static final String GRAPHIC_IMAGE_RENDERER = "javax.faces.Image";
/**
* Add children to the base container
*
*/
private void addChildrenAndFaces() {
// Set attributes of the base container
this.setRendererType(PANEL_GRID_RENDERER);
this.getAttributes().put(COLUMNS_ATTRIBUTE, new Integer(2));
// Add the input component
input = new UIInput();
input.setId(INPUT_ID);
input.setRendererType(INPUT_TEXT_RENDERER);
this.getChildren().add(input);
// Add the container of the up and down links
UIPanel linkContainer = new UIPanel();
linkContainer.setId(LINKPANEL_ID);
linkContainer.setRendererType(PANEL_GRID_RENDERER);
linkContainer.getAttributes().put(COLUMNS_ATTRIBUTE, new Integer(1));
ScrollerActionListener listener = new ScrollerActionListener();
// Add the up link
UICommand upLink = new UICommand();
upLink.setId(UPLINK_ID);
upLink.setRendererType(COMMAND_LINK_RENDERER);
upLink.addActionListener(listener);
UIGraphic upImage = new UIGraphic();
upImage.setId(UPIMAGE_ID);
upImage.setRendererType(GRAPHIC_IMAGE_RENDERER);
upImage.setUrl(UPIMAGE_URL);
upLink.getChildren().add(upImage);
linkContainer.getChildren().add(upLink);
// Add the down link
UICommand downLink = new UICommand();
downLink.setId(DOWNLINK_ID);
downLink.setRendererType(COMMAND_LINK_RENDERER);
downLink.addActionListener(listener);
UIGraphic downImage = new UIGraphic();
downImage.setId(DOWNIMAGE_ID);
downImage.setRendererType(GRAPHIC_IMAGE_RENDERER);
downImage.setUrl(DOWNIMAGE_URL);
downLink.getChildren().add(downImage);
linkContainer.getChildren().add(downLink);
this.getChildren().add(linkContainer);
}
將屬性值傳遞給標準組件
我們先看一下標籤描述檔案(TLD)中定義的 Value Scroller 提供的屬性。
清單 3. 在 TLD 中定義自訂複合組件的屬性
<tag>
<name>valueScroller</name>
<tag-class>component.taglib.ValueScrollerTag</tag-class>
<body-content>JSP</body-content>
<attribute>
<name>id</name>
<description>ValueScroller ID</description>
</attribute>
<attribute>
<name>value</name>
<description>ValueScroller value</description>
</attribute>
<attribute>
<name>size</name>
<description>Input field size</description>
</attribute>
<attribute>
<name>min</name>
<description>Minimum value</description>
</attribute>
<attribute>
<name>max</name>
<description>Maximum value</description>
</attribute>
<attribute>
<name>step</name>
<description>Scrolling step</description>
</attribute>
</tag>
我們看到,除了 min/max/step 是自訂的屬性之外,其他的都屬於 JSF 標準組件的屬性,可以直接傳遞給構成 Value Scroller 的標準組件處理,完全不用為這些標準組件的屬性覆蓋實現方法 saveState() 和 restoreState() 。
通常有兩種方法傳遞屬性值。當你需要對屬性進行一些額外的操作(如驗證或者轉換等),可以在標籤類 ValueScrollerTag 中將屬性傳遞給自訂群組件類,如下所示:
清單 4. 傳遞自訂屬性
/**
* Override the setProperties method
*/
protected void setProperties(UIComponent component) {
super.setProperties(component);
ValueScroller vs = (ValueScroller)component;
Application app = FacesContext.getCurrentInstance().getApplication();
// Set value attribute
if (value != null) {
if (isValueReference((String)value)) {
ValueBinding vb = app.createValueBinding((String)value);
vs.setValueBinding("value", vb);
} else {
throw new IllegalArgumentException("The value property must be a value " +
"binding expression that points to a bean property.");
}
}
// Set id attribute
if (id != null) {
vs.setId((String)id);
}
// Set other attributes
vs.setMin(min);
vs.setMax(max);
vs.setStep(step);
}
另外一種方法就是在標籤類 ValueScrollerTag 中直接把屬性值加入相應標準組件的屬性 Map 。例如,將 size 屬性傳遞給自訂複合組件包含的 UIInput:
清單 5. 傳遞標準屬性
vs.findComponent("input").getAttributes().put("size", new Integer(size));實現內部動作監聽器
在 Value Scroller 中,點擊增值或減值按鈕,輸入框內的值會隨之增大或者減小。我們可以簡單地在組件類 ValueScroller 中實現一個內部動作監聽器,重用 UICommand 的事件處理邏輯。
清單 6. 實現 Value Scroller 動作監聽器
/**
* Internal action listener for Value Scroller
* _cnnew1@author cll
*
*/
private class ScrollerActionListener implements ActionListener {
public void processAction(ActionEvent e) {
// Only Integer is supported for this demo
if (input.getValue() instanceof Integer) {
String commandId = ((UICommand)e.getSource()).getId();
int value = ((Integer)input.getValue()).intValue();
// Increase value if the up link is clicked
if (commandId.equals(UPLINK_ID)) {
if (value + getStep() > max) {
input.setValue(new Integer(max));
} else {
input.setValue(new Integer(value + getStep()));
}
}
// Decrease value if the down link is clicked
else if (commandId.equals(DOWNLINK_ID)) {
if (value - getStep() < min) {
input.setValue(new Integer(min));
} else {
input.setValue(new Integer(value - getStep()));
}
}
} else {
throw new IllegalArgumentException(
"Unsupported binding type, " +
"and only Integer instance allowed for this demo.");
}
}
}
最後,在調用 addChildrenAndFaces 方法建立添加子組件的時候,將這個自訂動作監聽器添加到增值和減值組件中去。
清單 7. 註冊 Value Scroller 動作監聽器
ScrollerActionListener listener = new ScrollerActionListener();
upLink.addActionListener(listener);
downLink.addActionListener(listener);
使用 Value Scroller
Value Scroller 的開發已經完成,使用也非常簡單,首先在 faces-config.xml 中聲明引用 Value Scroller,如下所示:
清單 8. 在 faces-config.xml 中聲明引用 Value Scroller
<component>
<component-type>xyz.ValueScroller</component-type>
<component-class>
component.ValueScroller
</component-class>
</component>
然後,在測試 JSP 頁面 Test.jsp 上包含 Value Scroller 的標籤描述檔案。
清單 9. 在 JSP 中包含 Value Scroller 的 TLD
<%@ taglib uri="/WEB-INF/lib/ValueScroller.tld" prefix="xyz"%>
最後,在 Test.jsp 頁面上使用 Value Scroller 的標籤,並且指定 size/min/max/step 屬性值,部署運行,就可以看到 圖 1 所示的結果了。
清單 10. 在 JSP 中建立 Value Scroller 並設定屬性
<xyz:valueScroller value="#{pc_Test.itemCount}" size="5" min="-50" max="10000" step="2">
</xyz:valueScroller>
結束語
我們可以看到,本文介紹的自訂複合組件 Value Scroller 的實現沒有編寫 encode/decode 和 state/event 管理相關的邏輯,簡單、快速、並且易於重用。本文總結的 JSF 自訂複合組件的開發技巧在很大程度上降低了複雜度和工作量,優於傳統的開發方式。
作者:IBM 陳荔龍 Ealine Zhan 齊克科