級別: 中級
Rick Hightower , CTO, ArcMind
2005 年 8 月 16 日
在四部分的 懷疑論者的 JSF 系列的最后一期中,Rick Hightower 介紹了省時運動,它可以一次或永遠地說服您:JSF 組件開發要比您想像的更容易。
組件模型的關鍵考驗就是:能否從第三方供應商購買組件,并把它們插入應用程序?與可購買可視 Swing 組件一樣,也可以購買 Java ServerFaces (JSF) 組件!需要一個好玩的日歷?可以在開源實現和商業組件之間選擇??梢赃x擇購買一個,而不是自行開發復雜的基于 Web 的 GUI 組件。
JSF 擁有一個與 AWT 的 GUI 組件模型類似的組件模型??梢杂?JSF 創建可重用組件。但不幸的是,存在一個誤解:用 JSF 創建組件很困難。不要相信這些從未試過它的人們的 FUD!開發 JSF 組件并不困難。由于不用一遍又一遍重復相同的代碼,可以節約時間。一旦創建了組件,就可以容易地把組件拖到任何 JSP、甚至任何 JSF 表單中,如果正在處理的站點有 250 個頁面,這就很重要了。JSF 的大多數功能來自基類。因為所有的繁重工作都由 API 和基類完成,所以 JSF 把組件創建變得很容易。
貫穿這個系列,我一直在試圖幫助您克服造成許多 Java 開發人員逃避使用 JSF 技術的 FUD。我討論了對這項技術的基本誤解,介紹了它的底層框架和它最有價值的開發特性。有了這些基礎工作之后,我認為您已經可以采取行動,開發自己的定制 JSF 組件了。使用 JSF 的東西,我敢保證要比您想像的要更加容易,而且從節約的時間和精力上來說,回報如此之多,多得不能忽略。
這篇文章中的示例是用 JDK 1.5 和 Tomcat 開發的。請單擊頁面頂部的 示例代碼 下載示例源代碼。注意,與以前的文章不同,這篇文章沒有關聯的 build 文件,因為我特意把它留給您作為一個練習了。只要設置 IDE 或編譯器,把 /src 中的類編譯到 /webapp/WEB-INF/classes,并在 /webapp/WEB-INF/lib 中包含所有 JAR 文件(以及 servlet-api.jar 和 jsp-api.jar,它們包含在 Tomcat 中)。
JSF 組件模型
JSF 組件模型與 AWT GUI 組件模型類似。它有事件和屬性,就像 Swing 組件模型一樣。它也有包含組件的容器,容器也是組件,也可以由其他容器包含。從理論上說,JSF 組件模型分離自 HTML 和 JSP。JSF 自帶的標準組件集里面有 JSP 綁定,可以生成 HTML 渲染。
JSF 組件的示例包括日歷輸入組件和 HTML 富文本輸入組件。您可能從來沒時間去編寫這樣的組件,但是如果它們已經存在,那會如何呢?通過把常用功能變成商品,組件模型降低了向 Web 應用程序添加更多功能的門檻。
組件的功能通常圍繞著兩個動作:解碼和編碼數據。解碼 是把進入的請求參數轉換成組件的值的過程。編碼 是把組件的當前值轉換成對應的標記(也就是 HTML)的過程。
JSF 框架提供了兩個選項用于編碼和解碼數據。使用直接實現 方式,組件自己實現解碼和編碼。使用委托實現 方式,組件委托渲染器進行編碼和解碼。如果選擇委托實現,可以把組件與不同的渲染器關聯,會在頁面上以不同的方式渲染組件;例如多選列表框和一列復選框。
因此,JSF 組件由兩部分構成:組件和渲染器。JSF 組件 類定義 UI 組件的狀態和行為;渲染器 定義如何從請求讀取組件、如何顯示組件 —— 通常通過 HTML 渲染。渲染器把組件的值轉換成適當的標記。事件排隊和性能驗證發生在組件內部。
在圖 1 中可以看到數據編碼和解碼出現在 JSF 生命周期中的什么階段(到現在,我希望您已經熟悉 JSF 生命周期了)。 圖 1. JSF 生命周期和 JSF 組件

 |
