JSF 2 fu: Ajax 组件

通过 JSF 2 实现可重用的 Ajax 化组件

Java™Server Faces (JSF) 2 专家组成员 David Geary 开始了新一期的文章系列,这次他将深入讨论 JSF 2 技术。在本期的文章中,您将了解如何集成 JSF 2 的复合组件与框架的 Ajax 开发支持。

David Geary, 总裁, Clarity Training, Inc.

David GearyDavid Geary 是一名作家、演讲家和顾问,也是 Clarity Training, Inc. 的总裁,他指导开发人员使用 JSF 和 Google Web Toolkit (GWT) 实现 Web 应用程序。他是 JSTL 1.0 和 JSF 1.0/2.0 专家组的成员,与人合作编写了 Sun 的 Web Developer 认证考试的内容,并且为多个开源项目作出贡献,包括 Apache Struts 和 Apache Shale。David 的 Graphic Java Swing 一直是关于 Java 的畅销书籍,而 Core JSF(与 Cay Horstman 合著)是关于 JSF 的畅销书。他还是 GWT Solutions 一书的作者。David 经常在各大会议和用户组发表演讲。他从 2003 年开始就一直是 NFJS tour 的定期演讲人,并且在 Java University 教授课程,三次当选为 JavaOne 之星。



2010 年 6 月 08 日

在 JSF 2 的众多新特性中,最引人注目的两个特性是饱受争议的复合组件和 Ajax 支持。但是,它们两者相结合时的强大之处是显而易见的:轻而易举地实现支持 Ajax 自定义组件。

关于本系列

JSF fu 系列建立在 David Geary 的 同名简介文章 的概念的基础之上。本系列将深入探究 JSF 2 及其生态系统,同时还将介绍如何将一些 Java EE 技术,如 Contexts 和 Dependency Injection,与 JSF 相集成。

在本文中,我将向您介绍如何实现自动完成组件,它将使用 Ajax 管理其完成项列表。在此过程中,您将了解如何将 Ajax 集成到您自己的复合组件中。

本系列的代码基于在企业容器,如 GlassFish 或 Resin,中运行的 JSF 2。本文的 最后一部分 将详细讨论如何使用 GlassFish 来安装和运行本文的代码。

JSF 自动完成自定义组件

因谷歌搜索字段而闻名的自动完成字段(也称作建议框)是许多 Web 应用程序的组合。它们也是 Ajax 的典型应用。自动完成字段随带了许多 Ajax 框架,比如 Scriptaculous 和 JQuery,如 图 1— AjaxDaddy 的自动完成组件集成(参阅 参考资料)— 所示:

图 1. AjaxDaddy 自动完成组件
AjaxDaddy 屏幕快照

本文将讨论如何使用 JSF 来实现支持 Ajax 的自动完成字段。您将了解如何实现如 图 2 所示的自动完成字段,其中将显示一个简短的虚拟国家列表(选自 Wikipedia 的“虚拟国家列表”一文;请参阅 参考资料):

图 2. 自动完成字段
自动完成字段

图 3图 4 显示了运行中的自动完成字段。在图 3 中,在字段中输入 Al 之后,国家列表将缩减至使用这两个字母开头的名称:

图 3. 使用 Al 开头的完成项目
使用 Al 开头的完成项目

同样,图 4 显示了在字段中输入 Bar 之后显示的结果。列表仅显示以 Bar 开头的国家名。

图 4. 以 Bar 开头的完成项目
以 Bar 开头的完成项目

使用自动完成组件

复合组件:基础

如果您不熟悉如何实现 JSF 2 复合组件,那么您可以从 “JSF 2 简介,第 2 部分:模板及复合组件” 这篇文章中了解基本知识。

Locations 自动完成字段是一个 JSF 复合组件,并应用于 facelet,如 清单 1 所示:

清单 1. facelet
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:util="http://java.sun.com/jsf/composite/util">
   <h:head>
      <title>#{msgs.autoCompleteWindowTitle}</title>
   </h:head>

   <h:body>
      <div style="padding: 20px;">
         <h:form>
            <h:panelGrid columns="2">
               #{msgs.locationsPrompt}
               <util:autoComplete value="#{user.country}"
                     completionItems="#{autoComplete.countries}" />
            </h:panelGrid>
         </h:form>
      </div>
   </h:body>
</html>

清单 1 中的 facelet 通过声明适当的名称空间 —util— 以及借助组件的相关标记(<util:autoComplete>)来使用 autoComplete 复合组件。

注意 清单 1<util:autoComplete> 标记的两个属性:

  • value 是名称为 user 的托管 bean 的国家属性。
  • completionItems 是字段的完成项目的初始集。

