JavaServer Faces(JSF)提供可扩展的组件模型,开发人员可以创建可重用的组件,使用这些自定义组件提高开发效率和降低开发成本。虽然对于定制和重用而言 JSF 的组件模型非常强大,但是开发人员普遍认为开发 JSF 自定义组件并不容易,因为通常至少需要熟悉 JSF encode/decode 和 state holder 的内部机制并覆盖相应的方法(参考“怀疑论者的 JSF: JSF 组件开发”所述),如 encodeBegine()、decode()、saveState() 和 restoreState() 等,对于开发复杂的自定义组件,甚至需要深入理解更多的接口,如 NamingContainer、StateHolder、EditableValueHolder 和 ActionSource 等接口。
然而,重用 JSF 标准组件的功能可以极大地简化自定义组件的开发,尤其对于自定义复合组件更是如此。在大部分情况下,我们可以重用 JSF 框架已经提供的标准渲染器、状态管理、事件监听器、转换器和验证器。已有的文章或书籍对如何重用这些标准功能涉及很少,本文基于重用的策略提出快速开发 JSF 自定义复合组件的原则和技巧。
本文首先总结了 JSF 组件开发的通用原则,然后通过一个例子(Value Scroller 自定义复合组件)的开发详解说明了哪些标准功能可以重以及如何重用,以达到简化 JSF 自定义复合组件开发的目的。
原则与技巧
开发 JSF 自定义复合组件主要有两个原则,一方面强调重用已有的标准组件;另一方面如何确保自定义组件易于重用。
- 尽可能的重用标准组件的功能和实现
传统的自定义复合组件开发建议完全覆盖实现 encode/decode 逻辑,但这样做耗费时间而且容易出错。毫无疑问,我们可以通过重用标准组件的渲染器等机制减少甚至根本不用自行编写这部分代码。另外,为了实现灵活的配置和使用,自定义复合组件通常需要提供很多属性,我们需要写很多代码来处理这些属性的读写和状态管理。实际上,我们可以简单地把自定义复合组件的属性传递给它自身包含的标准组件,由已有的标准代码去处理这些属性,而不用重复写这些代码。
- 清晰地分离组件类、标签类和模型类
JSF 的组件模型建议在组件类、标签类和模型类之间有明确清晰的责任分配,以便于重用和扩展。组件类不应该依赖于 javax.faces.component.html 包,因为组件类不仅可以用于 HTML,而且还应该可以重用于其它标记语言(如 WML)。也就是说组件类不应该直接引用 javax.faces.component.html 包内的 HTML 组件。例如,在你的组件类中创建一个 HtmlCommandButton 的实例是不可取的,你应该考虑用 javax.faces.component 包中的 UICommand 。另一方面,如果你希望你的模型类可以重用于不同的 Web 框架,那么你的模型类就不应该依赖于 JSF 的任何包,即模型类只表示业务对象而不包含任何用户界面相关的组件、数据和状态。
基于这些原则,对比传统方式和本文介绍的技巧,我们可以发现基于重用的开发策略会极大简化 JSF 自定义复合组件的编写。开发 JSF 自定义组件通常需要如下 3 个步骤(参考“怀疑论者的 JSF: JSF 组件开发”)。
- 扩展 UIComponent
传统方式:创建一个类,扩展 UIComponent,保存组件状态,在 faces-config.xml 中注册组件
重用技巧:
- 选择 UIPanel 作为布局容器,重用标准组件作为复合组件的子组件。
- 实现内部动作监听器。
- 定义渲染器或者内联实现它
传统方式:覆盖实现 encode/decode,在 faces-config.xml 中注册渲染器。
重用技巧:重用标准渲染器类型。
- 创建自定义标签,继承 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)); |
实现内部动作监听器
在 Value Scroller 中,点击增值或减值按钮,输入框内的值会随之增大或者减小。我们可以简单地在组件类 ValueScroller 中实现一个内部动作监听器,重用 UICommand 的事件处理逻辑。
清单 6. 实现 Value Scroller 动作监听器
/**
* 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 方法创建添加子组件的时候,将这个自定义动作监听器添加到增值和减值组件中去。
清单 7. 注册 Value Scroller 动作监听器
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 自定义复合组件的开发技巧在很大程度上降低了复杂度和工作量,优于传统的开发方式。
文章来源: