本文介绍如何构建可在业务线应用程序中使用的 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 技术来构建控件。
|
本文中开发的控件的主要设计目标如下:
- 提供与现有 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 的控件异步公开数据时,有三个选项:
- JavaServer Pages (JSP)
- Servlet
- SOAP 或 RESTful Web 服务
本文使用的是 Servlet,原因在于其效率和最低的开销。JSP 页面实现起来比 Servlet 更加简单,但从实现的角度看来,它并不简洁。
支持 Ajax 的控件的数据提供者可使用 XML 或 JSON 作为数据交换格式。XML 的人类可读性通常优于 JSON,但有以下一些不足之处:
- 与 JSON 相比,数据更大
- 相对而言,在 JavaScript 中解析的难度较高
出于这些原因,本文使用了 JSON。
示例应用程序的数据模型包含两个实体:
- 州,其中包含州的缩写和名称
- 位置,其中包含城市、邮编和其他位置数据
图 1 显示了本文中的示例页面所用的数据模型。
图 1. 数据模型
本文中的示例包含数据抽象层(DAL)、数据传输对象(DTO)、业务逻辑层(BLL)、表示层和用于支持的 helper 类。下图展示了这些类的 UML 类图。
helper 类提供数据库和表示层支持类(请参见图 2)。
图 2. UML 类图 —— helper 类
数据抽象层包含一个类,用于向业务层提供关于位置的信息(请参见图 3)。
图 3. UML 类图 —— 数据抽象层类
您使用两个 DTO,在三??中传递数据(请参见图 4)。StateDTO
包含与州有关的数据、LocationDTO
包含与位置有关的数据,包括邮编、城市名称、州、纬度和经度。
图 4. UML 类图 —— 数据传输对象类
业务逻辑层包含值提供者,为支持 Ajax 的控件提供数据(请参见图 5)。自动完成控件的值提供者必须实现 IJsonValueProvider
接口。位置服务从数据层接收 DTO 对象的集合,然后生成对应的 JSON 数据,在表示层使用。
图 5. UML 类图 —— 业务逻辑层类
Servlet 提供了一个接口,客户端异步调用将针对此接口执行(请参见图 6)。这些 Servlet 与值提供者交互,为 Web 浏览器提供 JSON 数据。
图 6. UML 类图 —— 业务逻辑层、Servlet 类
|
您将创建以下支持 Ajax 的控件:
- 级联式下拉控件 —— 根据其他表单字段或业务规则,动态填充
SELECT
控件中的值选项。 - 自动完成控件 —— 在用户输入时实时显示类似于 Google Suggest 的建议列表。将利用与数据提供者 servlet 的异步通信动态显示建议。
除了两个 JSP TagLib 控件之外,您还需要另一个控件来封装所有可重用的 JavaScript 函数,如清除/填充值、处理键盘/鼠标事件、支持异步通信。图 7 展示了这三个控件类。
图 7. UML 类图 —— JSP TagLib 控件类
|
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 id='heading1'>This is a Heading</h1>"); } catch (IOException e) { e.printStackTrace(); } |
创建了 JSP TagLib 控件的实现之后,必须在 /WEB-INF/tlds 目录中定义 TagLib 库定义(TLD),如清单 2 所示。
清单 2. 示例 JSP TagLib 库定义文件
<?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
来实现跨浏览器支持。
清单 4. 创建 XMLHttpRequest 对象
var req; function initializeXmlHttpRequest() { if (window.ActiveXObject) { req=new ActiveXObject('Microsoft.XMLHTTP'); } else { req=new XMLHttpRequest(); } } |
如前所述,可将清单 5 中的代码添加到 tag-implementation 类中,从而为页面呈现 JavaScript 代码。
清单 5. 为 XMLHttpRequest 对象呈现 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 展示了如何实现异步调用。
清单 6. 使用 XMLHttpRequest 对象
// 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
包含以下状态码之一:
- 0=initialized
- 1=Open
- 2=Sent
- 3=Receiving
- 4=Loaded
|
通常,Loaded
以外的内容都会被忽略,因为在服务器响应完成之前通常不需要采取任何操作。异步调用的 Loaded
值并不能保证成功。与其他任何 Web 页面请求类似,有可能无法找到页面或出现其他问题。如果 req.status
是 200
以外的值,则将出错。清单 7 展示了如何处理服务器响应。
清单 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/>
。
|
要构建自动完成控件,需完成以下步骤:
- 构建一个值提供者,为控件提供建议。
- 创建一个 Servlet 接口,为异步调用公开值提供者。
- 创建一个 JSP TagLib 控件,将一切封装在一个控件中,可在 JSP 页面中使用此控件。
下面几节将详细介绍这些步骤。
值提供者会为自动完成控件提供建议列表。值提供者必须实现 IJsonValueProvider
接口,它将定义一个 getValues()
方法,返回包含建议列表的 JSONArray
对象。清单 8 展示了该接口。
清单 8. IJsonValueProvider 接口
public interface IJsonValueProvider { JSONArray getValues(String criteria, Integer maxCount); } |
|
下一步是创建 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/>
控件一起使用。
清单 12. 处理 Enter 键的代码片段
... // 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 所示。
清单 14. 完整的 key-down 事件处理程序
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。
清单 15. key-up 事件处理程序中处理 Up Arrow 和 Down Arrow 键的代码片段
... 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
。随后即可取消之前选定元素的突出显示。
清单 16. 突出显示建议列表中的项
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)。
清单 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 展示了服务器响应函数。
清单 18. 服务器响应处理程序(由
<ajax:autocomplete/>
控件动态生成)
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
。
清单 19. 填充建议列表(在 <ajax:page/> 控件中呈现)
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 库定义条目(具有针对各属性描述的内嵌注释)。
清单 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 和异步技术,您将可以提供往往只能在桌面应用程序中看到的丰富而直观的用户体验。
以下几节描述了创建级联式下拉控件的步骤:
- 创建可通过 JavaScript 调用的值提供者/Servlet 接口。
- 创建 JSP TagLib 控件,将一切内容都封装在可置入任何 JSP 页面的控件中。
与为自动完成控件创建的值提供者类似,我们还要创建一个 Servlet,返回包含值的 JSON 数组。级联式控件的值提供者要更加复杂一些,因为需求和数据往往需要独立的 Servlet 或 Web 服务来应用业务规则。此外,您还可以使用内嵌的 JSP TabLib 控件(包含在任意标记主体内的控件),但这会使事情进一步复杂化。使用独立的 Servlet 可在向客户端返回数据时提供更大的灵活性。值可依赖于其他表单字段或 Servlet 内定义的其他复杂业务规则。
清单 21 展示了级联式下拉控件的两个值提供者。第一个是 City 值提供者,它依赖于 State 值。第二个值提供者用于 Country,它依赖于 State 和 City 值。两个 Servlet 均返回 JSON 数组并使用位置数据提供者(一个 DAL 组件)。代码类似于为自动完成控件开发值提供者;关键差异在于在这里使用独立的 Servlet 来保持实现的简单性和灵活性。
清单 21. CityServlet 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
控件呈现页面。 - 用户选择
SELECT
控件。在获得焦点时,则发出异步调用,从服务器检索值。 - 服务器将 JSON 值数组发回给客户端。
- 客户端使用 JSON 数组中的值动态填充
SELECT
控件。 - 用户从列表中选择了一个值之后,控件将失去焦点(在
blur
事件发生时),依赖于当前字段的控件将被清除。这种做法的目的在于保持数据完整性(如果 State 值发生了变化,City 值很可能不再有效)。
<ajax:page/>
控件呈现一个页面中所有级联式下拉控件均可用的通用函数,<ajax:dropdown/>
控件呈现特定于独立控件实例的 JavaScript。
SELECT
控件的呈现非常简单。清单 22 显示了为 onfocus
和 onblur
事件呈现事件处理程序的方法。
清单 22. 为级联式下拉控件呈现 SELECT 控件
... /** * 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 id='"); html.append(this.getId()); // Render on focus event handler html.append("' onfocus='"); html.append(this.getId()); html.append("_onSelect(this)'"); // Render on change event handler html.append(" onChange='"); html.append(this.getId()); html.append("_onChange(this)'"); // Render css class if specified if (this.getCssclass() != null && this.getCssclass().length() > 0) { html.append(" class='"); html.append(this.getCssclass()); html.append("'"); } // Render width if applicable (not 0/default/auto-fit) if (this.getWidth() > 0) { html.append(" style='"); html.append(this.getWidth()); html.append("px'"); } html.append("/>"); return html.toString(); } ... |
onSelect
事件处理程序检索控件值,当前控件利用这些值执行级联,此外还会生成 URL,以便将异步请求发送给服务器。在接收到响应时,将使用 JavaScipt 将 JSON 数组中返回的值填充到 SELECT
标记之中(请参见清单 23)。
清单 23. On-select 事件处理程序
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
中,将发生以下活动:
- 除非
Loaded
,否则忽略状态更改 - 如果在异步调用过程中出现错误,则通知用户
- 获得当前控件,并清除所有
OPTION
元素 - 获得响应数据,并将其转换为包含字符串的数组
- 使用数组填充
SELECT
控件
清单 24 是由 <ajax:dropdown/>
控件动态呈现的。
清单 24. 服务器响应处理程序(由控件动态生成)
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)。
清单 26. On-change 事件处理程序
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)。
清单 27. On-change 事件处理程序
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 库定义条目(具有针对各属性描述的内嵌注释)。
清单 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> |
|
下一步就是构建示例页面,测试支持 Ajax 的控件。您将使用 Create New Contact 页面测试 <ajax:autocomplete/>
控件,使用 Create New Employee 页面测试 <ajax:dropdown/>
控件。
图 9 展示了测试用的 Create New Contact 页面,它从用户的角度展示了自动完成控件的外观。
图 9. Create New Contact 页面展示了如何使用自动完成控件
此测试页面的 JSP 代码如清单 29 所示。
清单 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 id="container"> <form> <div class="dialog"> <div class="dialogTitle"> Contact Information </div> <div class="contentPane"> <div style="font-weight:bold">First Name:</div> <div> <input type="text" id="firstName" size="40"/> </div> <div style="font-weight:bold">Last Name:</div> <div> <input type="text" id="lastName" size="40"/> </div> <div style="font-weight:bold">Address:</div> <div> <input type="text" id="streetAddress" size="40"/> </div> <div style="font-weight:bold">City:</div> <div> <ajax:autocomplete id="cityName" width="40" providerclass="com.testwebsite.bll.CityValueProvider"/> </div> <div style="font-weight:bold">County:</div> <div> <input type="text" id="countyName" size="40"/> </div> <div style="font-weight:bold">Zip Code:</div> <div> <input type="text" id="zipCode" size="40"/> </div> </div> <div class="buttonPane"> <input type="reset" /> <input type="submit" value="Save"/> </div> </div> </form> </div> </body> </html> |
City Name 字段现支持 Ajax。用户在 City Name 字段中键入文本时,将动态显示建议,类似于 Google 的自动建议功能。
图 10 展示了 Create New Employee 页面,它从用户的角度演示了级联式下拉控件。
图 10. 演示级联式下拉控件的测试页面
此页面的 JSP 代码如清单 30 所示。
清单 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 id="container"> <form> <table class="dialog" cellspacing="0" cellpadding="0"> <thead> <tr> <td class="dialogTitle" colspan="2"> Employee Information </td> </tr> </thead> <tbody> <tr> <td class="fieldLabel"> Last Name: </td> <td class="fieldValue"> <input type="text" id="lastName" size="40"/> </td> </tr> <tr> <td class="fieldLabel"> First Name: </td> <td class="fieldValue"> <input type="text" id="firstName" size="40"/> </td> </tr> <tr> <td class="fieldLabel"> Address: </td> <td class="fieldValue"> <input type="text" id="streetAddress" size="40"/> </td> </tr> <tr> <td class="fieldLabel"> State: </td> <td class="fieldValue"> <ajax:dropdown id="stateName" dataurl="/State" width="240" updatemessage="Retrieving State data from server..." cascadeto="cityName,countyName" /> </td> </tr> <tr> <td class="fieldLabel"> City: </td> <td class="fieldValue"> <ajax:dropdown id="cityName" dataurl="/City" updatemessage="Retrieving City data from server..." cascadeto="countyName" width="240" cascadefrom="stateName" /> </td> </tr> <tr> <td class="fieldLabel"> County: </td> <td class="fieldValue"> <ajax:dropdown id="countyName" dataurl="/County" updatemessage="Retrieving County data from server..." cascadefrom="stateName,cityName" width="240"/> </td> </tr> <tr> <td class="fieldLabel"> Zip Code: </td> <td class="fieldValue"> <input type="text" id="zipCode" size="40" /> </td> </tr> </tbody> <tfoot align="right" class="buttonPane"> <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 控件。
可进一步扩展控件来实现以下功能:
- 支持与其他类型的控件(除 SELECT 控件之外的其他控件)之间的级联
- 为自动完成控件添加鼠标事件处理
- 为异步请求添加编码和检查功能
|
描述 | 名字 | 大小 | 下载方法 |
---|---|---|---|
包含本文全部源代码1 | ArticleCodeSample.zip | 50KB | HTTP |
包含本文介绍的 MySQL 数据库脚本2 | ArticleDatabaseScripts.zip | 50KB | HTTP |
关于下载方法的信息 |
注意:
- 本 ZIP 文件包含本文全部源代码。
- 本文件包含本文介绍的全部 MySQL 数据库脚本。
学习
- 您可以参阅本文在 developerWorks 全球网站上的 英文原文。
- 进一步了解 JSON for Java,这是在 Java 中使用 JSON 的一个开源库。
- The W3C Working Draft 15 April 2008 包含关于 XMLHttpRequest Object 标准的宝贵信息。
- 阅读 JSON 简介。
- 查看 Mozilla 针对 JavaScript 的官方文档。
获得产品和技术
- Zip Code Database Project 是一个免费的邮编数据库。
- About.com: ZIP Code Database 是另一个免费的邮编数据库。
Brian J. Stewart 目前是 Aqua Data Technologies 的一位首席顾问。他是这家公司的创始人,该公司关注内容管理、XML 技术和企业客户端/服务器与 Web 系统。他架构并开发了基于 J2EE 和 .NET 平台的企业解决方案。 |