User 类是一个简单的托管 bean,专为本例而设计。其代码如 清单 2 所示:

清单 2. User
package com.corejsf;

import java.io.Serializable;

import javax.inject.Named; 
import javax.enterprise.context.SessionScoped; 

@Named()
@SessionScoped
public class User implements Serializable {
  private String country;
  public String getCountry() { return country; }
  public void setCountry(String country) { this.country = country; }
}

请注意 @Named 注释,它与 @SessionScoped 一起实例化了一个名称为 user 的托管 bean,并在 JSF 第一次在 facelet 中遇到 #{user.country} 时将它置于 session 作用域中。此应用程序中唯一的 #{user.country} 引用发生在 清单 1 中,其中,我将 user 托管 bean 的 country 属性指定为 <util:autoComplete> 组件的值。

清单 3 显示了 AutoComplete 类,该类定义了 countries 属性,即自动完成组件的完成项目列表:

清单 3. 完成项目
package com.corejsf;

import java.io.Serializable;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;

@Named
@ApplicationScoped
public class AutoComplete implements Serializable {
   public String[] getLocations() {
      return new String[] {
    		  "Abari", "Absurdsvanj", "Adjikistan", "Afromacoland",
    		  "Agrabah", "Agaria", "Aijina", "Ajir", "Al-Alemand",
    		  "Al Amarja", "Alaine", "Albenistan", "Aldestan",
    		  "Al Hari", "Alpine Emirates", "Altruria",
    		  "Allied States of America", "BabaKiueria", "Babalstan",
    		  "Babar's Kingdom","Backhairistan", "Bacteria",
    		  "Bahar", "Bahavia", "Bahkan", "Bakaslavia",
    		  "Balamkadar", "Baki", "Balinderry", "Balochistan",
    		  "Baltish", "Baltonia", "Bataniland, Republic of",
    		  "Bayview", "Banania, Republica de", "Bandrika",
    		  "Bangalia", "Bangstoff", "Bapetikosweti", "Baracq",
    		  "Baraza", "Barataria", "Barclay Islands",
    		  "Barringtonia", "Bay View", "Basenji",
      };
   }
}

自动完成组件的使用方法已经介绍完毕。现在,您将了解它的工作原理。


自动完成组件的工作原理

自动完成组件是一个 JSF 2 复合组件,因此,与大多数复合组件相同,它是在 XHTML 文件中实现的。组件包括一个文本输入和一个列表框,以及一些 JavaScript 代码。最开始,列表框 styledisplay: none,其作用是让列表框不可见。

自动完成组件响应三个事件:

  • 文本输入中的 keyup 事件
  • 文本输入中的 blur (失焦) 事件
  • 列表框中的 change (选择) 事件

当用户在文本输入中键入内容时,自动完成组件会调用每个 keyup 事件的 JavaScript 函数。该函数结合键盘输入事件,以不大于 350ms 的间隔定期调用 Ajax。因此,在响应文本输入中的 keyup 事件时,自动完成组件会以不大于 350ms 的间隔定期向服务器发起 Ajax 调用。(其作用是防止快速输入时的大量 Ajax 调用将服务器淹没。在实践中,结合事件的频率可能会稍高,但这足以演示如何在 JavaScript 中结合事件,同时这是一个非常实用的工具。)

当用户从列表框中选择项目时,自动完成组件会向服务器发起另一个 Ajax 调用。

文本输入和列表框都附加了监听程序,它们在 Ajax 调用期间完成与服务器相关的大部分有意义的工作。在响应 keyup 事件时,文本输入的监听程序会更新列表框的完成项目。在响应列表框的选择事件时,列表框的监听程序会将列表框的选中项目复制到文本输入中,并隐藏列表框。

现在,您已经了解了自动完成组件的工作原理。接下来,我们来看看它的具体实现。


实现自动完成组件

自动完成组件实现包括以下工件:

  • 一个复合组件
  • 一系列 JavaScript 函数
  • 一个用于更新完成项目的值变更监听程序

我将从 清单 4 开始复合组件:

