JSF 2 事件处理、JavaScript 和 Ajax

JSF 的最大卖点在于它是一种基于组件的框架。这意味着您可以实现供其他人重用的组件。这种强大的重用机制在 JSF 1 中基本上是不可能实现的,因为在 JSF 1 中实现组件是非常困难的事情。

然而,正如 第 2 部分 所述,JSF 2 通过一种名为复合组件 的新特性简化了组件的实现 — 无需 Java 代码和配置。这一特性可以说是 JSF 2 中最重要的部分,因为它真正实现了 JSF 组件的潜力。

在这份有关 JSF 2 的第三篇也是最后一篇文章中,我将展示如何利用新的 Ajax 和事件处理功能(也在 JSF 2 中引入)构建复合组件特性,要从 JSF 2 中获得最大收益,需要遵循下面的技巧:

  • 技巧 1:组件化
  • 技巧 2:Ajax 化
  • 技巧 3:展示进度

对于第一个技巧,我将简要回顾已在 第 2 部分 中详细描述过的两个组件。对于后面的技巧,我将展示如何使用 Ajax 和事件处理功能来改造这些组件。

技巧 1:组件化

我在 第 1 部分 中引入的 places 应用程序包含有大量复合组件。其中之一便是 map 组件,它显示一个地址地图,其中包含一个缩放级别下拉菜单,如图 1 所示:


图 1. places 应用程序的 map 组件
map 组件

清单 1 显示了经过删减的 map 组件列表:


清单 1. map 组件

				
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
<html xmlns="http://www.w3.org/1999/xhtml"
    ...
    xmlns:composite="http://java.sun.com/jsf/composite"
    xmlns:places="http://java.sun.com/jsf/composite/components/places">
    
   <!-- INTERFACE -->
   <composite:interface>
     <composite:attribute name="title"/>
   </composite:interface>
        
   <!-- IMPLEMENTATION --> 
   <composite:implementation">
     <div class="map">
       ...
       <h:panelGrid...>
         <h:panelGrid...>
            <h:selectOneMenu onchange="submit()"
                 value="#{cc.parent.attrs.location.zoomIndex}"
                 valueChangeListener="#{cc.parent.attrs.location.zoomChanged}"
                 style="font-size:13px;font-family:Palatino">
            
              <f:selectItems value="#{places.zoomLevelItems}"/>
                           
            </h:selectOneMenu>               
          </h:panelGrid>
        </h:panelGrid>   
        
        <h:graphicImage url="#{cc.parent.attrs.location.mapUrl}" 
          style="border: thin solid gray"/>  
       ...
      
     </div>
   ...
       
  </composite:implementation>    
</html>

 

组件的一大优点就是可以使用更有效的替代方法替换它们,同时不会影响到相关的功能。比如,在图 2 中,我使用一个 Google Maps 组件替换了 清单 1 中的 image 组件,Google Maps 组件由 GMaps4JSF 提供(见 参考资料):


图 2. GMaps4JSF 的 map 图像
GMaps4JSF map 组件

map 组件的更新后的代码(进行了删减)如清单 2 所示:


清单 2. 使用一个 GMaps4JSF 组件替换 map 图形

				
<h:selectOneMenu onchange="submit()"
				value="#{cc.parent.attrs.location.zoomIndex}"
              valueChangeListener="#{cc.parent.attrs.location.zoomChanged}"
              style="font-size:13px;font-family:Palatino">
     
  <f:selectItems value="#{places.zoomLevelItems}"/>
                    
</h:selectOneMenu>   

...         

<m:map id="map" width="420px" height="400px" 
     address="#{cc.parent.attrs.location.streetAddress}, ..." 
     zoom="#{cc.parent.attrs.location.zoomIndex}"
     renderOnWindowLoad="false">
     
  <m:mapControl id="smallMapCtrl" 
              name="GLargeMapControl" 
          position="G_ANCHOR_TOP_RIGHT"/>
          
  <m:mapControl  id="smallMapTypeCtrl" name="GMapTypeControl"/>                  
  <m:marker id="placeMapMarker"/>     
    
</m:map>

 

要使用 GMaps4JSF 组件,我从 GMaps4JSF 组件集合中使用 <m:map> 标记替换了 <h:graphicImage> 标记。将 GMaps4JSF 组件与缩放下拉菜单连接起来也很简单,只需为 <m:map> 标记的 zoom 属性指定正确的 backing-bean 属性。

关于缩放级别需要注意一点,那就是当一名用户修改缩放级别时,我将通过 <h:selectOneMenu>onchange 属性强制执行表单提交,如 清单 1 中第一处使用粗体显示的代码行所示。这个表单提交将触发 JSF 生命周期,这实际上将把新的缩放级别推入到保存在父复合组件中的 location bean 的 zoomIndex 属性中。这个 bean 属性被绑定到输入组件,如 清单 2 中的第一行所示。

由于我没有为与缩放级别修改相关的表单提交指定任何导航,JSF 在处理请求后刷新了同一页面,重新绘制地图以反映新的缩放级别。然而,页面刷新还重新绘制了整个页面,即使只修改了地图图像。在 技巧 2:Ajax 化 中,我将展示如何使用 Ajax,只对图像部分重新绘制,以响应缩放级别的修改。

login 组件

places 应用程序中使用的另一个组件是 login 组件。图 3 展示了这个 login 组件的实际使用:


图 3. login 组件
login 组件

清单 3 展示了创建 图 3 所示的 login 组件的标记:


清单 3. 最基础的 login:只包含必需的属性

				
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
  xmlns:ui="http://java.sun.com/jsf/facelets"
  xmlns:util="http://java.sun.com/jsf/composite/components/util">

  <util:login loginAction="#{user.login}"
              managedBean="#{user}"/>
                
</ui:composition>

 

login 组件只包含两个必需的属性:

  • loginAction:登录 action 方法
  • managedBean:包含名称和密码属性的托管

清单 3 中指定的托管 bean 如清单 4 所示:


清单 4. User.groovy

				
package com.clarity

import javax.faces.context.FacesContext
import javax.faces.bean.ManagedBean
import javax.faces.bean.SessionScoped
 
@ManagedBean() 
@SessionScoped  
               
public class User {    
  private final String VALID_NAME     = "Hiro"
  private final String VALID_PASSWORD = "jsf"
  
  private String name, password;
 
  public String getName() { name }
  public void setName(String newValue) { name = newValue }
  
  public String getPassword() { return password }
  public void setPassword(String newValue) { password = newValue }  
  
  public String login() {
    "/views/places"
  }

  public String logout() {
    name = password = nameError = null
    "/views/login"
  }
}

 

清单 4 中的托管 bean 是一个 Groovy bean。在这里使用 Groovy 替代 Java 语言并不会带来多少好处,只是减少了处理分号和返回语句的麻烦。然而,在技巧 2 的 Validation 部分中,我将展示一个对 User 托管 bean 使用 Groovy 的更有说服力的原因。

大多数情况下,您将需要使用提示和按钮文本来配置登录组件,如图 4 所示:


图 4. 充分配置的 login 组件
充分配置的 login 组件

清单 5 展示了生成 图 4 所示的 login 组件的标记:


清单 5. 配置 login 组件

				
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
  xmlns:f="http://java.sun.com/jsf/core"
  xmlns:h="http://java.sun.com/jsf/html"
  xmlns:ui="http://java.sun.com/jsf/facelets"
  xmlns:util="http://java.sun.com/jsf/composite/components/util">
  
  <util:login loginPrompt="#{msgs.loginPrompt}"
               namePrompt="#{msgs.namePrompt}"
           passwordPrompt="#{msgs.passwordPrompt}"
          loginButtonText="#{msgs.loginButtonText}"
              loginAction="#{user.login}"          
              managedBean="#{user}"/>
                
</ui:composition>

 

清单 5 中,我从一个资源包中获取了用于提示的字符串和登录按钮的文本。

清单 6 定义了 login 组件:


清单 6. 定义 login 组件

				
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<!-- Usage:

  <util:login loginPrompt="#{msgs.loginPrompt}"
               namePrompt="#{msgs.namePrompt}"
           passwordPrompt="#{msgs.passwordPrompt}"
          loginButtonText="#{msgs.loginButtonText}"
              loginAction="#{user.login}"
              managedBean="#{user}">
                 
    <f:actionListener for="loginButton" 
                     type="com.clarity.LoginActionListener"/>
                            
  </util:login>

  managedBean must have two properties: name and password. 
  
  The loginAction attribute must be an action method that takes no
  arguments and returns a string. That string is used to navigate
  to the page the user sees after logging in.
  
  This component's loginButton is accessible so that you can
  add action listeners to it, as depicted above. The class specified
  in f:actionListener's type attribute must implement the
  javax.faces.event.ActionListener interface.
  
 -->
 
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:composite="http://java.sun.com/jsf/composite">
    
  <!-- INTERFACE -->
  <composite:interface>
  
    <!-- PROMPTS -->
    <composite:attribute name="loginPrompt"/>
    <composite:attribute name="namePrompt"/>
    <composite:attribute name="passwordPrompt"/>
    
    <!--  LOGIN BUTTON -->      
    <composite:attribute name="loginButtonText"/>
    
    <!-- loginAction is called when the form is submitted -->
    <composite:attribute name="loginAction" 
             method-signature="java.lang.String login()"
                     required="true"/>
                     
    <!-- You can add listeners to this actionSource:  -->
    <composite:actionSource name="loginButton" targets="form:loginButton"/>
      
    <!-- BACKING BEAN -->
    <composite:attribute name="managedBean" required="true"/>
  </composite:interface>
    
  <!-- IMPLEMENTATION -->
  <composite:implementation>
    <div class="prompt">
      #{cc.attrs.loginPrompt}
    </div>
    
    <!-- FORM -->       
    <h:form id="form">
      <h:panelGrid columns="2">
      
        <!-- NAME AND PASSWORD FIELDS -->
        #{cc.attrs.namePrompt}
        <h:inputText id="name" 
                  value="#{cc.attrs.managedBean.name}"/>
    
        #{cc.attrs.passwordPrompt} 
        <h:inputSecret id="password" size="8" 
          value="#{cc.attrs.managedBean.password}"/>
          
      </h:panelGrid>
    
      <p>
        <!-- LOGIN BUTTON -->    
        <h:commandButton id="loginButton"
          value="#{cc.attrs.loginButtonText}"
          action="#{cc.attrs.loginAction}"/>
      </p>
    </h:form>    
  </composite:implementation>
</html>

 

map 组件一样,login 也可以使用一个 Ajax 升级。在下一个技巧介绍 Validation 时,我将展示如何将 Ajax 验证添加到 login 组件中。


技巧 2:Ajax 化

与非 Ajax HTTP 请求相比,Ajax 请求通常需要额外执行两个步骤:在服务器中对表单进行局部处理,接着在客户机上对 Document Object Model (DOM) 进行局部呈现。

局部处理和呈现

通过将 JSF 生命周期分解为两个不同的逻辑部分 —— 执行和呈现,JSF 2 现在支持局部处理和局部呈现。图 5 突出显示了执行部分:


图 5. JSF 生命周期的执行部分
JSF 生命周期的执行部分

图 6 突出显示了 JSF 生命周期的呈现部分:


图 6. JSF 生命周期的呈现部分
JSF 生命周期的呈现部分

将生命周期划分为执行和呈现部分的原理很简单:您可以指定 JSF 在服务器上执行(处理)的组件,以及在返回 Ajax 调用时 JSF 呈现的组件。将使用 JSF 2 中新增的 <f:ajax> 实现这个目的,如清单 7 所示:


清单 7. 一个 Ajax 缩放菜单

				
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
<h:selectOneMenu id="menu"
     value="#{cc.parent.attrs.location.zoomIndex}"
     style="font-size:13px;font-family:Palatino">

  <f:ajax event="change" execute="@this" render="map"/>
  <f:selectItems value="#{places.zoomLevelItems}"/>
             
</h:selectOneMenu>             
     
<m:map id="map"...>

 

清单 7清单 2 中的第一行所示的菜单进行了修改:我从 清单 2 中删除了 onchange 属性,并添加了一个 <f:ajax> 标记。这个 <f:ajax> 标记指定了以下内容:

  • 触发 Ajax 调用的事件
  • 在服务器上执行的组件
  • 在客户机上呈现的组件

当用户从缩放菜单中选择一个菜单项时,JSF 将对服务器发出 Ajax 调用。随后,JSF 将菜单传递给生命周期的执行部分(@this 表示 <f:ajax> 周围的组件),并在生命周期的 Update Model Values 阶段更新菜单的 zoomIndex。当 Ajax 调用返回后,JSF 呈现地图组件,后者使用(新设置的)缩放指数重新绘制地图,现在您就有了一个 Ajax 化的缩放菜单,其中添加了一行 XHTML。

但是还可以进一步简化,因为 JSF 为 eventexecute 属性提供了默认值。

每个 JSF 组件都有一个默认事件,当在组件标记内部嵌入 <f:ajax> 标记时,该事件将触发 Ajax 调用。对于菜单,该事件为 change 事件。这意味着我可以删除 清单 7 中的 <f:ajax>event 属性。<f:ajax>execute 属性的默认值是 @this,这表示围绕在 <f:ajax> 周围的组件。在本例中,该组件为菜单,因此还可以删除 execute 属性。

