构建支持 Ajax 的自动完成和级联式下拉控件

来源:互联网 发布:吐槽大会 王刚 知乎 编辑:程序博客网 时间:2024/06/01 21:48

构建支持 Ajax 的自动完成和级联式下拉控件

利用 JSP TagLib、JSON 和 Ajax

 

级别: 中级

 

 

Brian J Stewart, 首席顾问, Aqua Data Technologies, Inc.

本文介绍如何构建可在业务线应用程序中使用的 Asynchronous JavaScript + XML (Ajax) 控件。这些基于 JSP TagLib 的可配置控件利用 JavaScript Serialized Object Notation (JSON)、JavaScript 和 CSS。它们是标准的 JSP Taglib 控件,本文将展示可多么轻松地将其拖放到任意应用程序之中,从而提供更加直观、更具响应性的用户界面。

Ajax 和 JSON 是支持新一代 Web 站点的两种关键技术。业务线应用程序可受益于这些技术,从而提供更加直观、更具响应性的用户界面。这篇文章描述了如何基于 Ajax 构建可重用的 JSP Taglib 控件,为 Java? Platform, Enterprise Edition (Java EE) Web 应用程序添加 Ajax 和 JSON。

在这篇文章中,我介绍了如何构建级联式下拉控件,根据其他表单字段值动态填充 HTML SELECT 控件中的值。我还介绍了如何构建类似于 Google Suggest 的自动完成控件,在用户输入时显示实时更新的建议列表。您将通过集成 JSON、JavaScript、CSS、HTML 和 Java EE 技术来构建控件。

 

Ajax 资源中心

请访问 Ajax 资源中心,这是有关 Ajax 编程模型信息的一站式中心,包括很多文档、教程、论坛、blog、wiki 和新闻。任何 Ajax 的新信息都能在这里找到。

 

技术概览

本文中开发的控件的主要设计目标如下:

提供与现有 Web 应用程序的轻松集成。控件应封装所有逻辑和 JavaScript 代码,以简化部署流程。 可配置。 最小化数据大小和页面大小开销。 利用 CSS 和 HTML 标准。 提供跨浏览器的支持(Microsoft® Internet Explorer、Mozilla Firefox)。 利用通用设计模式/最佳实践来改进代码的可维护性。

为了实现可轻松集成和配置控件的目标,这篇文章的示例尽可能使用了可配置的标记属性。此外,我们还会定义接口/协议,提供将自定义数据/值提供者与控件相集成的直观方法。

本文还使用了额外的控件来封装通用 JavaScript 函数,从而最小化数据和开销。文中使用了 JSON,以便在进行异步调用时最小化数据交换。

本文的示例使用了 Web 标准,包括 CSS 和 HTML,目的在于提供跨浏览器支持。控件所发出的 JavaScript、HTML 和 CSS 已在 Internet Explorer 7.x 和 Mozilla Firefox 2.x/3.x 中通过测试。

数据和值提供者是基于通用的面向对象编程设计模式和最佳实践构建的,比如 n 层架构、适配器设计模式和基于接口的编程。



对于本文中开发的支持 Ajax 的控件,有一些技术事项需要考虑,包括为 Ajax 控件提供值的机制、用于异步通信的数据交换格式、类设计和数据模型。

在向支持 Ajax 的控件异步公开数据时,有三个选项:

本文使用的是 Servlet,原因在于其效率和最低的开销。JSP 页面实现起来比 Servlet 更加简单,但从实现的角度看来,它并不简洁。

支持 Ajax 的控件的数据提供者可使用 XML 或 JSON 作为数据交换格式。XML 的人类可读性通常优于 JSON,但有以下一些不足之处:

出于这些原因,本文使用了 JSON。

数据模型

示例应用程序的数据模型包含两个实体:

图 1 显示了本文中的示例页面所用的数据模型。


图 1. 数据模型

类模型

本文中的示例包含数据抽象层(DAL)、数据传输对象(DTO)、业务逻辑层(BLL)、表示层和用于支持的 helper 类。下图展示了这些类的 UML 类图。

helper 类提供数据库和表示层支持类(请参见图 2)。



构建支持 Ajax 的自动完成和级联式下拉控件 - ahang8415 - ahang8415的博客

数据抽象层包含一个类,用于向业务层提供关于位置的信息(请参见图 3)。


图 3. UML 类图 —— 数据抽象层类

您使用两个 DTO,在三??中传递数据(请参见图 4)。StateDTO 包含与州有关的数据、LocationDTO 包含与位置有关的数据,包括邮编、城市名称、州、纬度和经度。



构建支持 Ajax 的自动完成和级联式下拉控件 - ahang8415 - ahang8415的博客

业务逻辑层包含值提供者,为支持 Ajax 的控件提供数据(请参见图 5)。自动完成控件的值提供者必须实现 IJsonValueProvider 接口。位置服务从数据层接收 DTO 对象的集合,然后生成对应的 JSON 数据,在表示层使用。


图 5. UML 类图 —— 业务逻辑层类
构建支持 Ajax 的自动完成和级联式下拉控件 - ahang8415 - ahang8415的博客

Servlet 提供了一个接口,客户端异步调用将针对此接口执行(请参见图 6)。这些 Servlet 与值提供者交互,为 Web 浏览器提供 JSON 数据。



构建支持 Ajax 的自动完成和级联式下拉控件 - ahang8415 - ahang8415的博客


