4. 事件处理
JSF的事件模型提供一个近似的桌面GUI事件方式,让熟悉GUI设计的人员也
能快速上手Web程序设计。
4.1 动作事件
JSF支持事件处理模型,虽然由于HTTP本身无状态(stateless)的特性,使
得这个模型多少有些地方仍不太相同,但JSF所提供的事件处理模型已足以让一
些传统GUI程序的设计人员,可以用类似的模型来开发程序。
在1.2简单的导航中,我们根据动作方法(action method)的结果来决定要导
向的网页,一个按钮绑定一个方法,这样的作法实际上是JSF所提供的简化的事
件处理程序,在按钮上使用action绑定一个动作方法(action method),实际上JSF
会为其自动产生一个「预定义的ActionListener」来处理事件,并根据其传回值来
决定导向的页面。
如果您需要使用同一个方法来应付多种事件来源,并想要取得事件来源的相
关信息,您可以让处理事件的方法接收一个javax.faces.event.ActionEvent事件参
数,例如:
•UserBean.java
package onlyfun.caterpillar;
import javax.faces.event.ActionEvent;
public class UserBean {
private String name;
private String password;
private String errMessage;
private String outcome;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
public void setErrMessage(String errMessage) {
this.errMessage = errMessage;
}
public String getErrMessage() {
return errMessage;
}
public void verify(ActionEvent e) {
if(!name.equals("justin") || !password.equals("123456")) {
errMessage = "名称或密码错误" + e.getSource();
outcome = "failure";
}
else {
outcome = "success";
}
}
public String outcome() {
return outcome;
}
}
在上例中,我们让verify方法接收一个ActionEvent对象,当使用者按下按钮,
会自动产生ActionEvent对象代表事件来源,我们故意在错误信息之后加上事件来
源的字符串描述,这样就可以在显示错误信息时一并显示事件来源描述。
为了提供ActionEvent的存取能力,您的index.jsp可以改写如下:
•index.jsp
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@page contentType="text/html;charset=Big5"%>
<html>
<head>
<title>第一个JSF程序</title>
</head>
<body>
<f:view>
<h:form>
<h3>请输入您的名称</h3>
<h:outputText value="#{user.errMessage}"/>
<p>名称: <h:inputText value="#{user.name}"/>
<p>密码: <h:inputSecret value="#{user.password}"/>
<p><h:commandButton value="送出" actionListener="#{user.verify}"
action="#{user.outcome}"/>
</h:form>
</f:view>
</body>
</html>
主要改变的是按钮上使用了actionListener属性,这种方法可以使用一个
ActionListener,JSF会先检查是否有指定的actionListener,然后再检查是否指定
了动作方法并产生预定义的ActionListener,并根据其传回值导航页面。
如果您要注册多个ActionListener,例如当使用者按下按钮时,顺便在记录文
件中增加一些记录信息,您可以实现javax.faces.event.ActionListener,例如:
•LogHandler.java
package onlyfun.caterpillar;
import javax.faces.event.ActionListener;
....
public class LogHandler implements ActionListener {
public void processAction(ActionEvent e) {
// 处理Log
}
}
• VerifyHandler.java
package onlyfun.caterpillar;
import javax.faces.event.ActionListener;
....
public class VerifyHandler implements ActionListener {
public void processAction(ActionEvent e) {
// 处理验证
}
}
这么一来,您就可以使用<f:actionListener>标签向组件注册事件,例如:
<h:commandButton value="送出" action="#{user.outcome}">
<f:actionListener type="onlyfun.caterpillar.LogHandler"/>
<f:actionListener type="onlyfun.caterpillar.VerifyHandler"/>
</h:commandButton>
<f:actionListener>会自动产生type所指定的对象,并呼叫组件的
addActionListener()方法注册Listener。
4.2 即时事件
所谓的即时事件(Immediate Events),是指JSF视图组件在取得请求中该取
得的值之后,即立即处理指定的事件,而不再进行后续的转换器处理、验证器处
理、更新模型值等流程。
在JSF的事件模型中之所以会有所谓即时事件,是因为Web应用程序的先天
特性不同于GUI程序,所以JSF的事件方式与GUI程序的事件方式仍有相当程度的
不同,一个最基本的问题正因为HTTP无状态的特性,使得Web应用程序天生就
无法直接唤起服务器端的特定对象。
所有的对象唤起都是在服务器端执行的,至于该唤起什么对象,则是依一个
基本的流程:
重建视图(Restore View)
依客户端传来的session数据或服务器端上的session数据,重建JSF视图组件。
套用请求值(Apply Request Values)
JSF视图组件各自获得请求中的属于自己的值,包括旧的值与新的值。
执行验证(Process Validations)
转换为对象并进行验证。
更新模型值(Update Model Values)
更新Bean或相关的模型值。
唤起应用程序(Invoke Application)
执行应用程序相关逻辑。
绘制响应页面(Render Response)
对先前的请求处理完之后,产生页面以反应客户端执行结果。
对于动作事件(Action Event)来说,组件的动作事件是在套用请求值阶段
就生成ActionEvent对象了,但相关的事件处理并不是马上进行,ActionEvent会
先被排入队列,然后必须再通过验证、更新阶段,之后才处理队列中的事件。
这样的流程对于按下按钮然后执行后端的应用程序来说不成问题,但有些事
件并不需要这样的流程,例如只影响页面的事件。
举个例子来说,在表单中可能有使用者名称、密码等栏目,并提供有一个地
区选项按钮,使用者可以在不填写名称、密码的情况下,就按下地区选项按钮,
如果依照正常的流程,则会进行验证、更新模型值、唤起应用程序等流程,但显
然的,使用者名称与密码是空白的,这会引起不必要的错误。
您可以设定组件的事件在套用请求值之后立即被处理,并跳过后续的阶段,
直接进行页面绘制以响应请求,对于JSF的input与command组件,都有一个
immediate属性可以设定,只要将其设定为true,则指定的事件就成为即时事件。
一个例子如下:
•index.jsp
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@page contentType="text/html;charset=UTF8"%>
<f:view locale="#{user.locale}">
<f:loadBundle basename="messages" var="msgs"/>
<html>
<head>
<title><h:outputText value="#{msgs.titleText}"/></title>
</head>
<body>
<h:form>
<h3><h:outputText value="#{msgs.hintText}"/></h3>
<h:outputText value="#{msgs.nameText}"/>:
<h:inputText value="#{user.name}"/><p>
<h:outputText value="#{msgs.passText}"/>:
<h:inputSecret value="#{user.password}"/><p>
<h:commandButton value="#{msgs.commandText}" action="#{user.verify}"/>
<h:commandButton value="#{msgs.Text}" immediate="true"
actionListener="#{user.changeLocale}"/>
</h:form>
</body>
</html>
</f:view>
这是一个可以让使用者决定使用语系的示范,最后一个commandButton组件
被设定了immediate属性,当按下这个按钮后,JSF套用请求值之后会立即处理指
定的actionListener,而不再进行验证、更新模型值,简单的说,就这个程序来说,
您在输入栏目与密码栏目中填入的值,不会影响您的user.name与user.password。
基于范例的完整起见,我们列出这个程序Bean对象及faces-config.xml:
•UserBean.java
package onlyfun.caterpillar;
import javax.faces.event.ActionEvent;
public class UserBean {
private String locale = "en";
private String name;
private String password;
private String errMessage;
public void changeLocale(ActionEvent e) {
if(locale.equals("en")) locale = "zh_TW";
else locale = "en";
}
public String getLocale() {
if (locale == null) {
locale = "en";
}
return locale;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
public void setErrMessage(String errMessage) {
this.errMessage = errMessage;
}
public String getErrMessage() {
return errMessage;
}
public String verify() {
if(!name.equals("justin") || !password.equals("123456")) {
errMessage = "名称或密码错误";
return "failure";
}
else {
return "success";
}
}
}
• faces-config.xml
<?xml version="1.0"?>
<!DOCTYPE faces-config PUBLIC
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"
"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
<faces-config>
<navigation-rule>
<from-view-id>/pages/index.jsp</from-view-id>
<navigation-case>
<from-outcome>success</from-outcome>
<to-view-id>/pages/welcome.jsp</to-view-id>
</navigation-case>
<navigation-case>
<from-outcome>failure</from-outcome>
<to-view-id>/pages/index.jsp</to-view-id>
</navigation-case>
</navigation-rule>
<managed-bean>
<managed-bean-name>user</managed-bean-name>
<managed-bean-class>
onlyfun.caterpillar.UserBean
</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
</faces-config>
信息资源文件的内容则是如下:
•messages_en.properties
titleText=JSF Demo
hintText=Please input your name and password
nameText=name
passText=password
commandText=Submit
Text=\u4e2d\u6587
Text中设定的是「中文」转换为Java Unicode Escape格式的结果,另一个信
息资源文件的内容则是英文信息的翻译而已,其转换为Java Unicode Escape格式
结果如下:
•messages_zh_TW.properties
titleText=JSF\u793a\u7bc4
hintText=\u8acb\u8f38\u5165\u540d\u7a31\u8207\u5bc6\u78bc
nameText=\u540d\u7a31
passText=\u5bc6\u78bc
commandText=\u9001\u51fa
Text=English
welcome.jsp就请自行设计了,程序的画面如下:
4.3 值变事件
如果使用者改变了JSF输入组件的值后提交表单,就会发生值变事件(Value
Change Event),这会丢出一个javax.faces.event.ValueChangeEvent对象,如果您
想要处理这个事件,有两种方式,一是直接设定JSF输入组件的
valueChangeListener属性,例如:
<h:selectOneMenu value="#{user.locale}"
onchange="this.form.submit();"
valueChangeListener="#{user.changeLocale}">
<f:selectItem itemValue="zh_CN" itemLabel="Chinese"/>
<f:selectItem itemValue="en" itemLabel="English"/>
</h:selectOneMenu>
为了模拟GUI中选择了选单项目之后就立即发生反应,我们在onchange属性
中使用了JavaScript,其作用是在选项项目发生改变之后,立即提交表单,而不
用按下提交按钮;而valueChangeListener属性所绑定的user.changeLocale方法必须
接受ValueChangeEvent对象,例如:
•UserBean.java
package onlyfun.caterpillar;
import javax.faces.event.ValueChangeEvent;
public class UserBean {
private String locale = "en";
private String name;
private String password;
private String errMessage;
public void changeLocale(ValueChangeEvent event) {
if(locale.equals("en")) locale = "zh_CN";
else locale = "en";
}
public void setLocale(String locale) {
this.locale = locale;
}
public String getLocale() {
if (locale == null) {
locale = "en";
}
return locale;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
public void setErrMessage(String errMessage) {
this.errMessage = errMessage;
}
public String getErrMessage() {
return errMessage;
}
public String verify() {
if(!name.equals("justin") || !password.equals("123456")) {
errMessage = "名称或密码错误";
return "failure";
}
else {
return "success";
}
}
}
另一个方法是实现javax.faces.event.ValueChangeListener接口,并定义其
processValueChange()方法,例如:
•SomeListener.java
package onlyfun.caterpillar;
....
public class SomeListener implements ValueChangeListener {
public void processValueChange(ValueChangeEvent event) {
....
}
....
}
然后在JSF页面上使用<f:valueChangeListener>标签,并设定其type属性,例如:
<h:selectOneMenu value="#{user.locale}" onchange="this.form.submit();">
<f:valueChangeListener type="onlyfun.caterpillar.SomeListener"/>
<f:selectItem itemValue="zh_CN" itemLabel="Chinese"/>
<f:selectItem itemValue="en" itemLabel="English"/>
</h:selectOneMenu>
下面这个页面是对4.2即时事件中的范例程序作一个修改,将语言选项改以下拉
式选单的选择方式呈现,这必须配合上面提供的UserBean类来使用:
•index.jsp
<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %>
<%@page contentType="text/html;charset=UTF8"%>
<f:view locale="#{user.locale}">
<f:loadBundle basename="messages" var="msgs"/>
<html>
<head>
<title><h:outputText value="#{msgs.titleText}"/></title>
</head>
<body>
<h:form>
<h:selectOneMenu value="#{user.locale}"
immediate="true"
onchange="this.form.submit();"
valueChangeListener="#{user.changeLocale}">
<f:selectItem itemValue="zh_CN" itemLabel="Chinese"/>
<f:selectItem itemValue="en" itemLabel="English"/>
</h:selectOneMenu>
<h3><h:outputText value="#{msgs.hintText}"/></h3>
<h:outputText value="#{msgs.nameText}"/>:
<h:inputText value="#{user.name}"/><p>
<h:outputText value="#{msgs.passText}"/>:
<h:inputSecret value="#{user.password}"/><p>
<h:commandButton value="#{msgs.commandText}" action="#{user.verify}"/>
</h:form>
</body>
</html>
</f:view>
4.4 Phase事件
在4.2即时事件中我们提到,JSF的请求执行到反应,完整的过程会经过六个
阶段:
重建视图(Restore View)
依客户端传来的session数据或服务器端上的session数据,重建JSF视图组件。
套用请求值(Apply Request Values)
JSF视图组件各自获得请求中的属于自己的值,包括旧的值与新的值。
执行验证(Process Validations)
转换为对象并进行验证。
更新模型值(Update Model Values)
更新Bean或相关的模型值。
唤起应用程序(Invoke Application)
执行应用程序相关逻辑。
绘制响应页面(Render Response)
对先前的请求处理完之后,产生页面以反应客户端执行结果。
在每个阶段的前后会引发javax.faces.event.PhaseEvent,如果您想尝试在每个
阶段的前后捕捉这个事件,以进行一些处理,则可以实现
javax.faces.event.PhaseListener,并向javax.faces.lifecycle.Lifecycle登记这个
Listener,以在适当的时候通知事件的发生。
PhaseListener有三个必须实现的方法getPhaseId()、beforePhase()与
afterPhase(),其中getPhaseId()传回一个PhaseId对象,代表Listener想要被通知的
时机,可以设定的时机有:
PhaseId.RESTORE_VIEW
PhaseId.APPLY_REQUEST_VALUES
PhaseId.PROCESS_VALIDATIONS
PhaseId.UPDATE_MODEL_VALUES
PhaseId.INVOKE_APPLICATION
PhaseId.RENDER_RESPONSE
PhaseId.ANY_PHASE
其中PhaseId.ANY_PHASE指的是任何的阶段转换时,就进行通知;您可以
在beforePhase()与afterPhase()中编写阶段前后分别想要处理的动作,例如下面这
个简单的类会列出每个阶段的名称:
•ShowPhaseListener.java
package onlyfun.caterpillar;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
public class ShowPhaseListener implements PhaseListener {
public void beforePhase(PhaseEvent event) {
String phaseName = event.getPhaseId().toString();
System.out.println("Before " + phaseName);
}
public void afterPhase(PhaseEvent event) {
String phaseName = event.getPhaseId().toString();
System.out.println("After " + phaseName);
}
public PhaseId getPhaseId() {
return PhaseId.ANY_PHASE;
}
}
编写好PhaseListener后,我们可以在faces-config.xml中向Lifecycle进行注册:
•faces-config.xml
<?xml version="1.0"?>
<!DOCTYPE faces-config PUBLIC
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"
"http://java.sun.com/dtd/web-facesconfig_1_0.dtd">
<faces-config>
<lifecycle>
<phase-listener>
onlyfun.caterpillar.ShowPhaseListener
</phase-listener>
</lifecycle>
......
</faces-config>
您可以使用这个简单的类,看看在请求任一个JSF画面时所显示的内容,借
此了解JSF每个阶段的流程变化。