组件模型的关键考验就是:能否从第三方供应商购买组件,并把它们插入应用程序?与
可购买可视 Swing 组件一样,也可以购买 Java ServerFaces (JSF)
组件!需要一个好玩的日历?可以在开源实现和商业组件之间选择。可以选择购买一个,而不是自行开发复杂的基于 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 步:定义渲染器
下面要做的是内联地定义渲染器的功能。稍后我会介绍如何创建独立的渲染器。现在,先从编码 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 > <h:inputHidden value="#{CDManagerBean.rowIndex}" /> <arcmind:field value="#{CDManagerBean.title}" label="Title:" errorStyleClass="errorText" required="true" /> <br /> <arcmind:field value="#{CDManagerBean.artist}" label="Artist:" errorStyleClass="errorText" required="true" /> <br /> <arcmind:field value="#{CDManagerBean.price}" label="CD Price:" errorStyleClass="errorText" required="true"> <f:validateDoubleRange maximum="1000.0" minimum="1.0"/> </arcmind:field>
|
以上标记输出以下 HTML:
<label >Artist*</label><input type="text" " 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 error; 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 方法如下所示:
@Overridepublic 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
,如下所示:
@Overridepublic 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
样式的组件,以及更多。