事件处理,JavaScript和Ajax

JSF的最大卖点之一是它是一个基于组件的框架。 这意味着您可以实现您或其他人可以重复使用的组件。 在大多数情况下,这种强大的重用机制在JSF 1中变得无关紧要,因为很难实现组件。

但是,正如您在第2部分中所看到的那样,JSF 2借助一种称为复合组件的新功能,可以轻松地实现组件-无需Java代码也无需配置。 该功能很可能是JSF 2最重要的部分,因为它最终实现了JSF组件的潜力。

在有关JSF 2的第三篇也是最后一篇文章中,我将向您展示如何通过使用JSF 2中还引入的新Ajax和事件处理功能来构建复合组件功能,并提供以下技巧,以充分利用JSF 2:

  • 提示1:组件化
  • 提示2:Ajaxify
  • 提示3:显示进度

在第一个技巧中,我将简要回顾我在第2部分中详细讨论的两个组件。 在随后的技巧中,我将向您展示如何使用Ajax和事件处理来转换那些组件。

提示1:组件化

我在第1部分中介绍的places应用程序包含许多复合组件。 一个是map组件,它显示一个地址地图,并带有一个缩放级别的下拉菜单,如图1所示:

图1. places应用程序的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中,我已经更换了image组件从清单1用谷歌地图组件,GMaps4JSF的礼貌(参见相关主题 ):

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

清单2中显示了map组件的更新(和截断)代码:

清单2.用GMaps4JSF组件替换地图图像
<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>标记。 通过为<m:map>标记的zoom属性指定正确的backing-bean属性,将GMaps4JSF组件挂接到zoom下拉菜单也很简单。

说到缩放级别,请注意,当用户更改缩放级别时,我强制使用<h:selectOneMenu>onchange属性提交表单,如清单1的第一部分黑体所示 。 该表单提交触发了JSF生命周期,最终将缩放级别的新值推入存储在父复合组件中的location bean的zoomIndex属性中。 该bean属性绑定到清单2的第一行中的输入组件。

因为我没有为与缩放级别更改相关联的表单提交指定任何导航,所以JSF在处理请求后刷新了同一页面,重新绘制了地图图像以反映新的缩放级别。 但是,即使唯一的更改是在地图图像中,该页面刷新也会重绘整个页面。 在技巧2:Ajaxify中 ,我将向您展示如何使用Ajax来仅重绘图像以响应缩放级别的变化。

login组件

场所应用程序中使用的另一个组件是login组件。 图3显示了运行中的登录组件:

图3. 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 :登录操作方法
  • managedBean :具有名称和密码属性的托管Bean

清单3中显示了清单3中指定的托管bean:

清单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语言不会给我带来很多好处,除了使我摆脱分号和return语句的繁琐工作之外。 但是,在技巧2的“ 验证”部分中,我将向您展示一个更有说服力的理由,将Groovy用于User管bean。

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

图4.完全配置的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验证添加到登录组件。

提示2:Ajaxify

对于非Ajax HTTP请求,Ajax通常需要完成两个通常不执行的步骤:服务器上表单的部分处理,以及客户端上文档对象模型(DOM)的后续部分呈现。

部分处理和渲染

JSF 2通过将JSF生命周期分为两个不同的逻辑部分来支持部分处理和部分渲染:执行和渲染。 图5突出显示了执行部分:

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

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

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

生命周期的执行和呈现部分背后的想法很简单:您可以指定JSF在服务器上执行(处理)的组件,以及当Ajax调用返回时JSF呈现的组件。 使用<f:ajax> ,这是JSF 2的新增功能,如清单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>的周围组件),并在生命周期的“更新模型值”阶段更新菜单的zoomIndex 。 当Ajax调用返回时,JSF渲染地图组件,该组件使用(新设置的)缩放索引重绘地图,现在您有了Ajaxified缩放菜单,其中增加了一行XHTML。

但是事情会变得更加简单,因为JSF为event提供了默认值并execute属性。

每个JSF组件都有一个默认事件,如果您将<f:ajax>标记嵌入到组件标记中,则该事件会触发Ajax调用。 对于菜单,该事件为change事件。 这意味着我可以摆脱清单7中的<f:ajax>event属性。 <f:ajax>execute属性的@this值为@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中显示了name字段的标记:

清单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将在生命周期的流程验证阶段调用清单9中指定的name输入的值更改侦听器。 清单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属性指定的方法。 因此,用英语来说, 清单11中<f:event>标记的含义是:验证表单之后,在用户传递给此复合组件的托管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阶段。 这样,我避免了“更新模型值”阶段,该阶段会将错误名称和密码推送到模型中(参见图5 )。

您可能已经注意到, 清单10清单12中的验证方法是用Groovy编写的。 与清单4不同, 清单4中使用Groovy的唯一优势是无需使用分号和return语句, 清单10清单12中的Groovy代码使我摆脱了强制转换。 例如,在清单10中ComponentSystemEvent.getComponent()UIComponent.findComponent()都返回类型UIComponent 。 使用Java语言,我将不得不转换那些方法的返回值。 Groovy为我做演员。

提示3:显示进度

Ajaxify中 ,我向您展示了如何Ajaxify map组件的缩放菜单,以便当用户更改缩放级别时,places应用程序仅重绘页面的地图部分。 另一个常见的Ajax用例是向用户提供有关Ajax事件正在进行中的一些反馈,如图9所示:

图9.进度条
进度条

图9中 ,我用动画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属性。 该属性引用清单14中所示JavaScript函数,而清单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不佳的name属性。

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

结论

多年来,JSF 1作为难以使用的框架而享有盛誉。 在许多方面,这种声誉是应得的。 JSF 1是在象牙塔中开发的,没有现实使用中的大量后见之明。 结果,JSF使实现应用程序和组件的难度比原先要困难得多。

另一方面,JSF 2是从现实经验的坩埚中诞生的,这些人是在JSF 1之上实施开源项目的人们。现实世界的事后见识导致了一个更为精明的框架,使其易于实施。强大的Ajaxified应用程序。

在整个系列中,我向您展示了一些最杰出的JSF 2功能,例如用于替换配置的注释和约定,简化的导航,对资源的支持,复合组件,内置的Ajax和扩展的事件模型。 但是JSF 2具有我在本系列中没有提到的更多功能,例如View和Page范围,对可标记页面的支持以及Project Stage。 所有这些功能以及更多功能,使JSF 2对其前身进行了巨大的改进。


翻译自: https://www.ibm.com/developerworks/java/library/j-jsf2fu3/index.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值