通过对 <f:ajax> 使用默认属性值,我可以将 清单 7 简化为清单 8:


清单 8. 简化后的 Ajax 缩放菜单

				
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
<h:selectOneMenu id="menu"
     value="#{cc.parent.attrs.location.zoomIndex}"
     style="font-size:13px;font-family:Palatino">

  <f:ajax render="map"/>
  <f:selectItems value="#{places.zoomLevelItems}"/>
             
</h:selectOneMenu>             

<m:map id="map"...>

 

这演示了使用 JSF 2 向组件添加 Ajax 有多么容易。当然,前面的例子非常简单:我仅仅是在用户选择某个缩放级别时重新绘制了地图而不是整个页面。验证表单中的各个字段等操作要更加复杂一些,因此接下来我将讨论这些用例。

验证

当用户移出某个字段后对字段进行验证并提供即时的反馈,这始终是一个好的做法。例如,在图 7 中,我使用了 Ajax 对名称字段进行了验证:


图 7. Ajax 验证
Ajax 验证

该名称字段的标记如清单 9 所示:


清单 9. 名称字段

				
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
<h:panelGrid columns="2">
  #{cc.attrs.namePrompt}
  <h:panelGroup>
    <h:inputText id="name" value="#{cc.attrs.managedBean.name}"
       valueChangeListener="#{cc.attrs.managedBean.validateName}">
       
       <f:ajax event="blur" render="nameError"/>
       
     </h:inputText>
     
     <h:outputText id="nameError" 
       value="#{cc.attrs.managedBean.nameError}"
       style="color: red;font-style: italic;"/>
  </h:panelGroup>     
  ... 
</h:panelGrid>

 

我再一次使用了 <f:ajax>,只不过这一次没有执行输入的默认事件 — change,因此我将 blur 指定为触发 Ajax 调用的事件。当用户移出名称字段时,JSF 将对服务器发出 Ajax 调用并在生命周期的执行部分运行 name 输入组件。这意味着 JSF 将在生命周期的 Process Validations 阶段调用 name 输入的值修改监听程序(在 清单 9 中指定)。清单 10 展示了这个值修改监听程序:


清单 10. validateName() 方法

				
package com.clarity

import javax.faces.context.FacesContext
import javax.faces.bean.ManagedBean
import javax.faces.bean.SessionScoped
import javax.faces.event.ValueChangeEvent 
import javax.faces.component.UIInput 
 
@ManagedBean()  
@SessionScoped    
       
public class User {    
  private String name, password, nameError;
 
  ...
  
  public void validateName(ValueChangeEvent e) {
    UIInput nameInput = e.getComponent()
    String name = nameInput.getValue()
    
    if (name.contains("_"))   nameError = "Name cannot contain underscores"
    else if (name.equals("")) nameError = "Name cannot be blank"
    else                      nameError = "" 
  }
  
  ...
}

 

这个修改值的监听程序(user 托管 bean 的 validateName() 方法)将验证名称字段并更新 user 托管 bean 的 nameError 属性。

返回 Ajax 调用后,借助 清单 9 中的 <f:ajax> 标记的 render 属性,JSF 呈现 nameError 输出。该输出显示了 user 托管 bean 的 nameError 属性。

多字段验证

在前面的小节中,我展示了如何对单一字段执行 Ajax 验证。但是,有些情况下,需要同时对多个字段进行验证。比如,图 8 展示了 places 应用程序同时验证名称和密码字段:


图 8. 验证多个字段
验证多个字段

我在用户提交表单时同时验证了名称和密码字段,因此对这个例子不需要用到 Ajax。相反,我将使用 JSF 2 的新事件系统,如清单 11 所示:


清单 11. 使用 <f:event>

				
<h:form id="form" prependId="false">
  
  <f:event type="postValidate" 
       listener="#{cc.attrs.managedBean.validate}"/>
  ...
</h:form>

<div class="error" style="padding-top:10px;">
  <h:messages layout="table"/>
</div>

 

清单 11 中,我使用了 <f:event> — 类似于 <f:ajax> ,它是 JSF 2 中新增的内容。<f:event> 标记在另一方面还类似于 <f:ajax>:使用起来很简单。

将一个 <f:event> 标记放到组件标记的内部,当该组件发生指定的事件(使用 type 属性指定)时,JSF 将调用一个使用 listener 属性指定的方法。因此,<f:event> 标记在 清单 11 中的含义就是:对表单进行验证后,对用户传递给这个复合组件的托管 bean 调用 validate() 方法。该方法如清单 12 所示:


清单 12. validate() 方法

				
package com.clarity

import javax.faces.context.FacesContext
import javax.faces.bean.ManagedBean
import javax.faces.bean.SessionScoped
import javax.faces.event.ValueChangeEvent 
import javax.faces.component.UIInput 
 
@ManagedBean()  
@SessionScoped    
       
public class User {    
  private final String VALID_NAME     = "Hiro";
  private final String VALID_PASSWORD = "jsf";
  
  ...
  
  public void validate(ComponentSystemEvent e) {
    UIForm form = e.getComponent() 
    UIInput nameInput = form.findComponent("name")
    UIInput pwdInput = form.findComponent("password")
    
    if ( ! (nameInput.getValue().equals(VALID_NAME) &&
        pwdInput.getValue().equals(VALID_PASSWORD))) {
      
      FacesContext fc = FacesContext.getCurrentInstance()
      fc.addMessage(form.getClientId(), 
        new FacesMessage("Name and password are invalid. Please try again."))
      fc.renderResponse()
    }
  }
  
  ...
}

 

JSF 将一个组件系统事件传递给 清单 12 中的 validate() 方法,方法从这个事件中获得对(适用于事件的)组件的引用 — 登录表单。对于这个表单,我使用 findComponent() 方法获得名称和密码组件。如果这些组件的值不为 Hiro 和 jsf,那么我将把一条消息存储到 faces 上下文并要求 JSF 继续处理生命周期的 Render Response 阶段。通过这种方法,就可以避免 Update Model Values 阶段,后者会将坏的名称和密码传递给模型(见 图 5)。

您可能已经注意到,清单 10清单 12 中的验证方法是使用 Groovy 编写的。与 清单 4 不同,后者使用 Groovy 的惟一好处就是避免了分号和返回语句,清单 10清单 12 中的 Groovy 代码使我不必进行类型转换。例如,在 清单 10 中,ComponentSystemEvent.getComponent()UIComponent.findComponent() 都返回类型 UIComponent。对于 Java 语言,我需要转换这些方法的返回值。Groovy 为我做了这一转换工作。


技巧 3:展示进度

Ajax 化 中,我展示了如何为 map 组件 Ajax 化缩放菜单,因此,当用户修改缩放级别时,places 应用程序将只重新绘制页面的地图部分。另一个常见 Ajax 用例是向用户提供反馈,表示一个 Ajax 事件正在处理中,如图 9 所示:


图 9. 进度条
进度条

图 9 中,我将使用一个动画 GIF 替换缩放菜单,这个动画 GIF 将在 Ajax 调用期间显示。当 Ajax 调用完成后,我将使用缩放菜单替换进度指示器。清单 13 展示了这一过程:


清单 13. 监视 Ajax 调用

				
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
<h:selectOneMenu id="menu"
     value="#{cc.parent.attrs.location.zoomIndex}"
     style="font-size:13px;font-family:Palatino">

  <f:ajax render="map" onevent="zoomChanging"/>
  <f:selectItems value="#{places.zoomLevelItems}"/>

  ...             
</h:selectOneMenu>
...
<h:graphicImage id="progressbar" style="display: none" 
              library="images" name="orange-barber-pole.gif"/>

 

清单 13 中,我添加了一个进度条图像(该图像最初不会显示出来),并为 <f:ajax> 指定了 onevent 属性。该属性引用一个 JavaScript 函数,如 清单 14 所示,这个函数将在 清单 13 中的 Ajax 调用被初始化时由 JSF 调用:


清单 14. 响应 Ajax 请求的 JavaScript

				
function zoomChanging(data) {
  var menuId = data.source.id;
  var progressbarId = menuId.substring(0, menuId.length - "menu".length)
      + "progressbar";

  if (data.name == "begin") {
    Element.hide(menuId);
    Element.show(progressbarId);
  } 
  else if (data.name == "success") {
    Element.show(menuId);
    Element.hide(progressbarId);
  }
}

 

JSF 向 清单 14 中的函数传递一个对象,该对象中包含有一些信息,比如触发了事件的组件的客户机标识符(在本例中为缩放级别菜单),以及 Ajax 请求的当前状态,使用 name 属性表示。

清单 14 中的 zoomChanging() 函数计算进度条图像的客户机标识符,然后使用 Prototype Element 对象在 Ajax 调用期间隐藏和显示对应的 HTML 元素。

你可能感兴趣的:(JavaScript,Ajax,JSF)