添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
Oracle Account

Manage your account and access personalized content. Sign up for an Oracle Account

Sign in to my Account
Sign in to Cloud

Access your cloud dashboard, manage orders, and more. Sign up for a free trial

Ajax 表编辑器和查看器

学习如何使用 Ajax 连接至服务器以构建动态表组件,以及如何利用 JavaScript 高效地生成 HTML 和 XML。

作者:Andrei Cioroianu

2007 年 12 月发布

典型的 Web 应用程序可以显示从数据库和其他数据源(如数据馈送)检索到的信息。在很多情况下,会有一个或多个 Web 表单供用户输入新信息或修改现有数据。当用户单击某个按钮时,表单的数据就会发送到对数据进行验证和处理的服务器上。然后,服务器将同一表单或另一页面返回给用户。此应用程序模型可以借助 Ajax 得到优化,从而最小化网络流量并极大地改善用户体验。

Ajax 客户端可以对用户的操作作出非常迅速的响应,而且能够提供类似于桌面的特性,从而提高用户生产率。用户界面 (UI) 不会针对服务器端的每个请求都重新生成 HTML 标记;相反地,它在浏览器中只加载一次,然后 Ajax 客户端仅检索它需要的数据,从而对用户界面进行动态更新。此外,很多数据请求都是异步发送的,这意味着它们不会阻塞 UI。

在本文中,我将向您演示如何构建基于 Web 的表编辑器,它允许用户插入/删除行并撤消更改,不必等待服务器响应也不会丢失滚动位置。表组件也会充当查看器,它使用 Ajax 从数据馈送检索信息。您将学习如何使用 JavaScript 构建表模型、创建表编辑器/查看器、利用正则表达式验证数据、访问数据馈送、使用事件处理程序以及监视 Web 浏览器中的数据变化。此外,我将提供一种用于在客户端生成 XML 和 HTML 的非常有趣的方法,它使用类似于 JSP 的语法,使得代码可读性更强而且更易于维护。

应用程序概述

本文的示例应用程序是一个投资组合编辑器/查看器:

用户输入股票信息,并且能够随时撤消数据更改。股价是从数据馈送检索的,因此股票价值和它们的损/益可以每秒进行更新。当用户保存数据时,表组件切换到只读模式:

在以前的一篇标题为“ 在 Ajax 应用程序中实现数据交换 ”的 OTN 文章中,我已经提供了该应用程序的一半代码;Ajax 实用程序函数(包含在 ajaxUtil.js 文件中)在我的另一篇标题为“ 使用 Ajax、JSF 和 ADF Faces 开发智能 Web UI ”的 OTN 文章中得到了重用。下表指出了描述各个源文件和目录的文章,您可以在这些目录下找到相关文件。

可下载的源存档包含一个 WAR 文件(名为 ajaxapp.war),您可以将它导入到 Oracle JDeveloper 中,这样就可以使用嵌入式 OC4J 服务器运行它了。选择一个现有的应用程序或者在 Oracle JDeveloper 中创建一个新的应用程序,并确保已经在 Application Navigator 中选择了它。然后,单击 File 菜单的 Import 选项,在 Import 对话框中选择 WAR File,单击 OK:

跳过向导的欢迎窗口,提供一个项目名:

选择 ajaxapp.war 文件,单击 Finish:

返回 Oracle JDeveloper 的主窗口后,在 Application Navigator 中,右键单击新创建的 ajaxapp 项目,并单击 Project Properties。然后,在左侧面板中选择 JSP Tag Libraries:

单击 Project Properties 窗口中的 Add 按钮,在 Choose Tag Libraries 对话框中选择 JSTL Core 1.1,然后单击 OK:

在 Project Properties 窗口中单击 OK,返回到 Oracle JDeveloper。现在可以准备运行示例应用程序了。右键单击项目的 index.jsp 页面,然后单击 Run:

数据模型和数据馈送

和任何典型的 Web 应用程序类似,投资组合示例有一个服务器端数据模型。我前面的文章 介绍了该模型以及 Ajax 客户端和服务器之间的数据交换方法。服务器端的 DataModel 类可以从 XML 文档进行初始化,它的状态可以在 JSON 字符串中进行编码,该字符串用于初始化客户端的数据模型。

