实现自定义组件
除了提供一组标准的组件、一个在HTML中呈现这些组件的呈现工具,以及一组相应的JSP标签之外,JavaServer Faces也让您可以创建自定义组件。下面的应用程序包含了一个JSF自定义组件的两个实例。该自定义组件用于显示两幅图像。当您单击那个组件时,它会切 换当前显示的图像。
图5a. 两个自定义组件显示了它们的原始图像。
图5b. 在用户单击左边组件后应用程序发生的变化
图5c. 在用户单击右边组件后应用程序发生的变化
图5a展示了应用程序启动后出现的画面。两个自定义组件显示了它们的原始图像。图5b展示了在用户单击左边组件后出现的画面。图5c展示在用户随后单击右边组件后出现的画面。如果您再次单击组件,组件将显示它们的原始图像。
这些自定义组件可能看起来微不足道,而且毫无用处;尽管前一种说法是正确的(故意让这些组件微不足道,从而可以说明JSF组件实现),但后 一种说法未必正确--这个组件可用作包含可点击图像的任何组件的一部分。例如,这个自定义组件可用在树形控制中,以展开和收缩树中显示的节点。
清单9列出了图5a展示的JSP页面。
清单9. /index.jsp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>Creating Custom Components with JavaServer Faces</title>
</head>
<body>
<%@ taglib uri="http://java.sun.com/j2ee/html_basic/" prefix="faces" %>
<%@ taglib uri="/WEB-INF/tlds/example.tld" prefix="sabreware" %>
<faces:usefaces>
<sabreware:toggleGraphic id='bananaKiwi'
imageOne='/graphics/banana.jpg'
imageTwo='/graphics/kiwi.jpg'/>
<sabreware:toggleGraphic id='pineappleStrawberry'
imageOne='/graphics/pineapple.jpg'
imageTwo='/graphics/strawberry.jpg'/>
</faces:usefaces>
</body>
</html>
前述的JSP页面是简单的--它使用一个自定义标签来创建自定义组件的两个实例。出于完整性考虑,清单10中列出了与<sabreware:toggleGraphic>
标签有关的标签库描述符(TLD)。
清单10. /WEB-INF/tlds/example.tld
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE taglib
PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.2//EN"
"http://java.sun.com/dtd/web-jsptaglibrary_1_2.dtd">
<taglib>
<tlib-version>1.0</tlib-version>
<jsp-version>1.2</jsp-version>
<short-name>JSF Example</short-name>
<display-name>A Simple JSF Example Tag</display-name>
<description>This library contains one simple custom JSF tag</description>
<tag>
<name>toggleGraphic</name>
<tag-class>com.sabreware.tags.ToggleGraphicTag</tag-class>
<body-content>JSP</body-content>
<description>A simple tag for a custom JSF component</description>
<attribute>
<name>imageOne</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<name>imageTwo</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<name>id</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
</taglib>
<sabreware:toggleGraphic>
标签有3个属性:一个标识符和组件的两个图像。清单11展示了标签处理程序。
清单11. /WEB-INF/classes/com/sabreware/tags/ToggleGraphicTag.java
package com.sabreware.tags;
import javax.faces.webapp.FacesTag;
import javax.faces.component.UIComponent;
import javax.faces.component.UIGraphic;
import javax.faces.context.FacesContext;
import javax.faces.event.FacesEvent;
import javax.faces.event.RequestEventHandler;
import com.sabreware.components.UIToggleGraphic;
public class ToggleGraphicTag extends FacesTag {
private String imageOne, imageTwo;
public void setImageOne(String imageOne) {
this.imageOne = imageOne;
}
public void setImageTwo(String imageTwo) {
this.imageTwo = imageTwo;
}
public void overrideProperties(UIComponent component) {
super.overrideProperties(component);
component.setAttribute("imageOne", imageOne);
component.setAttribute("imageTwo", imageTwo);
}
public UIComponent createComponent() {
UIToggleGraphic comp = new UIToggleGraphic();
comp.setURL(imageOne);
return comp;
}
public String getRendererType() {
return null;
}
}
前述的JSP自定义标签扩展了javax.faces.webapp目录中的FacesTag类。所有表示JSF组件的自定义标签要么扩展了FacesTag,要么在标签需要操纵其主体内容时扩展了FacesBodyTag。
除了将组件的图像存储为类成员变量之外,前述的JSP自定义标签也将这些图像作为该标签的组件属性存储在 overrideProperties()中。createComponent()方法是抽象的,因此必须由所有的FacesTag扩展实现。该方法创建 了一个组件并返回了到那个组件的引用。在前述的标签中,那个方法也通过调用该组件的setURL()方法来设置组件的原始图像。最后,FacesTag中 同样为抽象的getRendererType()方法返回了一个字符串常量,代表该组件的呈现程序的标识符。在这种情形中,组件不具有呈现程序。因此, getRendererType()方法返回null。
清单12展示了该自定义组件。
清单12. /WEB-INF/classes/com/sabreware/components/UIToggleGraphic.java
package com.sabreware.components;
import javax.servlet.http.HttpServletRequest;
import javax.faces.component.UIGraphic;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.event.CommandEvent;
import javax.faces.event.FacesEvent;
public class UIToggleGraphic extends UIGraphic {
// This component supports one command: click.
private static String clickCommand = "click";
// This method indicates whether this component renders itself
// or delegates rendering to a renderer.
public boolean getRendersSelf() {
return true;
}
// This method, which is called during the Render Response phase,
// generates the markup that represents the component.
public void encodeEnd(FacesContext context) throws java.io.IOException {
ResponseWriter writer = context.getResponseWriter();
HttpServletRequest request = (HttpServletRequest)
context.getServletRequest();
// Represent this component as an HTML anchor element with an
// image. When the image is clicked, the current page is reloaded
// with a request parameter named component whose value is this
// component's ID.
writer.write("<a href='?component=" + getComponentId() + "'>");
writer.write("<img border='0' src='");
writer.write(request.getContextPath() + (String)getURL() + "'/>");
writer.write("</a>");
}
// This method, which is called during the Apply Request Values phase,
// decodes request parameters.
public void decode(FacesContext context) throws java.io.IOException {
HttpServletRequest request = (HttpServletRequest)
context.getServletRequest();
// If there's a request parameter named component whose value
// matches this component's ID...
if(getComponentId().equals(request.getParameter("component"))) {
// ...enqueue a command event on the FacesContext event queue
// for this component. The processEvent method, listed below,
// processes that event.
context.addRequestEvent(this, new CommandEvent(this, clickCommand));
}
}
// This method, which is called during the Handle Request Events phase,
// processes command events that were added to the FacesContext by
// the decode method.
public boolean processEvent(FacesContext context, FacesEvent event) {
if(event instanceof CommandEvent) {
CommandEvent cmdEvent = (CommandEvent)event;
// If the event's command name equals "click"...
if(clickCommand.equals(cmdEvent.getCommandName())) {
// ...toggle the component's image.
toggleImage();
}
return false; // Go directly to render phase.
}
return true; // Process request normally.
}
// This method returns a string representing the component's type.
public String getComponentType() {
return "com.sabreware.components.UIToggleGraphic";
}
// This private method toggles the component's image.
private void toggleImage() {
String imageOne = (String)getAttribute("imageOne");
String imageTwo = (String)getAttribute("imageTwo");
String currentImage = (String)getAttribute("image");
if(imageTwo.equals(currentImage))
setAttribute("image", imageOne);
else
setAttribute("image", imageTwo);
// The setURL() method is defined in the superclass (UIGraphic).
setURL((String)getAttribute("image"));
}
}
前述的自定义组件扩展了UIGraphic类--一个用于显示不能由用户操纵的图像的JSF标准组件。UIToggleGraphic类重载了getRendersSelf,以返回true。这表明组件自己处理呈现和事件处理。
呈现是在encodeEnd()方法中发生的,这个方法生成了组件的标记--一个HTML anchor元素。当单击那个锚点时,就会重新加载当前页,并创建一个名为component的请求参数,其值为该组件的ID。 encodeEnd()方法是在JSF生命周期的Render Response阶段由JSF实现调用的。
decode()方法是在Apply Request Values阶段由JSF实现调用的。该方法会查找名为component的参数,如果请求参数值匹配组件的ID,decode()方法就把一个请求事件添加到JSF的上下文中。
事件处理是在 processEvent()方法中发生的,该方法是在Handle Request Events阶段由JSF实现调用的。这个方法检查是否单击了事件的名称(由 decode()方法生成的事件)。如果是, processEvent()方法就调用组件的 toggleImage()方法,以切换组件显示的图像。随后在组件呈现时,它就会显示新选中的图像。
前述的例子展示了如何可以使用JavaServer Faces实现自定义组件。但在清单12中,组件是自己处理呈现和事件处理的,因此它没有达到最大的灵活性。例如,组件不能安装其他的呈现程序,因此也不 能生成除HTML之外的标记。尽管还要多做一点工作,但把组件与呈现和事件处理分开将大大增加这些组件的灵活性(正如下一节所讨论的那样)。
分离呈现和事件处理
本节描述了前一节所讨论的,即如何把自定义组件与呈现和事件处理分离。把组件与它们的呈现和事件处理分离将增加重用性和灵活性,让您可以将其他的呈现程序与一个组件关联起来,以使用其他标记语言来生成这个组件的表示。
在清单13中,我已经重写了清单12中的组件。
清单13. /WEB-INF/classes/com/sabreware/components/UIToggleGraphic.java
package com.sabreware.components;
import javax.faces.component.UIGraphic;
import com.sabreware.eventHandlers.ToggleGraphicEventHandler;
public class UIToggleGraphic extends UIGraphic {
// This component supports one command: click
private static String clickCommand = "click";
public UIToggleGraphic() {
addRequestEventHandler(new ToggleGraphicEventHandler());
}
public boolean getRendersSelf() {
return false;
}
public String getComponentType() {
return "com.sabreware.components.UIToggleGraphic";
}
public String getRendererType() {
return "ToggleGraphicHTMLRenderer";
}
public String getClickCommandName() {
return clickCommand;
}
// This private method toggles the component's image.
public void toggleImage() {
String imageOne = (String)getAttribute("imageOne");
String imageTwo = (String)getAttribute("imageTwo");
String currentImage = (String)getAttribute("image");
if(imageTwo.equals(currentImage))
setAttribute("image", imageOne);
else
setAttribute("image", imageTwo);
// The setURL method is defined in the superclass (UIGraphic)
setURL((String)getAttribute("image"));
}
}
前述的组件将呈现和事件处理委托给其他对象。它通过重写getRendersSelf()方法以返回false来指明了委托。
组件的构造器创建了一个事件处理程序,并将这个事件处理程序添加到该组件的事件处理程序清单中。该组件也重写了 getRendererType()方法,以返回该组件呈现程序的标识符。
该组件的呈现代码被封装在一个呈现程序中,如清单14中所示。
清单14. /WEB-INF/classes/com/sabreware/renderers/ToggleGraphicHTMLRenderer.java
package com.sabreware.renderers;
import java.util.Iterator;
import java.util.Vector;
import javax.servlet.http.HttpServletRequest;
import javax.faces.component.AttributeDescriptor;
import javax.faces.component.UIComponent;
import javax.faces.component.UIGraphic;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.event.CommandEvent;
import javax.faces.event.FacesEvent;
import javax.faces.render.Renderer;
import com.sabreware.components.UIToggleGraphic;
public class ToggleGraphicHTMLRenderer extends Renderer {
// This vector's iterator is returned from the getAttributeNames methods.
private Vector emptyVector = new Vector();
// This method, which is called during the Apply Request Values phase,
// decodes request parameters.
public void decode(FacesContext context, UIComponent component)
throws java.io.IOException {
HttpServletRequest request = (HttpServletRequest)
context.getServletRequest();
// If there's a request parameter named component whose value
// matches this component's ID...
if(component.getComponentId().equals(
request.getParameter("component"))) {
// ...enqueue a command event on the FacesContext event queue
// for this component. The processEvent method, listed below,
// processes that event.
context.addRequestEvent(component,
new CommandEvent(component, ((UIToggleGraphic)component).
getClickCommandName()));
}
}
// This method, which is called during the Render Response phase,
// generates the markup that represents the component.
public void encodeEnd(FacesContext context, UIComponent component)
throws java.io.IOException {
UIToggleGraphic toggleGraphic = (UIToggleGraphic)component;
ResponseWriter writer = context.getResponseWriter();
HttpServletRequest request = (HttpServletRequest)
context.getServletRequest();
// Represent this component as an HTML anchor element with an
// image. When the image is clicked, the current page is reloaded
// with a request parameter named component whose value is this
// component's ID.
writer.write("/<a href='?component=" + component.getComponentId() + "'>");
writer.write("/<img border='0' src='");
writer.write(request.getContextPath() + (String)toggleGraphic.getURL() + "'/>");
writer.write("/</a>");
}
public void encodeBegin(FacesContext context, UIComponent component)
throws java.io.IOException {
}
public void encodeChildren(FacesContext context, UIComponent component)
throws java.io.IOException {
}
public AttributeDescriptor getAttributeDescriptor(String componentType,
String name) {
return null;
}
public AttributeDescriptor getAttributeDescriptor(UIComponent component,
String name) {
return null;
}
public Iterator getAttributeNames(String componentType) {
return emptyVector.iterator();
}
public Iterator getAttributeNames(UIComponent component) {
return emptyVector.iterator();
}
public boolean supportsComponentType(String componentType) {
return "com.sabreware.components.UIToggleGraphic".equals(componentType);
}
public boolean supportsComponentType(UIComponent component) {
return supportsComponentType(component.getComponentType());
}
}
像清单12的 原始组件一样,前述的呈现程序实现了decode()和encodeEnd()方法。后者生成了组件的HTML标记。现在,最后的8个方法必须由所有的呈 现程序实现(不管它们是相关的或不相关的),因为这些方法是由Renderer接口定义的,所有的呈现程序都必须实现这些方法。有望JSF 1.0会提供一种抽象类来实现 Renderer接口,并提供这些方法合理的默认实现。
清单15列出了组件的事件处理程序。
清单15. /WEB-INF/classes/com/sabreware/eventHandlers/ToggleGraphicEventHandler.java
package com.sabreware.eventHandlers;
import javax.faces.event.RequestEventHandler;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.event.CommandEvent;
import javax.faces.event.FacesEvent;
import com.sabreware.components.UIToggleGraphic;
public class ToggleGraphicEventHandler extends RequestEventHandler {
public boolean processEvent(FacesContext context, UIComponent component,
FacesEvent event) {
if(event instanceof CommandEvent) {
CommandEvent cmdEvent = (CommandEvent)event;
UIToggleGraphic toggleGraphic = (UIToggleGraphic)component;
// If the event's command name equals "click"...
if(toggleGraphic.getClickCommandName().
equals(cmdEvent.getCommandName())) {
// ...toggle the component's image
toggleGraphic.toggleImage();
return false; // go directly to render phase
}
}
return true; // process request normally
}
}
像清单12中的组件一样,前述的事件处理程序实现了processEvent()方法,以处理组件的请求事件。如果这个方法返回true,JSF生命周期就正常继续。否则,JSF生命周期就直接进入Render Response阶段。
Web应用程序的革命
JavaServer Faces是一个Web应用程序框架,它定义了一个请求生命周期和一种丰富的组件层次结构。请求生命周期将开发人员从他们的Web应用程序的代码编写中解 放出来,使得这些应用程序更加易于实现。组件层次结构让开发人员可以实现呈现不同标记类型的自定义组件。另外,开发人员可以为JSF内置组件实现呈现程 序,因此这些组件也可以生成其他的标记类型。JavaServer Faces有望对基于Java的Web应用程序的开发的产生关键影响。
关于作者
David Geary是Core JSTL Mastering the JSP Standard Tag Library (Prentice Hall,2002; ISBN: 0131001531)、 Advanced JavaServer Pages (Prentice Hall,2001; ISBN:0130307041)和Graphic Java系列(Sun Microsystems Press)的作者。18年来,David一直使用多种面向对象语言开发面向对象的软件。从GOF Design Patterns一书于1994年出版以来,David就是一位设计模式的积极倡议者,并在Smalltalk、C++和Java中使用了一些已实现的设 计模式。1997年,David开始成为一个专职的作者和业余的演讲者和顾问。David是定义JSP标准标签库专家组的一名成员,也是Apache Struts JSP框架的贡献者。他主持编写JavaWorld的 Java Design Patterns专栏。
免责声明
本文讨论的代码是针对EA2 JSF参考实现编写的。如前所述,规范和参考实现处在不固定状态,因此,本文中的代码在不远的将来可能会过时;然而,这些代码可作为EA2参考实现的广 告,它们分别在Tomcat 4.0.6和Resin 2.1.6上测试通过。此外,您可以一直阅读JSF规范,直到它成熟为止,但要真正掌握这些概念,还必须反复琢磨这些代码。
- 可从如下网址下载本文的源代码:
http://www.javaworld.com/javaworld/jw-11-2002/jsf/jw-1129-jsf.jar - "初识JavaServer Faces" David Geary (JavaWorld):
第1部分:学习如何使用JSF实现Web用户界面
第2部分:探讨JavaServer Faces组件 - Struts和JavaServer Faces的集成策略:
http://www.mail-archive.com/struts-dev@jakarta.apache.org/msg08457.html - 下载JSF规范、参考实现、两个示范程序以及JSF教程:
http://java.sun.com/j2ee/javaserverfaces - 浏览JavaWorld 的Topical Index的JavaServer Pages部分:
http://www.javaworld.com/channel_content/jw-jsp-index.shtml - 浏览JavaWorld 的Topical Index的 Enterprise Java部分:
http://www.javaworld.com/channel_content/jw-enterprise-index.shtml