摘要
2002年9月,在Java Specification Request(JSR)127上发布了JavaServer Faces规范的早期访问(early access,EA)草案。JavaServer Faces具有定义良好的请求处理生命周期和丰富的组件层次结构,将会深远地影响到J2EE(Java 2 Platform, Enterprise Edition)应用程序的开发。本系列文章篇分为两部分,在第 1 部分中,David Geary介绍了 JavaServer Faces并初步探讨它的基本概念。在第2部分中,他考察了一些更高级的概念,比如自定义验证、国际化和自定义组件实现。(2002年12月27日)
正文
在第1部分我们已经谈到,JSF从概念上组合了Struts(Apache的流行开放源代码JSP框架)和Swing(Java的标准桌面应用程序框架)。像Struts一样,JSF提供了定义良好的生命周期来处理请求(如图1所示)。
图1. JavaServer Faces生命周期。
第1部分已经详细讨论了JSF生命周期,这里就不再赘述。但本文提及了JSF生命周期,为了方便起见,这里再次显示了图1。
像Swing一样,JSF指定了一种丰富的组件层次结构,以实现服务器端用户界面(UI)组件。该层次结构让开发人员能够使用各种呈现程序从头开始实现组件,这些呈现程序可呈现不同的标记语言。JSF组件层次结构及相关对象(比如验证程序、事件处理程序和模型对象)是本文下面这些章节的重点:
- JSF组件
- 实现自定义验证程序
- 运行中的模型对象
- 使用JSF国际化Web应用程序
- 实现自定义组件
请阅读整个"初识JavaServer Faces"系列:
第1部分: 学习如何使用JSF实现Web用户界面
第2部分: 探讨JavaServer Faces组件
JSF组件
JSF的组件层次结构把JSF与Struts这样的其他Web应用程序构架分开。例如,Struts和JSF都有一组表示HTML组件(比如文本字段和选项列表)的JavaServer Pages (JSP)标签,但Struts标签直接生成HTML,而JSF标签创建生成HTML的服务器端组件。在您认识到这一点之前:可将用于生成除HTML之外的标记语言的呈现程序安装到JSF组件,JSF方法起初看起来不会有明显的优势。因此,您可为您选择的标记语言实现一个呈现程序,并将该呈现程序与现有的JSF组件关联起来。此外,借助这种丰富的JSF组件层次结构,创建像树形查看器或查询生成器这样的自定义组件就变得相对容易了。
图2展示了JSF组件的类图。
图2. JSF UI组件类图。
JSF组件实现了javax.faces.component.UIComponent接口,该接口指定了多达46种方法用于定义JSF组件的基本要素。图2中只列出其中的一些方法。幸运的是,您的组件可以扩展抽象类javax.faces.component.UIComponentBase,该类为除getComponentType()之外的所有UIComponent方法实现了合理的默认实现。这表明您可以通过扩展UIComponentBase和实现getComponentType()来简单地实现一个自定义组件。
每个JSF组件具有:
- 一系列的子组件
- 属性的散列图
- 一个或多个验证程序
- 一个或多个事件处理程序
- 一个针对可选呈现程序的标识符
所有的JSF组件都是潜在的容器,因为子组件列表是由每个JSF组件维护的。这使您可在其他组件中嵌套组件(即使它们包含了其他组件)。这种关键能力是复合(Composite)设计模式背后的推动力,通常是由像Swing或VisualWorks Smalltalk这样的面向对象图形用户界面(GUI)来实现的。
JSF组件还维护一系列属性。这些属性存储了特定于组件的信息。例如,您可能想存储与组件使用的图像相关联的URL。这样您就可将那个URL(或图像本身)存储在组件的属性列表中。在散列图中,组件属性是通过名称来存储的。
所有JSF组件都执行3个基本任务:
- 呈现组件(一般通过生成标记来呈现)
- 处理组件的事件
- 验证组件的值
JSF组件可以自己呈现或将呈现委托给一个呈现程序。布尔型的UIComponent.rendersSelf()方法告诉JSF实现一个组件是否自己呈现。如果没有自己呈现,JSF实现就使用 UIComponent.getRendererType()方法来取得到该组件的呈现程序的一个引用,然后调用那个呈现程序来产生该组件的标记。
JSF组件事件处理也可由组件直接进行管理,另外,组件也可将其委托给一个事件处理程序。通常,在JSF生命周期的Apply Request Values阶段,组件的呈现程序或组件本身可为组件注册一个或多个事件处理程序。
最后,JSF组件可以具有一个或多个验证输入的验证程序。这些验证程序通常是由JSF实现创建的,并由组件存储在一个数组列表中。
既然我们已经对JSF组件有了一个基本的理解,就让我来看一下如何实现自定义验证程序,并将它与一个组件关联起来。
实现自定义验证程序
在第1部分中,我们讨论了如何使用内置的验证程序来验证JSF组件的输入。如果您使用JSP作为Web应用程序的视图(通常是这样的),那么您就可以使用 <faces:validator> 标签来为JSF组件指定验证程序,如下面的代码所示:
<faces:textentry_input id=name> <faces:validator className=javax.faces.validator.RequiredValidator/> </faces:textentry_input>
上面的代码片断将一个验证程序与一个文本字段关联起来。您只要使用<faces:validate> 标签的className属性来指定验证程序的类名。上面指定的验证程序是一个JSF内置的验证程序,用于确保一个组件的值不为null。正如第1部分所讨论的那样,JSF提供了很多内置的验证程序,但您也可以实现自己的验证程序,并将它们与一个JSF组件关联起来。例如,图3中展示的应用程序使用了一个自定义验证程序来验证用户名。
图3. 使用自定义验证程序。
清单1中列出了图3中显示的JSP页面。
清单1
<!DOCTYPE HTML PUBLIC -//W3C//DTD HTML 4.0 Transitional//EN> <html> <head> <title>A Simple JavaServer Faces Application</title> </head>
<body> <%@ taglib uri=http://java.sun.com/j2ee/html_basic/ prefix=faces %>
<font size=4>Please enter your name and password</font>
<faces:usefaces> <faces:form id=simpleForm formName=simpleForm> <table> <tr> <td>Name:</td> <td> <faces:textentry_input id=name> <faces:validator className=com.sabreware.validators.NameValidator/> <faces:validator className=javax.faces.validator.LengthValidator/> <faces:attribute name=javax.faces.validator.LengthValidator.MINIMUM value=3/> </faces:textentry_input> </td>
<td> <faces:validation_message componentId=name/> </td> </tr>
<tr> <td>Password:</td> <td> <faces:textentry_secret id=password/> </td> </tr> </table>
<p><faces:command_button id=submit commandName=Log In/> </faces:form> </faces:usefaces> </body> </html>
像本节开始展示的代码片段一样,前述的JSP页面使用<faces:validator> 标签来将验证程序与一个JSF组件关联起来。在这种情形中,验证程序是一个验证用户名的自定义验证程序和一个JSF内置验证程序,用于保证用户名至少有3个字符长。
清单2中列出了清单1中使用的自定义验证程序。
清单2. WEB-INF/classes/com/sabreware/validators/NameValidator.java
package com.sabreware.validators;
import java.util.Iterator; import javax.faces.component.UIComponent; import javax.faces.component.AttributeDescriptor; import javax.faces.context.FacesContext; import javax.faces.context.Message; import javax.faces.context.MessageImpl; import javax.faces.validator.Validator;
public class NameValidator implements Validator { public AttributeDescriptor getAttributeDescriptor(String attributeName) { return null; } public Iterator getAttributeNames() { return null; } public void validate(FacesContext context, UIComponent component) { String name = (String)component.getValue();
if(!"phillip".equalsIgnoreCase(name)) { context.addMessage(component, new MessageImpl(Message.SEVERITY_ERROR, "bad username", "The username " + name + " is invalid")); } } }
前述的验证程序实现了javax.faces.validator.Validator接口,而该接口定义了以下方法:
- void validate(FacesContext, UIComponent)
- Iterator getAttributeNames(String)
- AttributeDescriptor getAttributeDescriptor(String)
validate()方法为指定的组件执行实际的验证。 Validator接口定义的另外两个方法被工具用来发现与特定验证程序相关的属性(及其描述)。在这种情形下,验证程序没有任何属性,因此getAttributeDescriptor()和getAttributeNames()方法只返回null。
如果组件的值是有效的,那么验证程序的validate()方法就什么都不做。如果其值是无效的,validator()方法就会创建消息将其添加到JSF上下文中。所有的这一切发生在JSF生命周期的Process Validations阶段。如果一个组件验证失败(表明已经将消息添加到JSF上下文中),JSF实现就直接进入Render Response阶段。否则,生命周期进入Apply Model Values阶段。(参阅图1以获取有关JSF生命周期各阶段的更多信息)
运行中的模型对象
在"模型-视图-控制器"(MVC)体系结构中,视图通常是基于Java的Web应用程序的JSP页面,该视图显示了模型中包含的值。JavaServer Faces使得UI组件可以容易地连接到存储在模型对象中的字段。正如上一节所看到的那样,如果一个请求的值都是有效的,JSF生命周期将从Process Validations阶段转向Apply Model Values阶段。在后一阶段,JSF实现将组件值复制到与这些对象相关的模型对象中。图4a展示的HTML文本字段连接到了一个模型对象。当单击Log In按扭时,JSF实现将把控制权传递给一个JSP页面,以使用那个模型对象来显示一条个性化的问候信息(图4b)。
图4a. 连接到一个模型对象的HTML文本字段
图4b. 个性化问候信息
清单3展示了图4a中显示的JSP页面。
清单3. /index.jsp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>A Simple JavaServer Faces Application</title> </head>
<body> <%@ taglib uri="http://java.sun.com/j2ee/html_basic/" prefix="faces" %>
<font size="4">Please enter your name and password</font> <!-- We use jsp:useBean to create a model object, but real applications typically create model objects elsewhere: JSP pages are purely views of the model --> <jsp:useBean id=user class=com.sabreware.beans.User scope=session/>
<faces:usefaces> <faces:form id="simpleForm" formName="simpleForm"> <table> <tr> <td>Name:</td> <td><faces:textentry_input id="name" modelReference="${user.name}"/></td> </tr>
<tr> <td>Password:</td> <td><faces:textentry_secret id="password" modelReference="${user.password}"/></td> </tr> </table>
<p><faces:command_button id="submit" commandName="Log In"/> </faces:form> </faces:usefaces> </body> </html>
前述JSP页面创建了一个com.sabreware.beans.User类型的会话作用域(session-scope)变量。通常,JSP页面不会创建模型对象。相反,业务对象(比如servlet或servlet过滤器)一般会创建模型对象。前述JSP页面使用<faces:textentry_input> 标签的 modelReference属性来指定user对象中的一个字段。例如,name字段与user对象的name属性关联起来,password字段与user对象的password属性关联起来。
清单4展示了User类。
清单4. The model object: com/sabreware/beans/User
package com.sabreware.beans;
public class User { private String name = null, password = null;
public void setName(String name) { this.name = name; } public String getName() { return name; }
public void setPassword(String pwd) { this.password = pwd; } public String getPassword() { return password; } }
User类是一个简单的、存储了用户名和密码的JavaBean,以下是图4b中展示的JSP页面。
清单5. /welcome.jsp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <title>A Simple JavaServer Faces Application</title> </head>
<body> <%@ taglib uri=http://java.sun.com/jstl/core prefix=c %>
Welcome to JavaServer Faces, <c:out value=${user.name}/>! </body> </html>
前述的JSP页面访问了清单3列出的JSP页面创建的user对象的name属性。该属性是使用JSP标准标签库(JSTL)来访问的。注意,清单3中使用了完全相同的语法,以便通过 modelReference属性来指定用户属性。
使用JSF国际化Web应用程序
就JavaServer Faces而言,它并没有对国际化提供很多支持。相反,JSF依赖JSTL来支持国际化和本地化。
通常,基于Java的Web应用程序会在JSP页面中本地化消息(和格式或解析数字、货币、百分数以及日期)。它通常也会在Java类中本地化消息。例如,一个验证程序可以本地化错误消息。本节讨论了这两种方法,它们使用JSTL来国际化前一节"运行中的模型对象"讨论的Web应用程序。
首先,我们将所有的本地化文本存储在一个属性文件中:
清单6. /WEB-INF/classes/messages_en.properties
login.window-title=Internationalization Javaserver Faces login.name=Name login.password=Password login.submit=Log In
errors.bad-username=Bad username errors.bad-username-details=The username {0} is invalid
然后重写清单3中的JSP页面来本地化显示给用户的所有文本。清单7展示了重写过的JSP页面。
清单7. /index.jsp
<!DOCTYPE HTML PUBLIC -//W3C//DTD HTML 4.0 Transitional//EN> <html> <head> <%@ taglib uri=http://java.sun.com/jstl/fmt prefix=fmt %> <fmt:setLocale value=en-US scope=application/> <fmt:setBundle basename=messages scope=application/>
<title><fmt:message key=login.window-title/></title> </head>
<body> <%@ taglib uri=http://java.sun.com/j2ee/html_basic/ prefix=faces %>
<font size=4>Please enter your name and password</font>
<jsp:useBean id=user class=com.sabreware.beans.User scope=application/>
<faces:usefaces> <faces:form id=simpleForm formName=simpleForm> <table> <tr> <td><fmt:message key=login.name/></td> <td><faces:textentry_input id=name modelReference=${user.name}> <faces:validator className= com.sabreware.validators.NameValidator/> </faces:textentry_input> </td>
<td> <faces:validation_message componentId=name/> </td> </tr>
<tr> <td><fmt:message key=login.password/></td> <td><faces:textentry_secret id=password modelReference=${user.password}/></td> </tr> </table>
<p><faces:command_button id=submit commandName=Log In/> </faces:form> </faces:usefaces> </body> </html>
前述的JSP页面使用JSTL格式化库中的两个标签来设置地区和资源包基名(分别是<fmt:setLocale> 和<fmt:setBundle> )以及第3个标签(<fmt:message> )来显示存储在指定资源包中的本地化消息。
下面列出的验证程序也使用JSTL来本地化错误消息。
清单8. /WEB-INF/classes/com/sabreware/validators/NameValidator.java
package com.sabreware.validators;
import java.util.*; import java.text.MessageFormat;
import javax.servlet.ServletContext; import javax.servlet.jsp.jstl.core.Config; import javax.servlet.jsp.jstl.fmt.LocalizationContext;
import javax.faces.component.UIComponent; import javax.faces.component.AttributeDescriptor; import javax.faces.context.FacesContext; import javax.faces.context.Message; import javax.faces.context.MessageImpl; import javax.faces.validator.Validator;
public class NameValidator implements Validator { public AttributeDescriptor getAttributeDescriptor(String attributeName) { return null; } public Iterator getAttributeNames() { return null; } public void validate(FacesContext context, UIComponent component) { String name = (String)component.getValue();
if(!"phillip".equalsIgnoreCase(name)) { ServletContext app = context.getServletContext(); LocalizationContext lc = (LocalizationContext) Config.get(app, Config.FMT_LOCALIZATION_CONTEXT);
if(lc == null) { context.addMessage(component, new MessageImpl(Message.SEVERITY_ERROR, "bad username", "The username " + name + " is invalid")); } else { ResourceBundle rb = lc.getResourceBundle(); Object[] args = new Object[] { new String(name) }; String cs = rb.getString("errors.bad-username-details"); String s = MessageFormat.format(cs, args);
context.addMessage(component, new MessageImpl(Message.SEVERITY_ERROR, rb.getString("errors.bad-username"), s)); } } } }
JSTL维护着一个包含资源包和用于定位该资源包的地区的本地化上下文。该本地化上下文可通过JSTL Config类使用。前述的JSP页面使用上下文的资源包来本地化消息。
在我们转到JSF自定义组件之前,您应该注意到JSF的国际化支持还不完整。在清单7中,Submit按扭显示的文本没有被本地化,因为JSF<command_button> 标签不支持文本本地化。我们可以使用scriptlet来国际化文本,但这样做包含了不应该在JSP页面中存在的难于阅读的代码(ugly code)。国际化支持将在JSF 1.0版本中完成。
注意: 详细讨论国际化超出了本文的范围。为获取有关JSTL的进一步信息,请参阅参考资料。
实现自定义组件
除了提供一组标准的组件、一个在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 // components 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 theres a request parameter named component whose value // matches this components 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 events command name equals "click"... if(clickCommand.equals(cmdEvent.getCommandName())) { // ...toggle the components image. toggleImage(); } return false; // Go directly to render phase. } return true; // Process request normally. }
// This method returns a string representing the components type. public String getComponentType() { return "com.sabreware.components.UIToggleGraphic"; }
// This private method toggles the components 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 components 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 vectors 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 theres a request parameter named component whose value // matches this components 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 // components 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 events command name equals "click"... if(toggleGraphic.getClickCommandName(). equals(cmdEvent.getCommandName())) { // ...toggle the components 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规范,直到它成熟为止,但要真正掌握这些概念,还必须反复琢磨这些代码。 |