清单 4. autoComplete 组件
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    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>
      <composite:attribute name="value" required="true"/>
      <composite:attribute name="completionItems" required="true"/>
    </composite:interface> 

    <!-- IMPLEMENATION -->          
    <composite:implementation>
      <div id="#{cc.clientId}">
        <h:outputScript library="javascript" 
           name="prototype-1.6.0.2.js" target="head"/>
        
        <h:outputScript library="javascript" 
           name="autoComplete.js" target="head"/>
      
        <h:inputText id="input" value="#{cc.attrs.value}" 
           onkeyup="com.corejsf.updateCompletionItems(this, event)"
           onblur="com.corejsf.inputLostFocus(this)"
           valueChangeListener="#{autocompleteListener.valueChanged}"/>
            
        <h:selectOneListbox id="listbox" style="display: none"
           valueChangeListener="#{autocompleteListener.completionItemSelected}">
        
            <f:selectItems value="#{cc.attrs.completionItems}"/>
            <f:ajax render="input"/>
          
        </h:selectOneListbox>
      <div>
    </composite:implementation>    
</ui:composition>

清单 4 的实现部分完成了三项任务。首先,该组件发起 Ajax 调用以响应文本输入中的 keyup 事件,并通过分配给文本输入中的 keyupblur 事件的 JavaScript 函数在文本输入失焦时隐藏列表框。

其实,该组件通过 JSF 2 的 <f:ajax> 标记来发起 Ajax 调用来响应列表框中的 change 事件。当用户从列表框中进行选择时,JSF 会向服务器发起一个 Ajax 调用,并在 Ajax 调用返回时更新文本输入的值。

<div> 中封装复合组件

清单 4 中的复合组件通过复合组件的客户机标识符将其实现封装在 <div> 中。 这样,其他组件便可通过其客户机 ID 来引用自动完成组件。举例来说,另一个组件可能会希望在 Ajax 调用期间执行或呈现一个或多个自动完成组件。

第三,文本输入和列表框都附加了相应的值变更监听程序,因此当 JSF 发起 Ajax 调用来响应用户在文本输入中的键入操作时,JSF 会调用服务器上的文本输入的值变更监听程序。当用户从列表框中选择项目时,JSF 会向服务器发起一个 Ajax 调用并调用列表框的值变更监听程序。

清单 5 显示了自动完成组件所使用的 JavaScript:

清单 5. JavaScript
if (!com)
   var com = {}

if (!com.corejsf) {
   var focusLostTimeout
   com.corejsf = {
      errorHandler : function(data) {
         alert("Error occurred during Ajax call: " + data.description)
      },

      updateCompletionItems : function(input, event) {
         var keystrokeTimeout

         jsf.ajax.addOnError(com.corejsf.errorHandler)

         var ajaxRequest = function() {

            jsf.ajax.request(input, event, {
               render: com.corejsf.getListboxId(input),
               x: Element.cumulativeOffset(input)[0],
               y: Element.cumulativeOffset(input)[1]
                     + Element.getHeight(input)
            })
         }

         window.clearTimeout(keystrokeTimeout)
         keystrokeTimeout = window.setTimeout(ajaxRequest, 350)
      },

      inputLostFocus : function(input) {
         var hideListbox = function() {
            Element.hide(com.corejsf.getListboxId(input))
         }

         focusLostTimeout = window.setTimeout(hideListbox, 200)
      },

      getListboxId : function(input) {
         var clientId = new String(input.name)
         var lastIndex = clientId.lastIndexOf(':')
         return clientId.substring(0, lastIndex) + ':listbox'
      }
   }
}

清单 5 中的 JavaScript 包括三个函数,我把它们放置在 com.corejsf 名称空间的内部。我实现了名称空间(从技术上说是一个 JavaScript 字面对象),以防止其他人有意或无意修改我的三个函数。

如果这些函数未包含在 com.corejsf 中,则其他人可以实现自己的 updateCompletionItems 函数,从而将我的实现替换成它们。一些 JavaScript 库可以实现一个 updateCompletionItems 函数,但最理想的情况是任何人都不用设计 com.corejsf.updateCompletionItems。(相反,抛弃 com,并使用 corejsf.updateCompletionItems 可能已经足够,但有时会难以控制。)

因此,这些函数做了些什么? updateCompletionItems() 函数向服务器发起 Ajax 请求 — 通过调用 JSF 的 jsf.ajax.request() 函数 — 要求 JSF 仅在 Ajax 调用返回时呈现列表框组件。 updateCompletionItems() 函数还传递了两个额外的参数到 jsf.ajax.request() 中:列表框左上角的 x 和 y 坐标。jsf.ajax.request() 函数会将这些函数参数转换为通过 Ajax 调用发送的请求参数。

JSF 会在文本输入失焦时调用 inputLostFocus() 函数。该函数的作用是使用 Prototype 的 Element 对象来隐藏列表框。

updateCompletionItems()inputLostFocus() 将它们的功能存储在一个函数中。然后,它们安排自己的函数分别在 350ms 和 200ms 时执行。换句话说,每个函数都有各自的任务,但它会让任务延时 350ms 或 200ms。文本输入会在 keyup 事件后延时,因此,updateCompletionItems() 方法会最多每隔 350ms 发送一个 Ajax 请求。其思想是,如果用户输入速度极快,则不会让 Ajax 调用淹没服务器。

