JSF 组件开发

Rick Hightower , CTO, ArcMind

组件模型的关键考验就是:能否从第三方供应商购买组件,并把它们插入应用程序?与可购买可视 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.jarjsp-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 组件和 JSF 生命周期
 

更多组件概念

所有 JSF 组件的基类是 UIComponent。在开发自己的组件时,需要继承 UIComponentBase,它扩展了 UIComponent 并提供了 UIComponent 中所有抽象方法的默认实现。

组件拥有双亲和标识符。每个组件都关联着一个组件类型,组件类型用于在 face 的上下文配置文件(faces-config.xml)中登记组件。可以用 JSF-EL (表达式语言)把 JSF 组件绑定到受管理的 bean 属性。可以把表达式关联到组件上的任何属性,这样就允许用 JSF-EL 设置组件的属性值。在创建使用 JSF-EL 绑定的组件属性时,需要创建值绑定表达式。在调用绑定属性的 getter 方法时,除非 setter 方法已经设置了值,否则 getter 方法必须用值绑定获得值。

组件可以作为 ValueHolderEditableValueHolderValueHolder 与一个或多个 ValidatorConverter 相关联;所以 JSF UI 组件也与 ValidatorConverter 关联。

像表单字段组件这样的组件拥有一个 ValueBinding,它必须绑定到 JavaBean 的读写属性。组件可以调用 getParent 方法访问它们的双亲,也可以调用 getChildren 方法访问它们的子女。组件也可以有 facet 组件,facet 组件是当前组件的子组件,可以调用 getFacets 方法访问它,这个方法返回一个映射。Facets 是著名的子组件。

这里描述的许多组件的概念将会是接下来展示的示例的一部分,所以请记住它们!

JSF 样式的 Hello World!

我们用一个又好又容易的示例来开始 JSF 组件的开发:我将展示如何渲染 Label 标记(示例:<label>Form Test</label>)。

下面是我要采取的步骤:

  1. 扩展 UIComponent
    • 创建一个类,扩展 UIComponent
    • 保存组件状态
    • 用 faces-config.xml 登记组件
  2. 定义渲染器或者内联地实现它
    • 覆盖 encode
    • 覆盖 decode
    • 用 faces-config.xml 登记渲染器
  3. 创建定制标记,继承 UIComponentTag
    • 返回渲染器类型
    • 返回组件类型
    • 设置可能使用 JSF 表达式的属性

Label 示例将演示 JSF 组件开发的以下方面:

  • 创建组件
  • 直接实现渲染器
  • 编码输出
  • 把定制标记与组件关联

返回 图 1,可以看到在这个示例中会有两个生命周期属性在活动。它们是 Apply Request ValueRender Response

在图 2 中,可以看到在 JSP 中如何使用 Label 标记的(<label>Form Test</label>)。


图 2. 在 JSP 中使用 JSF 标记
在 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 等进行实际的存储和状态管理。(这通常是用户配置的设置)。要保存组件状态,需要覆盖组件的 saveStaterestoreState 方法,如清单 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 环境中工作了。

 

第 3 步:创建定制标记

JSF 组件不是天生绑定到 JSP 上的。要连接起 JSP 世界和 JSF 世界,需要能够返回组件类型的定制标记(然后在 faces-context 文件中登记)和渲染器,如图 3 所示。


图 3. 连接 JSF 和 JSP
连接 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
绑定 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 组件
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 组件却有多个属性,即 labelerrorStyleerrorStyleClassvaluelabelvalue 属性位于 Field 组件的核心,而 errorStyleerrorStyleClass 是特定于 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 属性传递时的方式与前面的示例相同,但是 errorStyleClasserrorStyle 属性不是这样传递的。相反,它们被添加到 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,委托给三个帮助器方法 encodeLabelencodeInputencodeMessage,如下所示:


            @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 方法检查是否有错误,如果有就输出 errorStyleerrorStyleClass(更好的版本是只有在它们不为空的时候才输出 —— 但是我把它留给您做练习!)。然后帮助器方法会检查组件是不是必需的字段,如果是,就输出星号。encodeMessagesencodeInput 方法做的就是这件事,即输出出错消息并为 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 样式的组件,以及更多。

在编写这篇文章的过程中,我遇到了 Renderer 的一个技术障碍,它使我发现了 getConvertedValues 方法的工作方式。尽管我以前遇到过 Converter 问题并处理过它,但是那时我是在一个紧张的(生产)日程中做这件事的。在生产工作中进行的研究,不必像在 how-to 文章中做得那么详细;所以这一次,我必须不仅学习如何修补问题,还要学习弄清如何 做对。通过这整个过程,我最终在非常深的层次上学会并体验了 JSF 组件处理工作的方式。所以,有时绕点弯路会看到优美的风景。

我希望在这个由四部分组成的系列中,您已经学到了关于使用 JSF 的优势的充足知识,以及它如何工作的基础知识,还希望您会喜欢进一步深入这项技术。而且当您有时可能迷失方向的时候,请不要陷入 FUD。相反,请记住我说过的:弯路会看到优美的风景,请继续前行。


参考资料

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • 请访问 JSF 主页 下载 JavaServer Faces 的 API、定制标记库和相关文档。
  • 请参阅 MyFaces 主页,学习更多关于 MyFaces 的内容。

关于作者

 

Rick Hightower 是 ArcMind 公司的 CTO。他是 Java Tools for Extreme Programming 一书的合著者之一,这是一本关于在 J2EE 开发中采用极限编程的书;他还与人合著了 Professional Struts 一书。

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值