JSP TagLib 控件

您将创建以下支持 Ajax 的控件:

级联式下拉控件 —— 根据其他表单字段或业务规则,动态填充 SELECT 控件中的值选项。

除了两个 JSP TagLib 控件之外,您还需要另一个控件来封装所有可重用的 JavaScript 函数,如清除/填充值、处理键盘/鼠标事件、支持异步通信。图 7 展示了这三个控件类。



构建支持 Ajax 的自动完成和级联式下拉控件 - ahang8415 - ahang8415的博客


LocationDataService 类是从数据库中检索位置相关数据的数据提供者。它会返回一个 TreeMap 对象,其中包含 LocationDTO 和 StateDTO 对象。强烈建议数据提供者将结果缓存在内存中,以便优化性能,特别是通过异步服务器调用使用数据时。



可通过扩展 TagSupport 或 TagBodySupport 来创建 JSP TagLib 控件,通过覆盖 doStartTag()、doAfterBody() 或 doEndBody() 方法在页面处理过程中呈现控件的内容(HTML 代码、JavaScript)。清单 1 展示了覆盖 doStartTag 方法的一个示例。


清单 1. JdbcQuery 类

/* (non-Javadoc) * _cnnew1@see javax.servlet.jsp.tagext.TagSupport#doStartTag() */ @Override public int doStartTag() throws JspException { JspWriter out = pageContext.getOut(); try { // An example of rendering output within a JSP page out.print("This is a string that will be rendered"); // A more practical example out.print("<h1 >This is a Heading</h1>"); } catch (IOException e) { e.printStackTrace(); }

 

创建了 JSP TagLib 控件的实现之后,必须在 /WEB-INF/tlds 目录中定义 TagLib 库定义(TLD),如清单 2 所示。


<?xml version="1.0" encoding="ISO-8859-1" ?> <!DOCTYPE taglib PUBLIC "-//Sun Microsystems, Inc.//DTD JSP Tag Library 1.1//EN" "http://java.sun.com/j2ee/dtds/web-jsptaglibrary_1_1.dtd"> <taglib> <tlibversion>1.0</tlibversion> <jspversion>1.1</jspversion> <shortname>ajax</shortname> <info>Ajax control library</info> <tag> <name>sample</name> <tagclass>com.testwebsite.controls.SampleJspTag</tagclass> <bodycontent>JSP</bodycontent> <info> This is a sample control </info> <attribute> <name>id</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> </tag> </tagLib>

 

您可将控件置于任何 JSP 页面中,只需添加清单 3 所示代码。


清单 3. 示例 JSP 页面

<%@ page contentType="text/html; charset=ISO-8859-5" %> <%@ taglib prefix="ajax" uri="/WEB-INF/tlds/ajax_controls.tld"%> <html> <head> <title>This is a test page</title> <link href="core.css" rel="stylesheet" type="text/css" /> </head> <body> This is a test page. <ajax:sample/> </body> </html>




<ajax:page/> 控件呈现向 JSP 页面添加异步支持时所必须的标准 JavaScript 函数。它还会为 <ajax:autocomplete/> 和 <ajax:dropdown/> 控件呈现 helper 函数。构建自动完成 JSP TagLib 控件 和 构建级联式下拉 JSP TagLib 控件 这两个对应的控件的介绍部分将分别介绍 helper 函数。在可能的情况下,最好在 <ajax:page/> 控件中为 JavaScript 函数提供支持,而不要使用独立的控件,因为这样可以缩减页面的大小。此外,也可将其存储在一个外部 JS 文件中,但由于减少了控件内部的封装,因而会使部署复杂一些。

XMLHttpRequest 对象可在 JavaScript 中访问,它是异步 Web 通信的核心。遗憾的是,XMLHttpRequest 并非广泛认可的标准,厂商支持的标准往往稍有不同。对于 Opera、Mozilla Firefox 和 Microsoft Internet Explorer 7.0 及其更新版本来说,应使用 new XMLHttpRequest() JavaScript 语法。对于旧版本的 Microsoft Internet Explorer,可使用 new ActiveXObject('Microsoft.XMLHTTP') 创建对象。清单 4 展示了如何初始化 XMLHttpRequest 来实现跨浏览器支持。


var req; function initializeXmlHttpRequest() { if (window.ActiveXObject) { req=new ActiveXObject('Microsoft.XMLHTTP'); } else { req=new XMLHttpRequest(); } }

 

如前所述,可将清单 5 中的代码添加到 tag-implementation 类中,从而为页面呈现 JavaScript 代码。


/* (non-Javadoc) * @see javax.servlet.jsp.tagext.TagSupport#doStartTag() */ @Override public int doStartTag() throws JspException { StringBuffer html = new StringBuffer(); html.append("<script type='text/javascript' language='javascript'>"); html.append("var req;"); html.append("var cursor = -1;"); // Generate functions to support Ajax html.append("function initializeXmlHttpRequest() {"); // Support for non-Microsoft browsers (and IE7+) html.append("if (window.ActiveXObject) {"); // Support for Microsoft browsers html.append("req=new ActiveXObject('Microsoft.XMLHTTP');"); html.append("}"); html.append("else {"); html.append("req=new XMLHttpRequest();"); html.append("}"); JspWriter out = pageContext.getOut(); try { out.append(html.toString()); } catch (IOException e) { e.printStackTrace(); } return this.SKIP_BODY; }

 

req 变量现在可以在 Web 页面内全局使用。清单 6 展示了如何实现异步调用。


// If req object initialized if (req!=null) { // Set callback function req.onreadystatechange=stateName_onServerResponse; // Set status text in browser window window.status='Retrieving State data from server...'; // Open asynchronous server call req.open('GET',dataUrl,true); // Send request req.send(null); }

 

请求的就绪状态发生变化时,req.onreadystatechange 中指定的函数将被调用。req.readystate 包含以下状态码之一:

使用 XML 作为数据交换格式

XMLHttpRequest 对象也有一个 responseXML 属性,用于检索 XML 数据响应。随后,JavaScript 的 DOM 则可用于处理。

通常,Loaded 以外的内容都会被忽略,因为在服务器响应完成之前通常不需要采取任何操作。异步调用的 Loaded 值并不能保证成功。与其他任何 Web 页面请求类似,有可能无法找到页面或出现其他问题。如果 req.status 是 200 以外的值,则将出错。清单 7 展示了如何处理服务器响应。


function stateName_onServerResponse() { if(req.readyState!=4) return; if(req.status != 200) { alert('An error occurred retrieving data.'); return; } // Obtain server response var responseData = req.responseText; ... Processing of result }

 

现在,您对进行异步调用和处理响应已经有了基本的认识。下面将开始构建第一个控件:<ajax:autocomplete/>。



要构建自动完成控件,需完成以下步骤:

下面几节将详细介绍这些步骤。

值提供者会为自动完成控件提供建议列表。值提供者必须实现 IJsonValueProvider 接口,它将定义一个 getValues() 方法,返回包含建议列表的 JSONArray 对象。清单 8 展示了该接口。


public interface IJsonValueProvider { JSONArray getValues(String criteria, Integer maxCount); }

 


这些对象是 JSON for Java 的组成部分,这是一种在 Java 中使用 JSON 的开源包装程序。关于此库的更多信息,请参见 参考资料 部分。

下一步是创建 CityValueProvider,也就是此接口的实现,它为 <ajax:autocomplete/> 控件提供城市数据。请注意以下几个关于 getValues() 实现的要点:

从位置数据提供者检索数据,这是一个数据抽象层(DAL)组件,在内存中缓存所有位置。 需要一个分为两阶段的方法来处理数据(TreeMap 中包含 LocationDTO 对象),由于位置数据提供者会返回按邮编排序的 TreeMap。结果需要根据 CityValueProvider 的城市名称排序。

清单 9 展示了实现方法。


清单 9. 城市值提供者

package com.testwebsite.bll; import java.util.Iterator; import java.util.Set; import java.util.TreeMap; import org.json.JSONArray; import com.testwebsite.dal.LocationDataService; import com.testwebsite.dto.LocationDTO; import com.testwebsite.interfaces.IJsonValueProvider; /** @model * @author Brian J. Stewart (Aqua Data Technologies, Inc. http://www.aquadatatech.com) */ public class CityValueProvider implements IJsonValueProvider { /* (non-Javadoc) * @see com.testwebsite.interfaces.IJsonValueProvider#getValues(java.lang.String) */ @Override public JSONArray getValues(String criteria, Integer maxCount) { String cityName = ""; // If city found, make the search case insensitive if (criteria != null && criteria.length() > 0) { cityName = criteria.toLowerCase(); } // Get Location data from Data Provider TreeMap<Integer, LocationDTO> locData = LocationDataService.getLocationData(); // The LocationDataService Data Provider returns a TreeMap containing // LocationDTO objects that are sorted by Zip Code. // First build a temporary TreeMap (sorted list) filtering with // only unique city names matching the specified cityName parameter TreeMap<String, String> cityData = this.getCityData(locData, cityName); // Finally iterate through sorted City list // and create JSONArray containing // the number elements specified by the maxCount parameter JSONArray json = this.getJsonData(cityData, maxCount); return json; } /** * The getCityData method returns a TreeMap containing Cities matching the * specified cityName criteria. The results are sorted by City Name and filter * out any duplicate city names. * @param locData Location Data from which to retrieve cities * @param cityName City Name prefix to which to search * @return */ protected TreeMap<String, String> getCityData( TreeMap<Integer, LocationDTO> locData, String cityName) { TreeMap<String, String> cityData = new TreeMap<String, String>(); // Iterate through all data looking for matching cities // and add to temporary TreeMap Set<Integer> keySet = locData.keySet(); Iterator<Integer> locIter = keySet.iterator(); while (locIter.hasNext()) { // Get current state Integer curKey = locIter.next(); LocationDTO curLocation = locData.get(curKey); // Get current location data if (curLocation != null) { String curCityName = curLocation.getCity().toLowerCase(); // Add current item if it starts with the cityName parameter if (curCityName.startsWith(cityName)) { cityData.put(curLocation.getCity(), curLocation.getCity()); } } } return cityData; } /** * The getJsonData method returns a JSONArray contain a list of strings * with the city name specified with a maximum number of elements as specified * by the maxCount parameter. * @param cityData TreeMap containing unique list of matching cities * @param maxCount Maximum number of items to include in the JSONArray * @return JSONArray contain sorted list of city names */ protected JSONArray getJsonData(TreeMap<String, String> cityData, int maxCount) { int count = 1; JSONArray json = new JSONArray(); // Get city name keys Set<String> citySet = cityData.keySet(); // Iterate through query results Iterator<String> cityIter = citySet.iterator(); while (cityIter.hasNext()) { // Get current item String curCity = cityIter.next(); // Add item to JSONArray json.put(curCity); // Increment counter count ++; // If maximum number of entries has been met, then exit loop if (count >= maxCount) break; } return json; } }

 

下一步是构建 AutoCompleteServlet Servlet,供浏览器调用 IJsonValueProvider 实现的接口。这个 Servlet 较为简单,只有一点例外。为了满足 “轻松集成/部署” 的目标,应该仅需考虑实现一个值提供者,而不是 Servlet 接口。为了支持此目标,我们使用反射,在运行时使用 <ajax:autocomplete/> 控件的 classname 属性实例化值提供者。请参见清单 10。


清单 10. 自动完成 Servlet

package com.testwebsite.servlets; import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.json.JSONArray; /** * @model * @author Brian J. Stewart (Aqua Data Technologies, Inc. http://www.aquadatatech.com) */ public class AutoCompleteServlet extends HttpServlet { /** * */ private static final long serialVersionUID = -867804519793713551L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String data = ""; // Get parameters from query string String format = req.getParameter("format"); String criteria = req.getParameter("criteria"); String maxCountStr= req.getParameter("maxCount"); String className = req.getParameter("providerClass"); // If format is not null and it's 'json' if (format != null && format.equalsIgnoreCase("json")) { if (className != null && className.length() > 0) { data = this.getJsonResultAsString(criteria, maxCountStr, className); } resp.setContentType("text/plain"); } // Write response // Get writer for servlet response PrintWriter writer = resp.getWriter(); writer.println(data); writer.flush(); } public String getJsonResultAsString(String criteria, String maxCountStr, String className) { String data = ""; Integer maxCount = 10; if (maxCountStr != null && maxCountStr.length() > 0) { maxCount = new Integer(maxCountStr); } // Get dataprovider class using reflection // Construct class Class providerClass; try { // Get provider class providerClass = Class.forName(className); // Construct method and method param types Class[] paramTypes = new Class[2]; paramTypes[0] = String.class; paramTypes[1] = Integer.class; Method getValuesMethod = providerClass.getMethod("getValues", paramTypes); // Construct method param values Object[] argList = new Object[2]; argList[0] = criteria; argList[1] = maxCount; // Get instance of the provider class Object providerInstance = providerClass.newInstance(); // Invoke method using reflection JSONArray resultsArray = (JSONArray) getValuesMethod.invoke(providerInstance, argList); // Convert JSONArray result to string data = resultsArray.toString(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } return data; } }

 

图 8 展示了来自 AutoCompleteServlet Servlet 的服务器响应。


图 8. 自动完成 Servlet 响应

自动完成控件会呈现一个标准 INPUT 标记并设置事件处理程序,然后呈现建议列表容器 DIV 元素和恰当的 CSS,以便进行格式化。您需要添加以下用于支持的 JavaScript 函数:

处理键盘事件 —— <ajax:page/> 处理服务器响应和后异步调用处理 —— <ajax:autocomplete/>,以及 <ajax:page/> 中呈现的 helper 函数 突出显示建议列表中的特定项 —— <ajax:page/> 隐藏建议列表 —— <ajax:page/> 处理建议列表中项的选择(在用户按下 Enter 键时)—— <ajax:page/> 在控件失去焦点时进行处理 —— <ajax:page/>

让我们首先从 onSuggestionKeyDown 函数开始介绍,此函数处理 Esc 键、Enter 键和其他控制键。如果用户按下 Esc 键,建议列表将隐藏,JavaScript 事件链中的后续事件将取消(例如,Key Up 事件不再被处理,因为该事件已经通过隐藏建议列表而得到了处理);请参见清单 11。


清单 11. 处理 Esc 键的代码片段

var keyCode = (window.event) ? window.event.keyCode : ev.keyCode; switch(keyCode) { ... // Handle ESCAPE key case 27: hideSelectionList(curControl, suggestionList); ev.cancelBubble = true; // IE if (window.event) { ev.returnValue = false; } // Firefox else { ev.preventDefault(); } break; ...

 

如果用户按下 Enter 键,当前项应复制到输入控件中,输入列表将隐藏。为了隐藏/显示建议列表,可使用标准 CSS 来进行格式化,并使用 JavaScript 来更改类名。display 属性设置为 none 以隐藏控件,设置为 block 时则会显示列表。清单 12 展示了 JavaScript 函数,我们会将此函数添加到 <ajax:page/> 控件,因为它可与任何 <ajax:autocomplete/> 控件一起使用。


... // Handle ENTER key case 13: handleSelectSuggestItem(curControl, suggestionList); ev.cancelBubble = true; // IE if (window.event) { ev.returnValue = false; } // Firefox else { ev.preventDefault(); } break; ...

 

key-down 事件处理程序会调用 handleSelectSuggestItem,这是在 <ajax:page/> 中定义的(参见清单 13)。


清单 13. 处理 Enter 键

function handleSelectSuggestItem(curControl, suggestionList) { // Get selected node // Cursor is a global variable that is incremented/decremented // when the UP ARROW or DOWN ARROW key is pressed. var selectedNode = suggestionList.childNodes[cursor]; // Get selected value var selectedValue = selectedNode.childNodes[0].nodeValue; // Set the value of the INPUT control curControl.value = selectedValue; // Finally hide the selection list hideSelectionList(curControl, suggestionList); } function hideSelectionList(curControl, suggestionList) { // If suggestion not found if (suggestionList == null || suggestionList == undefined) { return; } // Clear the suggestion list elements suggestionList.innerHTML=''; // Toggle display to none suggestionList.style.display='none'; curControl.focus(); }

 

对于控制键(Shift、Alt 和 Ctrl)的按键事件并不需要过多的处理。您需要通过以下方法忽略这些键盘事件:

将 EVENT 对象的 returnValue 设置为 false(针对 Internet Explorer)并在 EVENT 对象上执行 preventDefault()(针对 Firefox),从而避免在返回过程中更改输入控件 将 EVENT 对象的 cancelBubble 属性设置为 true,取消键盘事件的事件链

onSuggestionKeyDown 的完整代码如清单 14 所示。


function onSuggestionKeyDown(curControl, ev) { // Get suggestion list container var suggestionList= document.getElementById(curControl.id + '_suggest'); // Get key code of key pressed var keyCode = (window.event) ? window.event.keyCode : ev.keyCode; switch(keyCode) { // Ignore certain keys case 16, 17, 18, 20: ev.cancelBubble = true; // IE if (window.event) { ev.returnValue = false; } // Firefox else { ev.preventDefault(); } break; // Handle ESCAPE key case 27: hideSelectionList(curControl, suggestionList); ev.cancelBubble = true; // IE if (window.event) { ev.returnValue = false; } // Firefox else { ev.preventDefault(); } break; // Handle ENTER key case 13: handleSelectSuggestItem(curControl, suggestionList); ev.cancelBubble = true; // IE if (window.event) { ev.returnValue = false; } // Firefox else { ev.preventDefault(); } break; } }

 

key-up 事件处理器更加有趣。如果用户按下 Up Arrow 或 Down Arrow 键,则将改变突出显示的选项。如果用户输入了最小字符数(默认值:3),则应向服务器发出异步调用,填充建议列表。

如果用户按下了 Up Arrow 或 Down Arrow 键,全局 cursor 变量将相应地递增或递减。cursor 变量会跟踪当前选定的项。随后将调用 highlightSelectedNode 函数来突出显示值。请参见清单 15。


... switch(keyCode) { // Ignore ESCAPE case 27: // Handle UP ARROW case 38: if (suggestionList.childNodes.length > 0 && cursor > 0){ var selectedNode = suggestionList.childNodes[--cursor]; highlightSelectedNode(suggestionList, selectedNode); } break; // Handle DOWN ARROW case 40: if (suggestionList.childNodes.length > 0 && cursor < suggestionList.childNodes.length-1) { var selectedNode = suggestionList.childNodes[++cursor]; highlightSelectedNode(suggestionList, selectedNode); } break; ...

 

清单 16 展示了突出显示项的 highlightSelectedNode 函数,其中为选定和取消选定的项定义了 CSS 规则。使用 JavaScript 切换 className。随后即可取消之前选定元素的突出显示。


function highlightSelectedNode(suggestionList, selectedNode) { if (suggestionList == null || selectedNode == null) { return; } // Iterate through all items searching for a node that // matches the node selected for (var i=0; i < suggestionList.childNodes.length; i++) { var curNode = suggestionList.childNodes[i]; if (curNode == selectedNode){ curNode.className = 'autoCompleteItemSelected' } else { curNode.className = 'autoCompleteItem'; } } }

 

如果用户按下其他任何键,且输入了最小字符数或更多字符,则将对服务器发出异步调用,检索建议的 JSON 数组。在就绪状态发生变化后,将调用 req.onreadystatechange 属性中指定的函数(请参见清单 17)。


// If control not found (shouldn't happen) // or minimum number of characters not entered if (curControl == null || curControl.value.length < minChars) { // Hide selected item hideSelectionList(curControl, suggestionList); return; } // Initialize XMLHttpRequest object initializeXmlHttpRequest(); // If req object initialized if (req!=null) { // Set callback function req.onreadystatechange=cityName_onServerResponse; // Set status text in browser window window.status='Retrieving State data from server...'; // Open asynchronous server call req.open('GET',dataUrl,true); // Send request req.send(null); }

 

调用服务器响应函数时,将检查 readyState,确保它为 Loaded。status 也会被检查。如果一切正常,则使用 eval JavaScript 函数将 JSON 数组的字符串表示将转换为数组。随后将该数组传递给 populateSuggestionList 函数,它将为建议列表添加元素。清单 18 展示了服务器响应函数。


function cityName_onServerResponse() { // If loaded if(req.readyState!=4) { return; } // If an error occurred if(req.status != 200) { alert('An error occurred retrieving data.'); return; } // Get response and convert it to an array var responseData = req.responseText; var dataValues=eval('(' + responseData + ')'); // Get current control var curControl = document.getElementById('cityName'); /// Populate suggestion list for control populateSuggestionList(curControl, dataValues); }

 

populateSuggestionList 函数呈现在 <ajax:page/> 控件中,它负责使用异步服务器调用返回的值填充建议列表。随后遍历该数组,为数组中的各项创建一个 DIV 元素。将 DIV 元素添加到建议列表中。清单 19 展示了 populateSuggestionList。


populateSuggestionList(curControl, dataValues) { // Get Suggest List Container for control var container = document.getElementById(curControl.id + '_suggest'); // If container not found (shouldn't happen), then simply return if (container == null) { return; } // Clear suggestion list container container.innerHTML = ''; // If no values return, hide suggestion list if (dataValues.length < 1) { container.style.display='none'; return; } // Show suggestion list container.style.display='block'; container.style.top = (curControl.offsetTop+curControl.offsetHeight) + 'px'; container.style.left = curControl.offsetLeft + 'px'; // Iterate through all values // 1. Create DIV element // 2. Set attributes and text node value // 3. Append new element to the container for(var i=0;i < dataValues.length;i++) { // Get current value var curValue= dataValues[i]; // If value is not blank if (curValue != null && curValue.length > 0 ) { // Create DIV element var newItem = document.createElement('div'); // Append current value as a text node newItem.appendChild(document.createTextNode(curValue)); // Set attributes newItem.setAttribute('class', 'autoCompleteItem'); // Finally append new element to container container.appendChild(newItem); } } // Set first item as the selected node cursor = 0; // Get first node var selectedNode = container.childNodes[cursor]; // If first node is equal to the first node, hide the selection list if (selectedNode.childNodes[0].nodeValue == curControl.value) { hideSelectionList(curControl, container); } else { // Highlight the first node highlightSelectedNode(container, selectedNode); } }

 

清单 20 包含自动完成控件的 TagLib 库定义条目(具有针对各属性描述的内嵌注释)。


<tag> <name>autocomplete</name> <tagclass>com.testwebsite.controls.AutoCompleteTag</tagclass> <bodycontent>JSP</bodycontent> <info> Auto-complete/suggest form input fields based on a specified value. </info> <!-- Unique identifier for control --> <attribute> <name>id</name> <required>true</required> <rtexprvalue>false</rtexprvalue> </attribute> <!-- Minimum string length before submitting asynchronous request --> <attribute> <name>minimumlength</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Maximum number of items to include in suggestion list --> <attribute> <name>maxcount</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Width of control --> <attribute> <name>width</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Value of control --> <attribute> <name>value</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Data Url for asynchronous call. A default Servlet has been created, but for greater flexibility, a Web Service or another Servlet can be specified--> <attribute> <name>dataurl</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> <!-- Class that provides suggest value list for control (Used if dataUrl not specified --> <attribute> <name>providerclass</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> </tag>




通常,业务线应用程序包括选择列表,其值独立于其他表单字段(例如,依赖于产品分类的产品名称)。

在深入探究 Ajax 和异步 Web 编程技术之前,您必须将所有值呈现到 Web 页面中(通常以 JavaScript 数组的形式呈现),同时在 JavaScript 内动态填充值。JavaScript 数组可能是多维的,也可能包含标记,例如,包含 | 字符来分隔级联值。此外,整个页面可被刷新,从级联选择列表中检索值。在处理庞大的数据集或尝试构建用户友好的 Web 应用程序时,这两种方法都不是最理想的。利用 Ajax 和异步技术,您将可以提供往往只能在桌面应用程序中看到的丰富而直观的用户体验。

以下几节描述了创建级联式下拉控件的步骤:

与为自动完成控件创建的值提供者类似,我们还要创建一个 Servlet,返回包含值的 JSON 数组。级联式控件的值提供者要更加复杂一些,因为需求和数据往往需要独立的 Servlet 或 Web 服务来应用业务规则。此外,您还可以使用内嵌的 JSP TabLib 控件(包含在任意标记主体内的控件),但这会使事情进一步复杂化。使用独立的 Servlet 可在向客户端返回数据时提供更大的灵活性。值可依赖于其他表单字段或 Servlet 内定义的其他复杂业务规则。

清单 21 展示了级联式下拉控件的两个值提供者。第一个是 City 值提供者,它依赖于 State 值。第二个值提供者用于 Country,它依赖于 State 和 City 值。两个 Servlet 均返回 JSON 数组并使用位置数据提供者(一个 DAL 组件)。代码类似于为自动完成控件开发值提供者;关键差异在于在这里使用独立的 Servlet 来保持实现的简单性和灵活性。


package com.testwebsite.servlets; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.testwebsite.bll.LocationService; public class CityServlet extends HttpServlet { private static final long serialVersionUID = 3231866266466404450L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String data = null; // Get parameters from query string String format = req.getParameter("format"); String cityName = req.getParameter("cityName"); String stateName = req.getParameter("stateName"); // If format is not null and it's 'json' if (format != null & format.equalsIgnoreCase("json")) { // Get city data based on state name and city name prefix data = LocationService.getCitiesAsJson(cityName, stateName); resp.setContentType("text/plain"); } // Write response // Get writer for servlet response PrintWriter writer = resp.getWriter(); writer.println(data); writer.flush(); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost(req, resp); } } /** * @model * @author Brian J. Stewart (Aqua Data Technologies, Inc. http://www.aquadatatech.com) * */ public class CountyServlet extends HttpServlet { /** * */ private static final long serialVersionUID = 3231866266466404450L; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String data = null; // Get parameters from query string String format = req.getParameter("format"); String cityName = req.getParameter("cityName"); String stateName = req.getParameter("stateName"); String countyName = req.getParameter("countyName"); // If format is not null and it's 'json' if (format != null && format.equalsIgnoreCase("json")) { data = LocationService.getCountiesAsJson(countyName, stateName, cityName); resp.setContentType("text/plain"); } // Write response // Get writer for servlet response PrintWriter writer = resp.getWriter(); writer.println(data); writer.flush(); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doPost(req, resp); } }

 

级联式下拉控件的工作方式如下:

用户选择 SELECT 控件。在获得焦点时,则发出异步调用,从服务器检索值。 服务器将 JSON 值数组发回给客户端。 用户从列表中选择了一个值之后,控件将失去焦点(在 blur 事件发生时),依赖于当前字段的控件将被清除。这种做法的目的在于保持数据完整性(如果 State 值发生了变化,City 值很可能不再有效)。

<ajax:page/> 控件呈现一个页面中所有级联式下拉控件均可用的通用函数,<ajax:dropdown/> 控件呈现特定于独立控件实例的 JavaScript。

SELECT 控件的呈现非常简单。清单 22 显示了为 onfocus 和 onblur 事件呈现事件处理程序的方法。


... /** * The getSelectControlHtml method returns the html code to render the drop down (html * select) control. * @return Html code for drop down (html select) control */ protected String getSelectControlHtml() { StringBuffer html = new StringBuffer(); // Render dropdown/select control html.append("<select ); html.append(this.getId()); // Render on focus event handler html.append("' ); html.append(this.getId()); html.append("_onSelect(this)'"); // Render on change event handler html.append(" ); html.append(this.getId()); html.append("_onChange(this)'"); // Render css class if specified if (this.getCssclass() != null && this.getCssclass().length() > 0) { html.append(" ); html.append(this.getCssclass()); html.append("'"); } // Render width if applicable (not 0/default/auto-fit) if (this.getWidth() > 0) { html.append(" style='width:"); html.append(this.getWidth()); html.append("px'"); } html.append("/>"); return html.toString(); } ...

 

onSelect 事件处理程序检索控件值,当前控件利用这些值执行级联,此外还会生成 URL,以便将异步请求发送给服务器。在接收到响应时,将使用 JavaScipt 将 JSON 数组中返回的值填充到 SELECT 标记之中(请参见清单 23)。


function stateName_onSelect(curControl) { if(curControl.options.length > 0) { return; } clearOptions(curControl); // Set waiting message in control var waitingOption = new Option('Retrieving values...','',true,true); curControl.options[curControl.options.length]=waitingOption; // The dataUrl is built dynamically based on the cascadeTo control var dataUrl = '/TestWebSite/State?format=json&stateName=' + getSelectedValue('stateName'); // Initialize the XMLHttpRequest object initializeXmlHttpRequest(); // If initialization was successful if (req!=null) { // Set callback function req.onreadystatechange=stateName_onServerResponse; // Set status text in browser window window.status='Retrieving State data from server...'; // Open asynchronous server call req.open('GET',dataUrl,true); // Send request req.send(null); } }

 

在级联式下拉控件标记动态生成的服务器响应处理程序 CONTROL-NAME_onServerResponse 中,将发生以下活动:

如果在异步调用过程中出现错误,则通知用户 获得响应数据,并将其转换为包含字符串的数组

清单 24 是由 <ajax:dropdown/> 控件动态呈现的。


function cityName_onServerResponse() { // If not finished, then return if(req.readyState!=4) { return; } // If an error occurred notify user and return if(req.status != 200) { alert('An error occurred retrieving data.'); return; } // Get current control var curControl = document.getElementById('cityName'); // Clear options clearOptions(curControl); // Get response data var responseData = req.responseText; // Convert to array var dataValues=eval('(' + responseData + ')'); // Populate SELECT tag with OPTION elements populateSelectControl(curControl, dataValues);window.status=''; }

 

populateSelectControl 函数是由 <ajax:page/> 标记生成的,它会为 SELECT 控件添加一个空白的 OPTION,还会为 dataValues 数组中的各值添加一个 OPTION 元素。动态生成的代码片段如清单 25 所示。


清单 25. 填充 SELECT 控件

function populateSelectControl(curControl, dataValues) { // Append blank option var blankOption= new Option('','',false,true); curControl.options[curControl.options.length]=blankOption; // Iterate through data value array for (var i=0;i<dataValues.length;i++) { // Create option var newOption= new Option(dataValues[i],dataValues[i],false,false); // Add option to control options curControl.options[curControl.options.length]=newOption; } }

 

在 onChange 事件处理程序中,依赖于当前控件的所有控件均会被清除(请参见清单 26)。


function stateName_onChange(curControl) { // Array dynamically generated by the control var toList=['cityName','countyName']; // If no controls are dependent on this function, simply return if (toList == null || toList.length == 0) { return; } // Iterate through list of controls that are dependent on // the current control for (var i=0; i < toList.length; i++) { // Get current control name var curControlName = toList[i]; // Get current control var curToControl = document.getElementById(curControlName); // If control not found, then exit if (curToControl == null) return; // Clear the current control clearOptions(curToControl); } }

 

clearOptions 函数将移除父 SELECT 控件中的所有项,它是在 <ajax:page/> 控件中呈现的(请参见清单 27)。


function clearOptions(curControl) { // If current control is null then exit if (curControl == null) { alert('Unable to clear control'); return; } // Check if control is already blank and return if it is if (curControl.options.length < 1) { return; } // Clear the options curControl.options.length = 0; }

 

清单 28 显示了级联式下拉控件的 TagLib 库定义条目(具有针对各属性描述的内嵌注释)。


<tag> <name>dropdown</name> <tagclass>com.testwebsite.controls.DropDownTag</tagclass> <bodycontent>empty</bodycontent> <info> Populates Drop Down control asynchronously cascading values. </info> <!-- Unique identifier for control --> <attribute> <name>id</name> <required>true</required> <rtexprvalue>false</rtexprvalue> </attribute> <!-- Url for Value Provider --> <attribute> <name>dataurl</name> <required>true</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Message displayed while retrieving values from Value Provider --> <attribute> <name>updatemessage</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> <!-- CSS class name --> <attribute> <name>cssclass</name> <required>false</required> <rtexprvalue>false</rtexprvalue> </attribute> <!-- Current control value--> <attribute> <name>value</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Comma separated list of control id from which the current control cascades --> <attribute> <name>cascadefrom</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Comma separated list of control id to which the current control cascades --> <attribute> <name>cascadeto</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> <!-- Width of control --> <attribute> <name>width</name> <required>false</required> <rtexprvalue>true</rtexprvalue> </attribute> </tag>




构建测试 Web 页面

下一步就是构建示例页面,测试支持 Ajax 的控件。您将使用 Create New Contact 页面测试 <ajax:autocomplete/> 控件,使用 Create New Employee 页面测试 <ajax:dropdown/> 控件。

新建联系人

图 9 展示了测试用的 Create New Contact 页面,它从用户的角度展示了自动完成控件的外观。



构建支持 Ajax 的自动完成和级联式下拉控件 - ahang8415 - ahang8415的博客

此测试页面的 JSP 代码如清单 29 所示。


<%@ page contentType="text/html; charset=ISO-8859-5" %> <%@ taglib prefix="ajax" uri="/WEB-INF/tlds/ajax_controls.tld"%> <html> <head> <title>New Contact Information</title> <link href="core.css" rel="stylesheet" type="text/css" /> <ajax:page/> </head> <body> <div > <form> <div > <div > Contact Information </div> <div > <div style="font-weight:bold">First Name:</div> <div> <input type="text" size="40"/> </div> <div style="font-weight:bold">Last Name:</div> <div> <input type="text" size="40"/> </div> <div style="font-weight:bold">Address:</div> <div> <input type="text" size="40"/> </div> <div style="font-weight:bold">City:</div> <div> <ajax:autocomplete width="40" providerclass="com.testwebsite.bll.CityValueProvider"/> </div> <div style="font-weight:bold">County:</div> <div> <input type="text" size="40"/> </div> <div style="font-weight:bold">Zip Code:</div> <div> <input type="text" size="40"/> </div> </div> <div > <input type="reset" />  <input type="submit" value="Save"/> </div> </div> </form> </div> </body> </html>

 

City Name 字段现支持 Ajax。用户在 City Name 字段中键入文本时,将动态显示建议,类似于 Google 的自动建议功能。

新建员工

图 10 展示了 Create New Employee 页面,它从用户的角度演示了级联式下拉控件。



构建支持 Ajax 的自动完成和级联式下拉控件 - ahang8415 - ahang8415的博客

此页面的 JSP 代码如清单 30 所示。


<%@ page contentType="text/html; charset=ISO-8859-5" %> <%@ taglib prefix="ajax" uri="/WEB-INF/tlds/ajax_controls.tld"%> <html> <head> <title>New Employee</title> <ajax:page/> <link href="core.css" rel="stylesheet" type="text/css" /> </head> <body> <div > <form> <table cellspacing="0" cellpadding="0"> <thead> <tr> <td colspan="2"> Employee Information </td> </tr> </thead> <tbody> <tr> <td > Last Name: </td> <td > <input type="text" size="40"/> </td> </tr> <tr> <td > First Name: </td> <td > <input type="text" size="40"/> </td> </tr> <tr> <td > Address: </td> <td > <input type="text" size="40"/> </td> </tr> <tr> <td > State: </td> <td > <ajax:dropdown dataurl="/State" width="240" updatemessage="Retrieving State data from server..." cascadeto="cityName,countyName" /> </td> </tr> <tr> <td > City: </td> <td > <ajax:dropdown dataurl="/City" updatemessage="Retrieving City data from server..." cascadeto="countyName" width="240" cascadefrom="stateName" /> </td> </tr> <tr> <td > County: </td> <td > <ajax:dropdown dataurl="/County" updatemessage="Retrieving County data from server..." cascadefrom="stateName,cityName" width="240"/> </td> </tr> <tr> <td > Zip Code: </td> <td > <input type="text" size="40" /> </td> </tr> </tbody> <tfoot align="right" > <tr> <td colspan="2"> <input type="reset" />  <input type="submit" value="Save"/> </td> </tr> </tfoot> </table> </form> </div> </body> </html>




结束语

在这篇文章中,您学习了一些异步通信技术,了解了如何通过可重用的 JSP TagLib 控件为业务线应用程序添加 JSON 和 Ajax。基于 Ajax 的控件有更出色的用户体验和更具响应性、更直观的用户界面,因而可为业务线应用程序带来显著的收益。代码并非十分复杂,只需整合关键代码块(JavaScript、CSS 和 J2EE 技术)即可构建支持 Ajax 的 JSP 控件。

可进一步扩展控件来实现以下功能:

原创粉丝点击