提示!
在許多情況下,可以在保持組件本身不變的情況下,通過改變渲染而簡化開發過程。在這些情況下,可以編寫定制渲染器而不是定制組件。 | |
更多組件概念
所有 JSF 組件的基類是 UIComponent 。在開發自己的組件時,需要繼承 UIComponentBase ,它擴展了 UIComponent 并提供了 UIComponent 中所有抽象方法的默認實現。
組件擁有雙親和標識符。每個組件都關聯著一個組件類型,組件類型用于在 face 的上下文配置文件(faces-config.xml)中登記組件??梢杂?JSF-EL (表達式語言)把 JSF 組件綁定到受管理的 bean 屬性??梢园驯磉_式關聯到組件上的任何屬性,這樣就允許用 JSF-EL 設置組件的屬性值。在創建使用 JSF-EL 綁定的組件屬性時,需要創建值綁定表達式。在調用綁定屬性的 getter 方法時,除非 setter 方法已經設置了值,否則 getter 方法必須用值綁定獲得值。
組件可以作為 ValueHolder 或 EditableValueHolder 。ValueHolder 與一個或多個 Validator 和 Converter 相關聯;所以 JSF UI 組件也與 Validator 和 Converter 關聯(請參閱 參考資料 獲得更多關于 JSF 驗證和轉換的內容。)
像表單字段組件這樣的組件擁有一個 ValueBinding ,它必須綁定到 JavaBean 的讀寫屬性。組件可以調用 getParent 方法訪問它們的雙親,也可以調用 getChildren 方法訪問它們的子女。組件也可以有 facet 組件,facet 組件是當前組件的子組件,可以調用 getFacets 方法訪問它,這個方法返回一個映射。Facets 是著名的子組件。
這里描述的許多組件的概念將會是接下來展示的示例的一部分,所以請記住它們!
JSF 樣式的 Hello World!
我們用一個又好又容易的示例來開始 JSF 組件的開發:我將展示如何渲染 Label 標記(示例:<label>Form Test</label> )。
下面是我要采取的步驟:
- 擴展 UIComponent
- 創建一個類,擴展
UIComponent
- 保存組件狀態
- 用 faces-config.xml 登記組件
- 定義渲染器或者內聯地實現它
- 覆蓋 encode
- 覆蓋 decode
- 用 faces-config.xml 登記渲染器
- 創建定制標記,繼承 UIComponentTag
- 返回渲染器類型
- 返回組件類型
- 設置可能使用 JSF 表達式的屬性
Label 示例將演示 JSF 組件開發的以下方面:
- 創建組件
- 直接實現渲染器
- 編碼輸出
- 把定制標記與組件關聯
返回 圖 1,可以看到在這個示例中會有兩個生命周期屬性在活動。它們是 Apply Request Value 和 Render Response。
在圖 2 中,可以看到在 JSP 中如何使用 Label 標記的(<label>Form Test</label> )。 圖 2. 在 JSP 中使用 JSF 標記