inputLostFocus 函数会在文本输入失焦时调用,并延时其任务 200ms。这种延时是必要的,因为该值会在 Ajax 调用返回时复制到列表框之外,并且列表框必须为可见才能确保它正常运行。

最后,请注意 getListBoxId() 函数。这个帮助器函数会从文本输入的客户机标识符中获取列表框的客户机标识符。该函数可以完成此任务,因为它将与 清单 4 中的 autoComplete 组件相结合。 autoComplete 组件将 inputlistbox 分别指定为文本框和列表框的组件标识符,因此 getListBoxId() 函数会删除 input 并附加 listbox,以便获取文本输入的客户机标识符。

清单 6 显示了监听程序的最终实现:

清单 6. 监听程序
package com.corejsf;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.enterprise.context.SessionScoped;
import javax.faces.component.UIInput;
import javax.faces.component.UISelectItems;
import javax.faces.component.UISelectOne;
import javax.faces.context.FacesContext;
import javax.faces.event.ValueChangeEvent;
import javax.inject.Named;

@Named
@SessionScoped
public class AutocompleteListener implements Serializable {
   private static String COMPLETION_ITEMS_ATTR = "corejsf.completionItems";
  
   public void valueChanged(ValueChangeEvent e) {
      UIInput input = (UIInput)e.getSource();
      UISelectOne listbox = (UISelectOne)input.findComponent("listbox");

      if (listbox != null) {
         UISelectItems items = (UISelectItems)listbox.getChildren().get(0);
         Map<String, Object> attrs = listbox.getAttributes();
         List<String> newItems = getNewItems((String)input.getValue(),
            getCompletionItems(listbox, items, attrs));

         items.setValue(newItems.toArray());
         setListboxStyle(newItems.size(), attrs);
      }
   }
  
   public void completionItemSelected(ValueChangeEvent e) {
     UISelectOne listbox = (UISelectOne)e.getSource();
     UIInput input = (UIInput)listbox.findComponent("input");
    
     if(input != null) {
        input.setValue(listbox.getValue());
     }
     Map<String, Object> attrs = listbox.getAttributes();
     attrs.put("style", "display: none");
   }
   
   private List<String> getNewItems(String inputValue, String[] completionItems) {
      List<String> newItems = new ArrayList<String>();
    
      for (String item : completionItems) {
         String s = item.substring(0, inputValue.length());
         if (s.equalsIgnoreCase(inputValue))
           newItems.add(item);
      }
    
      return newItems;
   }
  
   private void setListboxStyle(int rows, Map<String, Object> attrs) {
      if (rows > 0) {
         Map<String, String> reqParams = FacesContext.getCurrentInstance()
            .getExternalContext().getRequestParameterMap();
      
         attrs.put("style", "display: inline; position: absolute; left: "
             + reqParams.get("x") + "px;" + " top: " + reqParams.get("y") + "px");

         attrs.put("size", rows == 1 ? 2 : rows);
      }
      else
         attrs.put("style", "display: none;");
   }

   private String[] getCompletionItems(UISelectOne listbox,
      UISelectItems items, Map<String, Object> attrs) {
         Strings] completionItems = (String[])attrs.get(COMPLETION_ITEMS_ATTR);
    
         if (completionItems == null) {
            completionItems = (String[])items.getValue();
            attrs.put(COMPLETION_ITEMS_ATTR, completionItems);
         }
      return completionItems;
   }
}

JSF 在 Ajax 调用期间调用监听程序的 valueChanged() 方法来响应文本输入中的 keyup 事件。该方法会创建一组新的完成项目,然后将列表框的项目设置为这个新的项目集。该方法还会设置列表框的样式属性,以确定 Ajax 调用返回时是否显示列表框。

清单 6 中的 setListboxStyle() 方法将使用 xy 请求我在发起 清单 5 中的 Ajax 调用时指定的参数值。

JSF 会在 Ajax 调用期间调用监听程序的其他公共方法 completionItemSelected(),以响应列表框中的选择事件。该方法会将列表框的值复制到文本输入中,并隐藏列表框。

请注意,valueChanged() 方法还会将原始完成项目存储在列表框的某个属性中。由于每个 autoComplete 组件都维护自己的完成项目列表,因此多个 autoComplete 组件可以在相同页面中和谐共存,而不会影响彼此的完成项目。


使用 GlassFish 和 Eclipse 运行示例