构建表模型

投资组合应用程序使用 JavaScript 表模型,该模型使用 Ajax 维护从数据馈送检索的用户数据和信息。dataModel.js 文件创建一个对象,该对象的方法类似于 Swing 的 TableModel 接口所定义的方法:getRowCount()、getColumnCount()、getColumnName()、getValueAt() 和 setValueAt()。下面是创建数据模型对象的代码:

var dataModel = { columns: [ "Symbol", "Shares", "Paid Price", "Last Price", "Stock Value", "Gain/Loss" ], stocks: [ ], getRowCount: function() { return this.stocks.length; getColumnCount: function() { return this.columns.length; getColumnName: function(index) { return this.columns[index]; getValueAt: function(row, column) { setValueAt: function(value, row, column) {

数据模型对象还有一些其他的方法,它们可以返回股票的价值和收益、计算投资组合的价值和收益、获取/设置所有股票、插入新股票和删除现有的股票。您可以在 dataModel.js 文件中找到这些简单方法的代码。

实施数据馈送

数据馈送通常提供一个 Web 服务或基于 HTTP 的协议,以便可以通过网络对其进行访问。但是,由于 Web 浏览器采取了安全限制,Ajax 客户端不能直接连接到第三方 Web 服务上。因此,您需要在安装有 Ajax 应用程序的服务器上运行 Ajax 控制器。此控制器应当连接到第三方 Web 服务上,以便为 Ajax 客户端检索数据。为了简化问题并聚焦于 Ajax 主题,示例应用程序模拟了数据馈送,在 ShareBean 类中生成随机的股价:

package ajaxapp.feed; public class ShareBean implements java.io.Serializable { private String symbol; private int trend; private double lastPrice; public ShareBean(String symbol) { this.symbol = symbol; trend = 1; lastPrice = Math.random() * 100; public String getSymbol() { return symbol; public int getTrend() { return trend; public double getLastPrice() { lastPrice += trend * Math.random() * 0.1; if (Math.random() < 0.2) trend = -trend; return lastPrice;

DataFeed 类提供了一个 getData() 方法,它接受一个符号数组并返回在 JSON 字符串中编码的股票信息:

package ajaxapp.feed; import ajaxapp.util.JSONEncoder; import java.util.*; public class DataFeed implements java.io.Serializable { private HashMap<String, ShareBean> shareMap; public DataFeed() { shareMap = new HashMap<String, ShareBean>(); public synchronized String getData(String symbols[]) { JSONEncoder json = new JSONEncoder(); json.startArray(); for (int i = 0; i < symbols.length; i++) { String symbol = symbols[i]; ShareBean share = shareMap.get(symbol); if (share == null) { share = new ShareBean(symbol); shareMap.put(symbol, share); json.startObject(); json.property("symbol", share.getSymbol()); json.property("trend", share.getTrend()); json.property("lastPrice", share.getLastPrice()); json.endObject(); json.endArray(); return json.toString(); public static String getData( DataFeed feed, String symbols[]) { return feed.getData(symbols);

使用在 feed.tld 中定义的 EL 函数,静态 getData() 方法可用于从 JSP 页面调用实例方法:

<?xml version="1.0" encoding="UTF-8" ?> <taglib ...> <tlib-version>1.0</tlib-version> <short-name>feed</short-name> <uri>/ajaxapp/feed</uri> <function> <name>getData</name> <function-class>ajaxapp.feed.DataFeed</function-class> <function-signature> java.lang.String getData( ajaxapp.feed.DataFeed, java.lang.String[]) </function-signature> </function> </taglib>

应用程序的 Ajax 控制器(称作 ajaxCtrl.jsp)使用 getData() EL 函数来获得 Ajax 客户端所需的股价。DataFeed 实例用作 ajaxCtrl.jsp 文件中的应用程序组件:

<%@ taglib prefix="tags" tagdir="/WEB-INF/tags/" %> <%@ taglib prefix="feed" uri="/WEB-INF/feed.tld" %> <tags:setHeader name="Cache-Control" value="no-cache"<//> <jsp:useBean id="dataFeed" scope="application" class="ajaxapp.feed.DataFeed" <//> ${feed:getData(dataFeed, paramValues.symbol)}

设置无缓存头非常重要,其目的是为了禁止 Web 浏览器的缓存。下面是 setHeader.tag 文件的代码:

<%@ attribute name="name" required="true" %> <%@ attribute name="value" required="true" %> String name = (String) jspContext.getAttribute("name"); String value = (String) jspContext.getAttribute("value"); response.setHeader(name, value);

既然已经在服务器端实施了数据馈送,我们现在看一下如何从 Ajax 客户端对其进行访问。

访问数据馈送

Ajax 客户端发送 HTTP 请求至控制器,后者调用 getData() 方法并返回 JSON 字符串。HTTP 请求必须包含相关参数,在本例中为股票符号。requestFeedInfo() 函数(其代码可在 dataTable.js 中找到)从数据模型获取这些符号,并确保各个符号只包含一次,这是因为投资组合可能包含具有相同符号的多个股票:

function requestFeedInfo() { var symbolSet = new Array(); for (var i = 0; i < dataModel.stocks.length; i++) { var symbol = dataModel.stocks[i].symbol; if (isValidSymbol(symbol)) { for (var j = 0; j < symbolSet.length; j++) if (symbol == symbolSet[j].value) symbol = null; if (symbol) symbolSet[symbolSet.length] = symbol; if (symbolSet.length > 0) sendInfoRequest(symbolSet, feedInfoCallback);

该符号数组与 feedInfoCallback 一同传递到 sendInfoRequest(),从服务器接收数据时将调用该函数。如果前一 HTTP 请求没有完成,sendInfoRequest() 函数会放弃该请求,然后构建一个 HTTP 参数数组。该数组传递到 sendHttpRequest(),后者使用 Ajax API 构建请求并将其发送到服务器。sendHttpRequest() 函数可在 ajaxUtil.js 文件中找到,它的代码在前面标题为“ 使用 Ajax、JSF 和 ADF Faces 开发智能 Web UI ”的 OTN 文章中进行了讨论。下面是 ajaxLogic.js 中 sendInfoRequest() 函数的代码:

var ctrlURL = "ajaxCtrl.jsp"; var feedRequest = null; function sendInfoRequest(symbols, callback) { if (feedRequest) abortRequest(feedRequest); var params = new Array(); for (var i = 0; i < symbols.length; i++) params[i] = { name: "symbol", value: symbols[i] feedRequest = sendHttpRequest( "GET", ctrlURL, params, callback);

Ajax 回调可在 dataTable.js 文件中找到。feedInfoCallback() 函数使用 eval(request.responseText) 评估 JSON 响应,获得包含从数据馈送检索的信息的对象树。然后,它将最新的股价存储到数据模型中,并调用 updateDynamicCells() 来更新 UI:

function feedInfoCallback(request) { var feedInfo = eval(request.responseText); for (var i = 0; i < dataModel.stocks.length; i++) { var symbol = dataModel.stocks[i].symbol; dataModel.stocks[i].lastPrice = 0; for (var j = 0; j < feedInfo.length; j++) if (symbol == feedInfo[j].symbol) { var value = feedInfo[j].lastPrice; dataModel.stocks[i].lastPrice = value; updateDynamicCells();

在讨论 UI 代码前,我们看一下您如何使用 JavaScript 在 Web 浏览器中生成 XML 和 HTML。

使用 JavaScript 生成 XML 和 HTML

在 JSP 之前,servlet 用于在服务器端动态生成 HTML 内容。servlet 代码难以创建和维护,这是因为开发人员必须在 java 代码中手动编写 HTML 字符串代码,而且每一行标记必须放在 println() 语句中。另外其页面结构也难以查看,而且无法使用可视化工具设计编码为 servlet 的页面。

如今,servlet 作为 JSP 技术的基础表现非常出色,JSP 技术为在服务器端生成标记提供了一种更好的方式。但是在客户端,JavaScript 无法与 JSP 相比。很多开发人员使用类似于 servlet 的编码技巧来动态地创建 HTML 内容,或者使用 DOM API。当您需要使用 JavaScript 来生成 HTML 或 XML 时,这些方案没有一个简单易用。

将类似于 JSP 的语法与 JavaScript 结合使用

使用如下所示内容

<tag attr="[%= value %]"> [%= data %] </tag>

要比使用以下内容简单得多

"<tag attr=\"" + escapeXML(value) + "\">" + escapeXML(data) + "</tag>"

您再也不必使用 \ 来转义 " 了,而且 [%= %] 结构中包含的表达式会自动进行转义,这意味着 &、<、> 和 " 将分别为 &amp;、&lt;、&gt; 和 &quot; 所替代。有时您已经有了一段标记,并且您不想转义 &、<、> 和 "。此时,您可以使用 [%# %] 而非 [%= %]。

这样您可以方便地查看标记的结构,JavaScript 代码块可以包装在 [% 和 %] 之间,如下例所示:

<table class="tableClass"> [% for (var i = 0; i < rowCount; i++) { %] <tr class="[%= getRowClass(i) %]"> [% for (var j = 0; j < columnCount; j++) { %] <td class="[%= getCellClass(i, j) %]"> [%# buildCell(i, j) %] </td> [% </tr> [% </table>

上面的模板必须在服务器端转换为有效的 JavaScript 代码。如下所示:

var content = ""; content += "<table class=\"tableClass\">"; for (var i = 0; i < rowCount; i++) { content += "<tr class=\""; content += escapeXML(getRowClass(i)); content += "\">"; for (var j = 0; j < columnCount; j++) { content += "<td class=\""; content += escapeXML(getCellClass(i, j)); content += "\">"; content += buildCell(i, j); content += "</td>"; content += "</tr>"; content += "</table>";

如您所见,使用类似于 JSP 的语法时,代码的读取和维护要简单得多。下面我们看一下如何从模板生成 JavaScript 代码。

创建代码生成器

首先,我们需要一种方法对 JavaScript 字符串进行编码。我们将重用 JSONEncoder 类,它包含 character() 和 string() 方法:

package ajaxapp.util; public class JSONEncoder { private StringBuilder buf; public JSONEncoder() { buf = new StringBuilder(); public void character(char ch) { switch (ch) { case '\'': case '\"': case '\\': buf.append('\\'); buf.append(ch); break; default: if (ch >= 32 && ch < 128) buf.append(ch); else { public void string(String str) { int length = str.length(); for (int i = 0; i < length; i++) character(str.charAt(i)); public String toString() { return buf.toString(); public void clear() { buf.setLength(0);

JSBuilder 类(ajaxapp.builder 软件包提供)分析模板并生成 JavaScript 代码。需要对 JSONEncoder 类的 character() 和 string() 方法进行一些修改,以便供代码生成器使用:

ajaxapp.builder; import ajaxapp.util.JSONEncoder; public class JSBuilder { private JSONEncoder json; private String varName; private boolean isHTML; private PrintWriter out; public JSBuilder() { json = createEncoder(); private JSONEncoder createEncoder() { return new JSONEncoder() { private void outputLine() { String line = toString(); out.println(varName + " += \"" + line + "\";"); clear(); public void character(char ch) { super.character(ch); if (ch == '\n') outputLine(); public void string(String str) { super.string(str); if (toString().length() > 0) outputLine();

printContent() 方法使用定制的编码器生成 JavaScript 代码,以便在客户端重新构造一段 HTML 或 XML 内容:

public class JSBuilder { private void printContent(String content) { json.clear(); json.string(content);

printExpr() 方法生成可以转义和追加表达式的代码:

public class JSBuilder { private void printExpr(String expr, boolean escapeXML) { if (escapeXML) expr = "escapeXML(" + expr + ", " + isHTML + ")"; out.println(varName + " += " + expr + ";");

escapeXML() JavaScript 函数包含在 ajaxUtil.js 文件中。

build() 方法分析模板并生成 JavaScript 代码:

public class JSBuilder { public static final String CODE_STARTER = "[%"; public static final String CODE_ENDER = "%]"; public static final char EXPR_STARTER = '='; public static final char INCL_STARTER = '#'; public void build(String template, String varName, Writer writer, boolean isHTML) { this.varName = varName; this.isHTML = isHTML; out = new PrintWriter(writer, true); out.println("var " + varName + " = \"\";"); int contentStart = 0; int contentEnd = template.indexOf(CODE_STARTER); while (contentEnd != -1) { String content = template.substring( contentStart, contentEnd); printContent(content); int codeStart = contentEnd + 2; int codeEnd = template.indexOf(CODE_ENDER, codeStart); if (codeEnd == -1) { codeEnd = template.length(); template += CODE_ENDER; char starter = template.charAt(codeStart); if (starter == EXPR_STARTER || starter == INCL_STARTER) { codeStart++; String expr = template.substring(codeStart, codeEnd); printExpr(expr, starter == EXPR_STARTER); } else { String code = template.substring(codeStart, codeEnd); out.println(code); contentStart = codeEnd + 2; contentEnd = template.indexOf(CODE_STARTER, contentStart); String content = template.substring(contentStart); printContent(content);

我们用不到 100 行代码构建了一个模板分析器/代码生成器,可以显著地改善代码的可读性和可维护性。

在 JSP 页面中使用代码生成器

为了保持 JSP 页面的无脚本形式,可以从一个自定义的标记文件(名为 template.tag)中调用 JSBuilder 的 build() 方法,该文件将生成可返回生成内容的 JavaScript 函数。该函数的头可以通过一个属性来提供,模板可以放在起始标记和结束标记之间。下面是自定义标记的语法:

<tags:template function="myTemplate(...)"> ... template ... </tags:template>

<jsp:doBody> 操作在标记文件中用于获得模板的主体,后者存储在一个 JSP 变量中。接着,将模板传递给 build() 方法,该方法会生成 JavaScript 代码,该代码将生成客户端内容。下面是 template.tag 文件的代码:

<%@ attribute name="function" required="true" %> <%@ attribute name="isHTML" required="false" type="java.lang.Boolean" %> <%@ tag import="ajaxapp.builder.JSBuilder" %> <script language="javascript"> function ${function} { <jsp:doBody var="templateBody"<//> Boolean isHTML = (Boolean) jspContext.getAttribute("isHTML"); if (isHTML == null) isHTML = new Boolean(false); String templateBody = (String) jspContext.getAttribute("templateBody"); new JSBuilder().build(templateBody, "content", jspContext.getOut(), isHTML.booleanValue()); return content; </script>

index.jsp 页面使用模板标记来生成包含投资组合数据的 XML 文档。您可以使用 Oracle JDeveloper 将标记插入到 JSP 页面中。在 Component Palette 中选择 Local Tag Files:/WEB-INF/tags/,然后单击 Template 。Oracle JDeveloper 打开一个对话框。您可以在它的 Common Properties 选项卡中输入所需的函数属性,在 Advanced Properties 选项卡中输入可选的 isHTML 属性。输入函数的头,然后单击 OK:

然后,您可以输入模板的主体:

<tags:template function="buildPortfolioDoc()"> <portfolio> [% var stocks = dataModel.stocks; for (var i = 0; i < stocks.length; i++) { var stock = stocks[i]; %] <stock symbol="[%= stock.symbol %]" shares="[%= stock.shares %]" paidPrice="[%= stock.paidPrice %]"<//> [% </portfolio> </tags:template>

下一节将演示如何使用自定义标记来构建表编辑器/查看器。

表编辑器和查看器

在很多情况下,表组件必须针对用户的操作动态地进行变化。例如,用户单击某个按钮时,可能会插入或删除某一行。如果在服务器上重新创建了表的内容,用户在重新加载表时必须等待,而此时滚动位置会丢失。如果用户刚刚插入了一个空行,他/她必须向下滚动找到新行以输入数据。该操作非常耗时,不过使用 Ajax 和 DHTML 可以避免此操作。

初始化用户界面

应用程序的主页面 (index.jsp) 声明所使用的标记库、JavaScript 文件和样式表:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="tags" tagdir="/WEB-INF/tags/" %> </head> </html>

该页面的主体包含两个元素(<span> 和 <div>),稍后将借助 innerHTML 设置它们的内容。onLoad 属性指明浏览器在加载该页面后必须调用的函数:

<body onLoad="init()"> <h1 align="center"><span id="title"></span></h1> <div id="dataTable"> </body>

init() 函数尝试使用 ajaxLogic.js 中的 sendLoadRequest() 实用程序来加载投资组合。如果加载成功,股票将保存到数据模型中,编辑标志设置为 false。否则,init() 会增加三个空行,并设置编辑模式。setInterval() 将编程为每秒调用 requestFeedInfo() 函数:

function init() { var stocks = sendLoadRequest(); if (stocks && stocks.length > 0) { dataModel.setStocks(stocks); addDataChange(); setEditing(false); } else { for (var i = 0; i < 3; i++) dataModel.insertStock(i); addDataChange(); setEditing(true); setInterval("requestFeedInfo()", 1000);

使用层叠样式表

示例应用程序使用以下 CSS 文件:

BODY { background: #FFFFFF; color: #000000; } TH { font-weight: bold; background-color: #EEF8FF; border-top: 1px solid #CCCCCC; border-right: 1px solid #CCCCCC; border-bottom: 1px solid #CCCCCC; } TD { font-weight: normal; background-color: #FFFFFF; border-right: 1px solid #CCCCCC; border-bottom: 1px solid #CCCCCC; } TH.symbol, TD.symbol { text-align: left; border-left: 1px solid #CCCCCC; } TH.number, TD.number { text-align: right; } TD.button { text-align: center; } TD.leftButton { text-align: center; border-left: 1px solid #CCCCCC; } BUTTON { background-color: #EEEEEE; } INPUT.valid { background-color: #FFFFFF; } INPUT.error { background-color: #FFEEEE; } SPAN.gain { color: #008000; } SPAN.loss { color: #FF0000; } A:link, A:visited { color: #000000; text-decoration: underline; } A:hover, A:active { color: #FF0000; text-decoration: underline; }

您可以使用 Oracle JDeveloper 来输入 CSS 属性的值:

使用 JavaScript 创建表

构建示例应用程序的表的 JavaScript 代码由上一节提供的自定义模板标记在 index.jsp 文件中生成。下面的代码用于构建表的一个单元格:

<tags:template function="buildCell(row, column)" isHTML="true"> [% var value = ""; if (!isDynamic(row, column)) value = dataModel.getValueAt(row, column); if (isEditable(row, column)) { var id = getInputId(row, column); %] <input id="[%= id %]" name="[%= id %]" value="[%= value %]" type="text" size="[%= column == 2 ? 6 : 4 %]" onKeyUp="validateCellData([%= row %], [%= column %])" onChange="dataChanged([%= row %], [%= column %])"> [% } else { var id = getSpanId(row, column); %] <span id="[%= id %]">[%= value %]</span> [% </tags:template>

dataTable.js 文件包含在模板代码中调用的 JavaScript 函数。如果单元格的值是使用从馈送检索到的数据计算得出的,isDynamic() 函数返回 true:

function isDynamic(row, column) { return column >= 3;

如果用户可以修改单元格的值,isEditable() 函数返回 true:

function isEditable(row, column) { return isEditing() && column < 3;

getInputId() 和 getSpanId() 函数返回单元格的 <input> 和 <span> 元素的 ID:

function getInputId(row, column) { return "input_" + row + "_" + column; function getSpanId(row, column) { return "span_" + row + "_" + column;

按钮和链接通过另一模板生成。buildButton() 函数接受三个参数:按钮的标签、单击按钮(链接)时必须要调用的事件处理程序,以及用于该处理程序函数的一个可选参数:

<tags:template function="buildButton(label, handler, param)" isHTML="true"> [% if (!param) param = ""; %] <c:if test="${initParam.useButtons}"> <button onClick="[%# handler %]([%# param %])" type="button"> [%= label %] </button> </c:if> <c:if test="${!initParam.useButtons}"> <a href="javascript:[%# handler %]([%# param %])"> [%= label %] </c:if> </tags:template>

在 web.xml 文件中配置的 useButtons 参数确定 buildButton() 生成按钮还是链接:

<web-app ...> <context-param> <param-name>useButtons</param-name> <param-value>true</param-value> </context-param> </web-app>

表的模板以包含列名的表头开始。buildCell() 和 buildButton() 函数用于生成单元格的内容。最后一行包含附加的按钮和各项总计:

<tags:template function="buildTable()" isHTML="true"> <table border=0 cellpadding=5 cellspacing=0 align="center"> <tr> [% var columnCount = dataModel.getColumnCount(); for (var j = 0; j < columnCount; j++) { %] <th class="[%= j == 0 ? "symbol" : "number" %]"> [%= dataModel.getColumnName(j) %] </th> [% if (isEditing()) { %] <th class="header" colspan="2"> </th> [% </tr> [% var rowCount = dataModel.getRowCount(); for (var i = 0; i < rowCount; i++) { %] <tr> [% for (var j = 0; j < columnCount; j++) { %] <td class="[%= j == 0 ? "symbol" : "number" %]"> [%# buildCell(i, j) %] </td> [% if (isEditing()) { %] <td class="button"> [%# buildButton("Insert", "insertAction", i) %] <td class="button"> [%# buildButton("Delete", "deleteAction", i) %] </td> [% </tr> [% <td class="leftButton" colspan="3"> [% if (isEditing()) { %] [%# buildButton("Save", "saveAction") %] [% } else { %] [%# buildButton("Edit", "editAction") %] [% <td class="number">Total:</td> <td class="number"><span id="totalValue"></span></td> <td class="number"><span id="totalGain"></span></td> [% if (isEditing()) { %] <td class="button"> [%# buildButton("Add", "insertAction", rowCount) %] <td class="button"> [%# buildButton("Undo", "undoAction") %] </td> [% </table> </tags:template>

生成的 buildTable() 函数可以从 updateTableComponent() 中调用,后者的代码可在 dataTable.js 文件中找到:

function updateTableComponent() { getElementById("dataTable").innerHTML = buildTable(); updateDynamicCells(); requestFeedInfo(); if (isEditing()) validateTableData();

getElementById() 函数调用文档的 getElementById(),后者返回表示具有给定 ID 的 HTML 元素的 DOM 对象:

function getElementById(id) { return document.getElementById(id);

isEditing() 函数返回编辑标志的值,该值指明表组件是处于编辑模式还是查看模式:

var editing = false; function isEditing() { return editing;

除设置编辑标志外,setEditing() 函数还可修改页面的标题并更新表组件:

function setEditing(value) { editing = value; var title = "AJAX-based Portfolio " + (editing ? "Editor" : "Viewer"); getElementById("title").innerHTML = title; updateTableComponent();

验证表编辑器的数据

正则表达式提供了一种使用 JavaScript 验证数据的简单方式。例如,股票符号可以用下面的函数进行验证,函数代码可在 dataTable.js 中找到:

function isValidSymbol(symbol) { return symbol && (symbolRE.exec(symbol) == symbol);

示例应用程序使用三个正则表达式来验证用户输入:

var symbolRE = /[A-Za-z][A-Za-z][A-Za-z][A-Za-z]?/; var numberRE = /\d+/; var priceRE = /\d+\.?\d*/;

validateCellData() 函数验证单个单元格的值:

function validateCellData(row, column) { if (!isEditable(row, column)) return true; var valid = false; var inputElem = getElementById(getInputId(row, column)); var value = inputElem.value; if (value) { var regexp = null; switch (column) { case 0: regexp = symbolRE; break; case 1: regexp = numberRE; break; case 2: regexp = priceRE; break; if (regexp != null) valid = regexp.exec(value) == value; inputElem.className = valid ? "valid" : "error"; return valid;

用户错误用粉色的错误样式表示:

validateTableData() 函数验证整个表的数据:

function validateTableData() { var allValid = true; for (var i = 0; i < dataModel.getRowCount(); i++) for (var j = 0; j < dataModel.getColumnCount(); j++) { var valid = validateCellData(i, j); allValid = allValid && valid; return allValid;

出于安全考虑,应在服务器上重新验证数据。示例应用程序( 下载 )使用 portfolio.xsd 模式进行此操作。

刷新表查看器的数据

在 JavaScript 应用程序中,可以使用自定义实用程序(如 formatPrice() 函数)设置数据格式。formatPrice() 函数返回数值型价格的字符串表示:

function formatPrice(value) { var n = new Number(value); if (isNaN(n) || n == 0) return " "; return n.toFixed(2);

updateDynamicCells() 函数更新表的所有动态单元格。使用 <span> 元素的 innerHTML 属性,在找到每个动态单元格的 <span> 元素后,内容会发生变化。使用 <span> 元素的 className 属性,如果单元格表示收益或亏损,其样式也可以改变:

function updateDynamicCells() { for (var i = 0; i < dataModel.getRowCount(); i++) { for (var j = 0; j < dataModel.getColumnCount(); j++) { if (isDynamic(i, j)) { var value = dataModel.getValueAt(i, j); var spanElem = getElementById(getSpanId(i, j)); spanElem.innerHTML = formatPrice(value); if (j == 5) spanElem.className = value >= 0 ? "gain" : "loss"; var totalValue = dataModel.getTotalValue(); var totalValueElem = getElementById("totalValue") totalValueElem.innerHTML = formatPrice(totalValue); var totalGain = dataModel.getTotalGain(); var totalGainElem = getElementById("totalGain") totalGainElem.innerHTML = formatPrice(totalGain); totalGainElem.className = totalGain >= 0 ? "gain" : "loss";

使用事件处理程序

dataTable.js 文件含有处理 UI 事件的代码。每当用户改变一个单元格的值时,浏览器都会调用 dataChanged() 函数,该函数的调用在每个 <input> 元素的 onChange 属性的值中进行了编码。dataChanged() 函数能够验证单元格的数据、存储新值到数据模型中、更新动态单元格并请求馈送信息以防用户输入新符号:

function dataChanged(row, column) { validateCellData(row, column); var inputElem = getElementById(getInputId(row, column)); dataModel.setValueAt(inputElem.value, row, column); addDataChange(); updateDynamicCells(); requestFeedInfo();

addDataChange() 函数将股票保存到 dataChanges 数组中:

var dataChanges = new Array(); function addDataChange() { dataChanges[dataChanges.length] = dataModel.getStocks();

每次用户单击 Undo 按钮时,都会调用 undoAction() 函数。从 dataChanges 数组中检索前面的股票。然后,会更新数据模型和表:

function undoAction() { if (dataChanges.length >= 2) { var stocks = dataChanges[dataChanges.length-2]; dataChanges.length--; dataModel.setStocks(stocks); updateTableComponent();

insertAction() 函数插入一个新行:

function insertAction(row) { dataModel.insertStock(row); addDataChange(); updateTableComponent();

deleteAction() 函数删除一行:

function deleteAction(row) { dataModel.deleteStock(row); addDataChange(); updateTableComponent();

使用在 index.jsp 中生成的 buildPortfolioDoc() 函数和代码可在 ajaxLogic.js 中找到的 sendSaveRequest() 函数,saveAction() 函数可以将投资组合的数据发送至服务器。只有当整个表的数据都有效时,才会保存投资组合。

function saveAction() { if (validateTableData()) { sendSaveRequest(buildPortfolioDoc()); setEditing(false); } else { alert("Please provide valid values for the pink fields.");

editAction() 函数只调用 setEditing(true):

function editAction() { setEditing(true);

在本文中,您学习了如何构建类似于 Swing 的表模型和如何实现借助 Ajax 进行访问的数据馈送。然后,您看到了如何使用类似于 JSP 的语法通过 JavaScript 生成 HTML 和 XML。最后,您了解了如何构建表组件、使用 JavaScript 来验证它们的数据并进行格式设置、使用 CSS 定义它们的外观、处理事件以及撤消数据更改。

Andrei Cioroianu (devtools@devsphere.com) 是 Devsphere (www.devsphere.com) 的创始人,该公司是一个集开发、集成和咨询服务为一体的提供商。Cioroianu 撰写了许多 Java 文章,分别发表于 Oracle 技术网、ONJava (www.onjava.com)、JavaWorld (www.javaworld.com) 和 Java Developer’s Journal 。他还与别人合著了《Java XML Programmer's Reference》 和《Professional Java XML》 两书(均由 Wrox Press 出版)。