本文介紹開發 JavaServer Faces(JSF)自定義複合組件的新思路,提供如何快速開發可重用 JSF 組件的技巧,而不用像傳統方式那樣自己實現渲染器 (renderer)、狀態管理和事件JianTingQi.本文提供的原則和技術也對一般的 JSF 開發很有幫助.
介紹
JavaServer Faces(JSF)提供可擴展的組件模型,開發人員可以創建可重用的組件,使用這些自定義組件提高開發效率和降低開發成本.雖然對於定製和重用而言 JSF 的組件模型非常強大,但是開發人員普遍認為開發 JSF 自定義組件並不容易,因為通常至少需要熟悉 JSF encode/decode 和 state holder 的內部機制並覆蓋相應的方法,如 encodeBegine()、decode()、saveState() 和 restoreState() 等,對於開發複雜的自定義組件,甚至需要深入理解更多的介面,如 NamingContainer、StateHolder、EditableValueHolder 和 ActionSource 等介面.
然而,重用 JSF 標準組件的功能可以極大地簡化自定義組件的開發,尤其對於自定義複合組件更是如此.在大部分情況下,我們可以重用 JSF 框架已經提供的標準渲染器、狀態管理、事件JianTingQi、轉換器和驗證器.已有的文章或書籍對如何重用這些標準功能涉及很少,本文基於重用的策略提出快速開發 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 個步驟.
1、擴展 UIComponent
傳統方式:創建一個類,擴展 UIComponent,保存組件狀態,在 faces-config.xml 中註冊組件
重用技巧:
■ 選擇 UIPanel 作為布局容器,重用標準組件作為複合組件的子組件.
■ 實現內部動作JianTingQi.
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 標準渲染器
控制項族 | 組件類 | 渲染器類型 | HTML 渲染結果 |
Image | HtmlGraphicImage | Image | 顯示圖片 |
Input | HtmlInputHidden | Hidden | 隱藏類型輸入欄位 |
HtmlInputSecret | Secret | 密碼類型輸入欄位 | |
UIInput, HtmlInputText | Text | 文本類型輸入欄位 | |
HtmlInputTextarea | Textarea | 多行輸入欄位 | |
Message | UIMessage, HtmlMessage | Message | 特定組件消息 |
Messages | UIMessages, HtmlMessages | Messages | 所有消息 |
Output | HtmlOutputFormat | Format | 輸出參數化文本 |
HtmlOutputLabel | Label | 輸入欄位的文本標籤 | |
HtmlOutputLink | Link | 未與命令關聯的鏈接 | |
UIOutput, HtmlOutputText | Text | 普通文本 | |
Panel | HtmlPanelGrid | Grid | 可定製的表格 |
HtmlPanelGroup | Group | 將所包含組件歸為一組 | |
Checkbox | HtmlSelectBooleanCheckbox | Checkbox | 單個複選框 |
SelectMany | HtmlSelectManyCheckbox | Checkbox | 一組複選框 |
UISelectMany, HtmlSelectManyListbox | Listbox | 可多選的列表框 | |
HtmlSelectManyMenu | Menu | 可多選的菜單 | |
SelectOne | HtmlSelectOneRadio | Radio | 單選鈕 |
HtmlSelectOneListbox | Listbox | 單選列表框 | |
UISelectOne, HtmlSelectOneMenu | Menu | 單選菜單 |
從 表 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)); |
實現內部動作JianTingQi
在 Value Scroller 中,點擊增值或減值按鈕,輸入框內的值會隨之增大或者減小.我們可以簡單地在組件類 ValueScroller 中實現一個內部動作JianTingQi,重用 UICommand 的事件處理邏輯.
清單 6. 實現 Value Scroller 動作JianTingQi
/** * 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 方法創建添加子組件的時候,將這個自定義動作JianTingQi添加到增值和減值組件中去.
清單 7. 註冊 Value Scroller 動作JianTingQi
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 自定義複合組件的開發技巧在很大程度上降低了複雜度和工作量,優於傳統的開發方式.
下載
描述 | 名字 | 大小 | 下載方法 |
本文示例代碼 | Sample_Code.zip | 41 KB | FTP |
[火星人 ] 編寫JSF自定義複合組件的技巧和竅門已經有954次圍觀