本系列中的代码适合在 JEE 6 容器中运行,比如 GlassFish 或 Resin。您可以通过调整让它们适应 servlet 容器,但这并非理想方案。因此,我的目标是侧重于充分发挥 JSF 2 和 JEE 6 的潜力,而不是配置问题。我仍然坚持使用 GlassFish v3。

在本文的其余部分,我将向您展示如何使用 GlassFish v3 和 Eclipse 来运行本文的 示例代码。此处的说明还适用于本系列其他文章的代码。(我将使用 Eclipse 3.4.1,因此最好是使用与之相近的版本。)

图 5 展示了本文代码的目录结构。(请参见 下载,立即获取代码。)其中的 autoComplete 目录包含应用程序和一个空的 Eclipse 工作空间目录。

图 5. 本文下载部分的源代码
目录结构的屏幕快照

现在,您已经下载了代码,接下来就可以运行它了。首先,您需要 GlassFish Eclipse 插件,可从 https://glassfishplugins.dev.java.net 下载它,如 图 6 所示:

图 6. GlassFish Eclipse 插件
GlassFish eclipse 插件页面屏幕快照

请依照插件的安装说明操作。

要安装本文的代码,请在 Eclipse 中创建一个 Dynamic Web 项目。为此,可以通过 File > New 菜单来实现:如果未看到 Dynamic Web Project,那么请选择 Other,并在接下来的对话框中打开 Web 文件夹并选择 Dynamic Web Project,如 图 7 所示:

图 7. 创建一个 Dynamic Web 项目
Dynamic Web 项目对话框的屏幕快照

下一步是配置项目。在 New Dynamic Web Project 向导的第一个页面中做出以下选择,如 图 8 所示:

  1. Project contents 下,保留 Use default 框为未选中状态。在 Directory 字段中,输入(或浏览到)示例代码的 autoComplete 目录。
  2. 对于 Target Runtime,请选择 GlassFish v3 Java EE 6。
  3. 对于 Dynamic Web Module version,请输入 2.5
  4. 对于 Configuration,请选择 Default Configuration for GlassFish v3 Java EE 6。
  5. EAR Membership 下,保留 Add project to an EAR 框为未选中状态,并在 EAR Project Name: 字段中输入 autoCompleteEAR
图 8. 配置应用程序,步骤 1
创建 Dynamic Web 项目的屏幕快照

单击 Next,然后输入如图 9 所示的值:

  1. 对于 Context Root:,输入 autoComplete
  2. 对于 Content Directory:,输入 web
  3. 对于 Java Source Directory:,输入 src/java。保留 Generate deployment descriptor 框为未选中状态。
图 9. 配置应用程序,步骤 2
配置 Web 模块的屏幕快照

现在,您应该已经建立了一个 autoComplete 项目,它将显示在 Eclipse 的 Project Explorer 视图中,如 图 10 所示:

图 10. autoComplete 项目
Eclipse 的 Project Explorer 视图中的 autoComplete 项目

现在,选择项目,右键单击它并选择 Run on Server,如 图 11 所示:

图 11. 使用 Eclipse 在服务器上运行
使用 Eclipse 在服务器上运行时的菜单项屏幕快照

Run On Server 对话框的服务器列表中选择 GlassFish v3 Java EE 6,如 图 12 所示:

图 12. 选择 GlassFish
在 Run On Server 中选择了 GlassFish 的屏幕快照

单击 Finish。Eclipse 应该会相继启动 GlassFish 和 autoComplete 应用程序,如 图 13所示:

图 13. 在 Eclipse 中运行
在 Eclipse 中运行

结束语

借助 JSF 2,开发人员可以轻松地创建功能强大、支持 Ajax 的自定义组件。您不需要在 XML 中实现基于 Java 的组件或呈现器,或者集成第三方 JavaScript 来发起 Ajax 调用。借助 JSF 2,您只需要使用几乎与任何 JSF 2 facelet 视图相同的标记来创建一个复合组件,并根据需求添加一些 JavaScript 或 Java 代码,以及 voilà — 您将实现一个奇妙自定义组件,为应用程序用户提供极为方便的数据输入功能。

JSF fu 的下一期中,我将讨论实现 Ajax 化 JSF 自定义组件的更多方面,比如将 <f:ajax> 标记集成到您的自定义组件中,以参与其他人发起的 Ajax。


下载

描述名字大小
本文的示例代码j-jsf2fu-0410-src.zip39KB

参考资料

学习

获得产品和技术

  • JSF:下载 JSF 2.0。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development
ArticleID=494652
ArticleTitle=JSF 2 fu: Ajax 组件
publish-date=06082010