复合组件
在下一个示例中,我将介绍如何创建这样一个组件(和标记),它可以记住最后一个人离开的位置。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 显示了浏览器中这些内容可能显示的效果。
清单 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 即可。
定制标记有两个方法,分别返回组件类型和渲染器类型。这些方法用于查找配置在 faces-config.xml 中的正确的渲染器和组件。请注意(虽然图中没有)组件必须返回正确的 family 类型。
http://hi.baidu.com/rover828/blog/item/e28bdbc43145accf38db49ec.html
本文详细解析了JSF中的Field组件,包括其如何整合标签、文本输入和消息功能,以及如何处理值绑定、请求参数解码和出错消息。通过具体示例展示了Field组件在实际应用中的使用方式。
8万+

被折叠的 条评论
为什么被折叠?



