本文介绍如何构建可在业务线应用程序中使用的 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;
}
}
|
创建一个
Servlet 来处理对值提供者的异步请求
下一步是构建 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 响应
创建可在
JSP 页面中使用的 JSP TagLib 控件
自动完成控件会呈现一个标准 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);
}
} |
自动完成
TagLib 库定义条目
清单 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)
与为自动完成控件创建的值提供者类似,我们还要创建一个 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);
}
} |
创建
JSP TagLib 控件
级联式下拉控件的工作方式如下:
- 使用空白的 SELECT 控件呈现页面。
- 用户选择 SELECT 控件。在获得焦点时,则发出异步调用,从服务器检索值。
- 服务器将 JSON 值数组发回给客户端。
- 客户端使用 JSON 数组中的值动态填充 SELECT 控件。
- 用户从列表中选择了一个值之后,控件将失去焦点(在 blur 事件发生时),依赖于当前字段的控件将被清除。这种做法的目的在于保持数据完整性(如果
State 值发生了变化,City 值很可能不再有效)。
<ajax:page/> 控件呈现一个页面中所有级联式下拉控件均可用的通用函数,<ajax:dropdown/>
控件呈现特定于独立控件实例的 JavaScript。
为级联式下拉控件呈现
SELECT 控件
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='width:");
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;
}
|
级联式下拉
TagLib 库定义条目
清单 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 |
|
包含本文介绍的
MySQL 数据库脚本2 |
ArticleDatabaseScripts.zip |
50KB |
|
注意:
- 本 ZIP 文件包含本文全部源代码。
- 本文件包含本文介绍的全部 MySQL 数据库脚本。
学习
获得产品和技术 |