第 1 步:擴展 UIComponent
第一步是創建一個組件,繼承 UIOutput ,后者是 UIComponent 的子類。 除了繼承這個類之外,我還添加了組件將會顯示的 label 屬性,如清單 1 所示: 清單 1. 繼承 UIComponent 并添加 label
import java.io.IOException;
import javax.faces.component.UIOutput;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
public class LabelComponent extends UIOutput{
private String label;
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
...
|
接下來要做的是保存組件狀態。JSF 通常通過會話、隱藏表單字段、cookies 等進行實際的存儲和狀態管理。(這通常是用戶配置的設置)。要保存組件狀態,需要覆蓋組件的 saveState 和 restoreState 方法,如清單 2 所示: 清單 2. 保存組件狀態
@Override
public Object saveState(FacesContext context) {
Object values[] = new Object[2];
values[0] = super.saveState(context);
values[1] = label;
return ((Object) (values));
}
@Override
public void restoreState(FacesContext context, Object state) {
Object values[] = (Object[])state;
super.restoreState(context, values[0]);
label = (String)values[1];
}
|
可以注意到,我使用的是 JDK 1.5。我對編譯器進行了設置,所以我必須指定 override 注釋,以便指明哪些方法要覆蓋基類的方法。這樣做可以更容易地標識出 JSF 的鉤子在哪。
創建組件的最后一步是用 faces-config.xml 登記它,如下所示:
<faces-config>
<component>
<component-type>simple.Label</component-type>
<component-class>
arcmind.simple.LabelComponent
</component-class>
</component>
...
|
第 2 步:定義渲染器
下面要做的是內聯地定義渲染器的功能。稍后我會介紹如何創建獨立的渲染器?,F在,先從編碼 Label 組件的輸出、顯示 label 開始,如清單 3 所示: 清單 3. 編碼組件的輸出
public class LabelComponent extends UIOutput{
...
public void encodeBegin(FacesContext context)
throws IOException {
ResponseWriter writer =
context.getResponseWriter();
writer.startElement("label", this);
writer.write(label);
writer.endElement("label");
writer.flush();
}
...
}
|
注意,響應寫入器(javax.faces.context.ResponseWriter )可以容易地處理 HTML 這樣的標記語言。清單 3 的代碼輸出 <label> 元素體內的 label 的值。
下面顯示的 family 屬性用來把 Label 組件與渲染器關聯。雖然目前 Label 組件還不需要這個屬性(因為還沒有獨立的渲染器),但是在這篇文章后面,在介紹如何創建獨立渲染器的時候,會需要它。
public class LabelComponent extends UIOutput{
...
public String getFamily(){
return "simple.Label";
}
...
}
|
插曲:研究 JSF-RI
如果正在使用來自 Sun Microsystems 的 JSF 參考實現(不是 MyFaces 實現),那么就不得不在組件創建代碼中添加下面一段:
public void encodeEnd(FacesContext context)
throws IOException {
return;
}
public void decode(FacesContext context) {
return;
}
|
Sun 的 JSF RI 期望,在組件沒有渲染器的時候,渲染器會發送一個空指針異常。MyFaces 實現不要求處理這個需求,但是在代碼中包含以上方法依然是個好主意,這樣組件既可以在 MyFaces 環境中工作也可以在 JSF RI 環境中工作了。
 |
MyFaces 更好!
如果正在使用 Sun JSF RI 或其他替代品,那么請幫自己一個忙,轉到 MyFaces。雖然 MyFaces 不總是 更好的實現,但是目前它是。它的錯誤消息要比 Sun JSF RI 的好,而這個框架相比之下更嚴格。 | |
第 3 步:創建定制標記
JSF 組件不是天生綁定到 JSP 上的。要連接起 JSP 世界和 JSF 世界,需要能夠返回組件類型的定制標記(然后在 faces-context 文件中登記)和渲染器,如圖 3 所示。 圖 3. 連接 JSF 和 JSP

注意,由于沒有獨立的渲染器,所以可以給 getRendererType() 返回 null 值。還請注意,必須已經把 label 屬性的值從定制標記設置到組件上,如下所示:
[LabelTag.java]
public class LabelTag extends UIComponentTag {
…
protected void setProperties(UIComponent component) {
/* you have to call the super class */
super.setProperties(component);
((LabelComponent)component).setLabel(label);
}
|
記住,Tag 設置從 JSP 到 Label 組件的綁定,如圖 4 所示。 圖 4. 綁定 JSF 和 JSP

現在要做的全部工作就是創建一個 TLD(標記庫描述符)文件,以登記定制標記,如清單 4 所示: 清單 4. 登記定制標記
[arcmind.tld]
<taglib>
<tlib-version>0.03</tlib-version>
<jsp-version>1.2</jsp-version>
<short-name>arcmind</short-name>
<uri>http://arcmind.com/jsf/component/tags</uri>
<description>ArcMind tags</description>
<tag>
<name>slabel</name>
<tag-class>arcmind.simple.LabelTag</tag-class>
<attribute>
<name>label</name>
<description>The value of the label</description>
</attribute>
</tag>
...
|
一旦定義了 TLD 文件,就可以開始在 JSP 中使用標記了,如下面示例所示:
[test.jsp]
<%@ taglib prefix="arcmind"
uri="http://arcmind.com/jsf/component/tags" %>
...
<arcmind:slabel label="Form Test"/>
|
現在就可以了 —— 開發一個簡單的 JSP 組件不需要更多了。但是如果想創建稍微復雜一些的組件,針對更復雜的使用場景時該怎么辦?請繼續往下看。
復合組件
在下一個示例中,我將介紹如何創建這樣一個組件(和標記),它可以記住最后一個人離開的位置。Field 組件把多個組件的工作組合到一個組件中。復合組件是 JSF 組件開發的重點,會節約大量時間!
Field 組件把標簽、文本輸入和消息功能組合到一個組件。Field 的文本輸入功能允許用戶輸入文本。如果有問題(例如輸入不正確),它的標簽功能會顯示紅色,還會顯示星號(*)表示必需的字段。它的消息功能允許它在必要的時候寫出出錯消息。
Field 組件示例演示了以下內容:
- UIInput 組件
- 處理值綁定和組件屬性
- 解碼來自請求參數的值
- 處理出錯消息
與 Label 組件不同,Field 組件使用獨立渲染器。如果為一個基于 HTML 的應用程序開發組件,那么不要費力使用獨立渲染器。這么做是額外的無用功。如果正在開發許多 JSF 組件,打算賣給客戶,而針對的客戶又不止一個,那么就需要獨立的渲染器了。簡而言之,渲染器適用于商業框架的開發人員,不適用于開發內部 Web 應用程序的應用程序開發人員。
了解代碼
由于我已經介紹了創建組件、定義渲染器以及創建定制標記的基本步驟,所以這次我讓代碼自己說話,我只點出幾個重要的細節。在清單 5 中,可以看到在典型的應用程序示例中如何使用 Field 標記的: 清單 5. Field 標記
<f:view>
<h2>CD Form</h2>
<h:form id="cdForm">
<h:inputHidden id="rowIndex" value="#{CDManagerBean.rowIndex}" />
<arcmind:field id="title"
value="#{CDManagerBean.title}"
label="Title:"
errorStyleClass="errorText"
required="true" /> <br />
<arcmind:field id="artist"
value="#{CDManagerBean.artist}"
label="Artist:"
errorStyleClass="errorText"
required="true" /> <br />
<arcmind:field id="price"
value="#{CDManagerBean.price}"
label="CD Price:"
errorStyleClass="errorText"
required="true">
<f:validateDoubleRange maximum="1000.0" minimum="1.0"/>
</arcmind:field>
|
以上標記輸出以下 HTML:
<label style="" class="errorText">Artist*</label>
<input type="text" id="cdForm:artist "
name=" cdForm:artist " />
Artist is blank, it must contain characters
|
圖 5 顯示了瀏覽器中這些內容可能顯示的效果。 圖 5. Field 組件

清單 6 顯示了創建 Field 組件的代碼。因為這個組件負責輸入文本而不僅僅是輸出它(像 Label 那樣),所以要從繼承 UIInput 開始,而不是從繼承 UIOutput 開始。 清單 6. Field 繼承 UIInput
package com.arcmind.jsfquickstart;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
/**
* @author Richard Hightower
*
*/
public class FieldComponent extends UIInput {
private String label;
@Override
public Object saveState(FacesContext context) {
Object values[] = new Object[2];
values[0] = super.saveState(context);
values[1] = label;
return ((Object) (values));
}
@Override
public void restoreState(FacesContext context, Object state) {
Object values[] = (Object[])state;
super.restoreState(context, values[0]);
label = (String)values[1];
}
public FieldComponent (){
this.setRendererType("arcmind.Field");
}
/**
* @return Returns the label.
*/
public String getLabel() {
return label;
}
/**
* @param label
* The label to set.
*/
public void setLabel(String label) {
this.label = label;
}
@Override
public String getFamily() {
return "arcmind.Field";
}
public boolean isError() {
return !this.isValid();
}
}
|
可以注意到,代表片段中遺漏了編碼方法。這是因為編碼和解碼發生在獨立的渲染器中。我稍后會介紹它。
值綁定和組件屬性
雖然 Label 組件只有一個屬性(JSP 屬性),可是 Field 組件卻有多個屬性,即 label 、errorStyle 、errorStyleClass 和 value 。label 和 value 屬性位于 Field 組件的核心,而 errorStyle 和 errorStyleClass 是特定于 HTML 的。因為這些屬性是特定于 HTML 的,所以不需要讓它們作為 Field 組件的屬性;相反,只是把它們作為組件屬性進行傳遞,只有渲染器知道這些屬性。
像使用 Label 組件時一樣,需要用定制標記把 Field 組件綁定到 JSP,如清單 7 所示: 清單 7. 為 FieldComponent 創建定制標記
/*
* Created on Jul 19, 2004
*
*/
package com.arcmind.jsfquickstart;
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;
/**
* @author Richard Hightower
*
*/
public class FieldTag extends UIComponentTag {
private String label;
private String errorStyleClass="";
private String errorStyle="";
private boolean required;
private String value="";
/**
* @return Returns the label.
*/
public String getLabel() {
return label;
}
/**
* @param label The label to set.
*/
public void setLabel(String label) {
this.label = label;
}
/**
* @see javax.faces.webapp.UIComponentTag#setProperties
* (javax.faces.component.UIComponent)
*/
@Override
protected void setProperties(UIComponent component) {
/* You have to call the super class */
super.setProperties(component);
((FieldComponent)component).setLabel(label);
component.getAttributes().put("errorStyleClass",
errorStyleClass);
component.getAttributes().put("errorStyle",errorStyle);
((FieldComponent)component).setRequired(required);
FacesContext context = FacesContext.getCurrentInstance();
Application application = context.getApplication();
ValueBinding binding = application.createValueBinding(value);
component.setValueBinding("value", binding);
}
/**
* @see javax.faces.webapp.UIComponentTag#getComponentType()
*/
@Override
public String getComponentType() {
return "arcmind.Field";
}
/**
* @see javax.faces.webapp.UIComponentTag#getRendererType()
*/
@Override
public String getRendererType() {
return "arcmind.Field";
}
/**
* @return Returns the errorStyleClass.
*/
public String getErrorStyleClass() {
return errorStyleClass;
}
/**
* @param errorStyleClass The errorStyleClass to set.
*/
public void setErrorStyleClass(String errorStyleClass) {
this.errorStyleClass = errorStyleClass;
}
/**
* @return Returns the errorStyle.
*/
public String getErrorStyle() {
return errorStyle;
}
/**
* @param errorStyle The errorStyle to set.
*/
public void setErrorStyle(String errorStyle) {
this.errorStyle = errorStyle;
}
/**
* @return Returns the required.
*/
public boolean isRequired() {
return required;
}
/**
* @param required The required to set.
*/
public void setRequired(boolean required) {
this.required = required;
}
/**
* @return Returns the value.
*/
public String getValue() {
return value;
}
/**
* @param value The value to set.
*/
public void setValue(String value) {
this.value = value;
}
}
|
從概念上說,在上面的代碼和 Label 組件之間找不出太大區別。但是,在這個示例中,setProperties 方法有些不同:
protected void setProperties(UIComponent component) {
/* You have to call the super class */
super.setProperties(component);
((FieldComponent)component).setLabel(label);
component.getAttributes().put("errorStyleClass",
errorStyleClass);
component.getAttributes().put("errorStyle",errorStyle);
((FieldComponent)component).setRequired(required);
|
雖然 label 屬性傳遞時的方式與前面的示例相同,但是 errorStyleClass 和 errorStyle 屬性不是這樣傳遞的。相反,它們被添加到 JSF 組件的屬性映射 中。Renderer 類會使用屬性映射去渲染類和樣式屬性。這個設置允許特定于 HTML 的代碼從組件脫離。
這個修訂后的 setProperties 方法實際的值綁定代碼也有些不同,如下所示。
protected void setProperties(UIComponent component) {
...
FacesContext context = FacesContext.getCurrentInstance();
Application application = context.getApplication();
ValueBinding binding = application.createValueBinding(value);
component.setValueBinding("value", binding);
|
這個代碼允許 Field 組件的 value 屬性綁定到后臺 bean。出于示例的原因,我把 CDManagerBean 的 title 屬性綁定到 Field 組件,像下面這樣:value="#{CDManagerBean.title} 。值綁定是用 Application 對象創建的。Application 對象是創建值綁定的工廠。這個組件擁有保存值綁定的特殊方法,即 setValueBinding ;可以有不止一個值綁定。
獨立渲染器
最后介紹渲染器,但并不是說它不重要。獨立渲染器必須考慮的主要問題是解碼(輸入) 和編碼(輸出)。Field 組件做的編碼比解碼多得多,所以它的渲染器有許多編碼方法,而只有一個解碼方法。在清單 8 中,可以看到 Field 組件的渲染器: 清單 8. FieldRenderer 擴展自 Renderer
package com.arcmind.jsfquickstart;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;
import javax.faces.el.ValueBinding;
import javax.faces.render.Renderer;
/**
* @author Richard Hightower
*
*/
public class FieldRenderer extends Renderer {
@Override
public Object getConvertedValue(FacesContext facesContext, UIComponent component,
Object submittedValue) throws ConverterException {
//Try to find out by value binding
ValueBinding valueBinding = component.getValueBinding("value");
if (valueBinding == null) return null;
Class valueType = valueBinding.getType(facesContext);
if (valueType == null) return null;
if (String.class.equals(valueType)) return submittedValue;
if (Object.class.equals(valueType)) return submittedValue;
Converter converter = ((UIInput) component).getConverter();
converter = facesContext.getApplication().createConverter(valueType);
if (converter != null ) {
return converter.getAsObject(facesContext, component, (String) submittedValue);
}else {
return submittedValue;
}
}
@Override
public void decode(FacesContext context, UIComponent component) {
/* Grab the request map from the external context */
Map requestMap = context.getExternalContext().getRequestParameterMap();
/* Get client ID, use client ID to grab value from parameters */
String clientId = component.getClientId(context);
String value = (String) requestMap.get(clientId);
FieldComponent fieldComponent = (FieldComponent)component;
/* Set the submitted value */
((UIInput)component).setSubmittedValue(value);
}
@Override
public void encodeBegin(FacesContext context, UIComponent component)
throws IOException {
FieldComponent fieldComponent = (FieldComponent) component;
ResponseWriter writer = context.getResponseWriter();
encodeLabel(writer,fieldComponent);
encodeInput(writer,fieldComponent);
encodeMessage(context, writer, fieldComponent);
writer.flush();
}
private void encodeMessage(FacesContext context, ResponseWriter writer,
FieldComponent fieldComponent) throws IOException {
Iterator iter = context.getMessages(fieldComponent.getClientId(context));
while (iter.hasNext()){
FacesMessage message = (FacesMessage) iter.next();
writer.write(message.getDetail());
}
}
private void encodeLabel(ResponseWriter writer, FieldComponent
fieldComponent) throws IOException{
writer.startElement("label", fieldComponent);
if (fieldComponent.isError()) {
String errorStyleClass = (String) fieldComponent.getAttributes().get("errorStyleClass");
String errorStyle = (String) fieldComponent.getAttributes().get("errorStyle");
writer.writeAttribute("style", errorStyle, "style");
writer.writeAttribute("class", errorStyleClass, "class");
}
writer.write("" + fieldComponent.getLabel());
if (fieldComponent.isRequired()) {
writer.write("*");
}
writer.endElement("label");
}
private void encodeInput(ResponseWriter writer, FieldComponent
fieldComponent) throws IOException{
FacesContext currentInstance = FacesContext.getCurrentInstance();
writer.startElement("input", fieldComponent);
writer.writeAttribute("type", "text", "type");
writer.writeAttribute("id", fieldComponent.getClientId(currentInstance), "id");
writer.writeAttribute("name", fieldComponent.getClientId(currentInstance), "name");
if(fieldComponent.getValue()!=null)
writer.writeAttribute("value", fieldComponent.getValue().toString(), "value");
writer.endElement("input");
}
}
|
編碼和解碼
正如前面提到的,渲染器做的主要工作就是解碼輸入和編碼輸出。我先從解碼開始,因為它是最容易的。 FieldRenderer 的 decode 方法如下所示:
@Override
public void decode(FacesContext context, UIComponent component) {
/* Grab the request map from the external context */
Map requestMap = context.getExternalContext().getRequestParameterMap();
/* Get client ID, use client ID to grab value from parameters */
String clientId = component.getClientId(context);
String value = (String) requestMap.get(clientId);
FieldComponent fieldComponent = (FieldComponent)component;
/* Set the submitted value */
((UIInput)component).setSubmittedValue(value);
}
|
Label 組件不需要進行解碼,因為它是一個 UIOutput 組件。Field 組件是一個 UIInput 組件,這意味著它接受輸入,所以 必須 進行解碼。decode 方法可以從會話、cookie、頭、請求等處讀取值。在大多數請問下,decode 方法只是像上面那樣從請求參數讀取值。Field 渲染器 的 decode 方法從組件得到 clientId ,以標識要查找的請求參數。給定組件容器的路徑,clientId 被計算成為組件的全限定名稱。而且,因為示例組件在表單中(是個容器),所以它的 clientid 應當是 nameOfForm:nameOfComponent 這樣的,或者是示例中的 cdForm:artist、cdForm:price、cdForm:title 。decode 方法的最后一步是把提交的值保存到組件(稍后會轉換并驗證它,請參閱 參考資料 獲取更多關于驗證和轉換的內容)。
編碼方法沒什么驚訝的。它們與 Label 組件中看到的類似。第一個方法 encodeBegin ,委托給三個幫助器方法 encodeLabel 、encodeInput 和 encodeMessage ,如下所示:
@Override
public void encodeBegin(FacesContext context, UIComponent component)
throws IOException {
FieldComponent fieldComponent = (FieldComponent) component;
ResponseWriter writer = context.getResponseWriter();
encodeLabel(writer,fieldComponent);
encodeInput(writer,fieldComponent);
encodeMessage(context, writer, fieldComponent);
writer.flush();
}
|
encodeLabel 方法負責在出錯的時候,把標簽的顏色改成紅色(或者在樣式表中指定的其他什么顏色),并用星號 (*) 標出必需的字段,如下所示:
private void encodeLabel(ResponseWriter writer, FieldComponent fieldComponent) throws IOException{
writer.startElement("label", fieldComponent);
if (fieldComponent.isError()) {
String errorStyleClass = (String) fieldComponent.getAttributes().get("errorStyleClass");
String errorStyle = (String) fieldComponent.getAttributes().get("errorStyle");
writer.writeAttribute("style", errorStyle, "style");
writer.writeAttribute("class", errorStyleClass, "class");
}
writer.write("" + fieldComponent.getLabel());
if (fieldComponent.isRequired()) {
writer.write("*");
}
writer.endElement("label");
}
|
首先,encodeLabel 方法檢查是否有錯誤,如果有就輸出 errorStyle 和 errorStyleClass (更好的版本是只有在它們不為空的時候才輸出 —— 但是我把它留給您做練習?。?。然后幫助器方法會檢查組件是不是必需的字段,如果是,就輸出星號。encodeMessages 和 encodeInput 方法做的就是這件事,即輸出出錯消息并為 Field 組件生成 HTML 輸入的文本字段。
注意,神秘方法!
您可能已經注意到,有一個方法我還沒有介紹。這個方法就是這個類中的“黑馬”方法。如果您閱讀 Renderer (所有渲染器都要擴展的抽象類)的 javadoc,您可能會感覺到這樣的方法是不需要的,現有的就足夠了:這就是我最開始時想的。但是,您和我一樣,都錯了!
實際上,基類 Renderer 并不 自動調用 Renderer 子類的相關轉換器 —— 即使 Renderer 的 javadoc 和 JSF 規范建議它這樣做,它也沒做。MyFaces 和 JSF RI 擁有為它們的渲染器執行這個魔術的類(特定于它們的實現),但是在核心 JSF API 中并沒有涉及這項功能。
相反,需要使用方法 getConvertedValues 鎖定相關的轉換器并調用它。清單 9 顯示的方法根據值綁定的類型找到正確的轉換器: 清單 9. getConvertedValues 方法
@Override
public Object getConvertedValue(FacesContext facesContext,
UIComponent component, Object submittedValue) throws ConverterException {
//Try to find out by value binding
ValueBinding valueBinding = component.getValueBinding("value");
if (valueBinding == null) return null;
Class valueType = valueBinding.getType(facesContext);
if (valueType == null) return null;
if (String.class.equals(valueType)) return submittedValue;
if (Object.class.equals(valueType)) return submittedValue;
Converter converter = ((UIInput) component).getConverter();
converter = facesContext.getApplication().createConverter(valueType);
if (converter != null ) {
return converter.getAsObject(facesContext, component, (String) submittedValue);
}else {
return submittedValue;
}
}
|
清單 9 的代碼添加了 Render javadoc 和 JSF 規范都讓您相信應當是自動執行的功能,而實際上并不是。另一方面,請注意如果沒有 獨立的 Renderer ,就不需要 以上(getConvertedValues )方法。UIComponentBase 類(Field 組件的超類)在直接渲染器的情況下提供了這個功能。請接受我的建議,只在特別想嘗試或者在編寫商業框架的時候,才考慮采用渲染器。在其他情況下,它們不值得額外的付出。
如果想知道如何把組件和渲染器關聯,那么只要看看圖 6 即可。 圖 6. 把渲染器映射到組件

定制標記有兩個方法,分別返回組件類型和渲染器類型。這些方法用于查找配置在 faces-config.xml 中的正確的渲染器和組件。請注意(雖然圖中沒有)組件必須返回正確的 family 類型。
結束語
通過這些內容,您已經切實地了解了 JSF 組件開發的核心。當然,在這個領域還有許多其他主題需要涉及 —— 包括發出組件事件、國際化組件、創建 UICommand 樣式的組件,以及更多。請參閱 參考資料 獲得 JSF 的閱讀列表!
在編寫這篇文章的過程中,我遇到了 Renderer 的一個技術障礙,它使我發現了 getConvertedValues 方法的工作方式。盡管我以前遇到過 Converter 問題并處理過它,但是那時我是在一個緊張的(生產)日程中做這件事的。在生產工作中進行的研究,不必像在 how-to 文章中做得那么詳細;所以這一次,我必須不僅學習如何修補問題,還要學習弄清如何 做對。通過這整個過程,我最終在非常深的層次上學會并體驗了 JSF 組件處理工作的方式。所以,有時繞點彎路會看到優美的風景。
我希望在這個由四部分組成的系列中,您已經學到了關于使用 JSF 的優勢的充足知識,以及它如何工作的基礎知識,還希望您會喜歡進一步深入這項技術。而且當您有時可能迷失方向的時候,請不要陷入 FUD。相反,請記住我說過的:彎路會看到優美的風景,請繼續前行。 |