级别: 中级 Brian J Stewart, 首席顾问, Aqua Data Technologies, Inc.
2009 年 7 月 02 日 在本文中,构建可用于业务应用程序的 Asynchronous JavaScript + XML(Ajax)控件。这些基于 JavaServer Pages (JSP) TagLib 的可配置控件利用了 JavaScript Serialized Object Notation (JSON)、JavaScript 脚本语言及 Cascading Style Sheets(CSS)。由于它们是标准的 JSP TagLib 控件,您可以将其简单地置于任何应用程序中,从而提供更直观、响应性更好的用户界面。
简介
下一代 Web 站点的开发依赖于两个关键技术:Asynchronous JavaScript + XML(Ajax)和 JavaScript Serialized Object Notation(JSON)。业务应用程序能从这些技术中受益以提供更直观、响应性更好的用户界面。本文介绍了如何通过构建可重用的基于 Ajax 的 JavaServer
Pages (JSP) TagLib 控件向 Java™
Platform Enterprise Edition(Java EE)Web 应用程序中添加 Ajax 和 JSON。
在本文中,学习如何构建一个更新面板控件,该控件可以根据用户活动动态地从服务器异步检索内容。例如,可以在用户单击某个字段、在字段中输入值或从下拉列表中选择值时检索内容。本文还将描述如何构建一个
弹出对话框控件,它可以在对话框显示时从服务器异步检索对话框内容。例如,可以在用户将鼠标悬停在某个项目上、单击按钮或从下拉列表中选择值时显示对话框。
关于本系列
本文是由多部分组成的系列文章的其中之一,该系列文章介绍了如何为 J2EE 应用程序开发启用了 Ajax 的控件套件。这些控件均使用 JSP TagLib 控件构建并综合使用了 JSON、Servlet 和 JavaScript 技术。
关于本文
本文描述如何构建以下控件:
- 更新面板 — 异步检索内容,提供一个动态的用户界面。在检索内容时以及检索内容后,该控件为层叠样式表 (CSS) 格式提供可以配置的参数。它还提供了一些标准事件处理程序(即在载入内容之前以及在载入内容之后),允许未来进行进一步定制。
- 弹出对话框 — 提供一个窗口,用于在对话框初次显示时异步检索内容。这提供了一个快速响应的用户界面,它支持仅根据需要来检索内容。该控件为 CSS 格式化、对话框标题、位于窗口左上角的关闭按钮(类似于一个标准的 Windows® 对话框)以及对话框大小和位置提供了可选的可配置参数。
这些 JSP TagLib 控件封装所有异步通信、JavaScript 代码、CSS 格式化和超文本标记语言 (HTML) 生成。
技术概览
本文以本系列前两篇文章描述的设计原则和代码为基础(参见 参考资料)。本文将讨论一些初级和高级技巧,包括将事件处理程序添加到自定义控件,以及配置控件的外观和行为。
与本系列的上一篇文章相同,本文开发的支持 JSP Ajax 的控件的主要设计目标如下:
- 支持轻松地与已有 Web 应用程序集成 — 控件应该封装所有逻辑和 JavaScript 代码,以简化开发流程
- 允许简易配置
- 最大限度减少数据和页面大小开销
- 利用 CSS 和 HTML 标准
- 提供跨浏览器支持(Windows Internet Explorer、Mozilla Firefox)
- 利用公共设计模式/最佳实践改善代码可维护性
类似于本系列前两篇文章,<ajax:page/> 控件用于封装公共 JavaScript 功能,以最大限度减少数据和开销。一些 Web 标准(包括 CSS 和 HTML)用于提供跨浏览器支持。控件中的 JavaScript 代码、HTML 和 CSS 将针对 Windows Internet Explorer 7.x 和 Mozilla Firefox 2.x/3.x 进行测试。
类模型
本文中的示例由 Data Abstract Layer (DAL)、Data Transfer Objects (DTO)、Business Logic Layer (BLL)、Presentation Layer 和所支持的帮助类构成。本文还将使用本系列第一篇文章中的 LocationDataService DAL 类。该类返回一系列 LocationDTO DTO 实例。本文不需要任何额外的 DAL 或 DTO 类。BLL 包括向支持 Ajax 的控件提供数据的值提供程序,以及为 HTML 表单数据提供服务器验证的服务器端验证程序。
本文将使用本系列 第一篇文章 中的 LocationService 类和 JdbcQuery 帮助类。不需要其他实用工具类。
尽管您只构建两个控件,但您需要创建一些标记来确保这些控件稳定、灵活且可配置。首先来看图 1 中的更新面板控件。它是 <ajax:updatepanel/> 标记的实现类。
图 1. UpdatePanelTag 的 Unified Modeling Language (UML) 类图
更新面板控件支持可选参数,它们随更新面板的异步请求一同传递。<ajax:panelargument/> 标记的实现类如图 2 所示。
图 2. PanelArgumentTag 的 UML 类图
弹出控件更加复杂,并且将使用一些标记。主要标记是 <ajax:popupdialog/>。该标记的实现类如图 3 所示。
图 3. PopupDialogTag 的 UML 类图
<ajax:popupdialog/> 可以包含一个用于随对话框内容异步请求一同传递的参数的 <ajax:popuparguments/> 标记。<ajax:popuparguments/> 和 <ajax:popupargument/> 标记的实现类如图 4 所示。
图 4. PopupDialogButtonContainerTag 和 PopupDialogButtonTag 的 UML 类图
<ajax:popupdialog/> 可以包含一个用于为弹出对话框显示的按钮的 <ajax:popupbuttoncontainer/> 标记。<ajax:popupbuttoncontainer/> 和 <ajax:popupbutton/> 标记的实现类如图 5 所示。
图 5. PopupDialogArgumentContainerTag 和 PopupDialogArgumentTag 的 UML 类图
构建更新面板 JSP TagLib 控件
通常,即使用户操作只造成部分页面更改(比如用户在字段中输入一个值,从下拉列表中选择值,选中复选框或单击某个控件),Web 开发人员也需要刷新整个 Web 页面。虽然内容及业务需求存在差异,但大多数业务线应用程序都包含根据用户事件而更改的动态内容。
<ajax:updatepanel/> 控件的主要目标是构建一个可重用的控件来解决这一常见场景,并允许您轻松地动态更新页面的部分内容。该控件处理所有通信和表示功能,同时将内容留给 Web 开发人员处理。该控件允许在发起异步请求时选择性地传递参数,比如说一个表单字段值。为了提供最大的灵活性,应该能方便地显示或隐藏更新面板,并且不需要了解更新面板的内部原理。
图 6 显示了更新面板在更新时的外观。
图 6. 检索内容时的更新面板
当服务器响应异步请求时,更新面板内容将被更新。图 7 显示更新面板接收并显示异步内容之后的外观。
图 7. 检索内容后的更新面板
控件技术概览
初次呈现页面时,将为对话框呈现一个 DIV 容器。最初,更新面板元素为空。当事件触发显示更新面板时,将从服务器异步检索内容。然后,使用动态内容更新更新面板。
<ajax:updatepanel/> 提供如表 1 所示的可配置选项。
表 1. 更新面板的可配置选项
| 选项 | 描述 |
|---|
| id | 控件 ID |
|---|
| url | 检索更新面板内容的 URL |
|---|
| updatemessage | 异步检索内容时显示的消息 |
|---|
| loadingcss | 加载内容时应用的 CSS 类名(例如,“Please wait...” 消息) |
|---|
| cssclass | 加载内容后应用的 CSS 类名 |
|---|
| onclear | 清除更新面板之前调用的 JavaScript 函数的可选名称 |
|---|
| oncleared | 清除更新面板之后调用的 JavaScript 函数的可选名称 |
|---|
| onload | 加载更新面板之前调用的 JavaScript 函数的可选名称 |
|---|
| onloaded | 加载更新面板之后调用的 JavaScript 函数的可选名称 |
|---|
事件处理程序用例场景
事件处理程序是此控件的一个强大特性。虽然呈现和调用钩子(hook)的代码相当直观,但它们提供了一种机制可确保控件的高度灵活性和可重用性。
您可以使用 onload 事件处理程序测试该特定字段(更新面板所依赖的字段)是否已填充。如果未填充必要的字段,则可以向用户显示一条消息,并返回 False 取消对面板的更新。此函数的另一个作用是根据其他表单字段值填充任何隐藏的 INPUT 元素。例如,在事件处理程序中,可以对随异步请求一同传递的数据执行计算或字符串连接操作。
您可以使用 onloaded 事件处理程序根据服务器响应处理其他表单字段。例如,或许应该根据服务器响应禁用、隐藏或填充特定的字段。
您可以使用 onclear 事件处理程序在清除更新面板之前检查特定标准。例如,或许应该在清除更新面板之前提示用户保存数据。
您可以使用 oncleared 事件处理程序清除任何其他表单字段或重置表单。如果在报表解决方案中使用更新面板,您可以在表单上放置一个 Clear 按钮。该按钮可以调用隐藏面板函数,并且在清除面板时清除报表标准。
构建显示或隐藏更新面板的函数
<ajax:page/> 为 <ajax:updatepanel/> 控件呈现两个关键函数:
showUpdatePanel — 通过调用相应的 onShow 事件处理程序来显示更新面板
hideUpdatePanel — 通过调用相应的 onHide 事件处理程序来隐藏更新面板
这些函数几乎相同,其作用仅仅是特定于对话框的实例的 show 和 hide 对话框函数的包装器。对话框的名称通过 controlName 参数传递到这些函数中。onShow 函数名称将通过连接对话框名称与 _onShow 来动态构建。然后,使用 JavaScript 语句 window['functionName']() 调用在 <ajax:updatepanel/> 标记中呈现的特定于面板的函数。清单 1 显示了 showUpdatePanel 函数。
清单 1. showUpdatePanel JavaScript 函数
function showUpdatePanel(controlName) {
var functionName = controlName + '_onShow';
window[functionName]();
}
|
通过使用此方法,显示对话框以及异步检索内容的细节已经被使用 <ajax:updatepanel/> 控件的 Web 应用程序开发人员抽象出来。您只需要调用 showUpdatePanel 或 hideUpdatePanel JavaScript 函数,就可以传递要显示或隐藏的面板的名称。
显示更新面板
下一步是构建特定于面板的函数,它将由 showUpdatePanel JavaScript 函数调用。该函数由 <ajax:updatepanel/> 标记呈现,并且负责:
- 调用在
onclear、oncleared、onload 和 onloaded 标记属性中指定的任何事件处理程序
- 发起检索更新面板内容的异步请求
- 使用在属性中指定的选项显示更新面板,这些属性包括
cssclass、loadingcss 和 updatemessage
首先从 dialogName_onShow 函数开始,如 清单 2 所示。
清单 2. dialogName_onShow JavaScript 函数
function cityPanel_onShow() {
// Invoke the onload event.
// If true is not return by the event handler,
// cancel the update to the panel
if (!window['onCityPanelLoad']()) {
return;
}
// Get the dialog element
var controlName = 'cityPanel_Container';
var curControl= document.getElementById(controlName);
// Update status message to the value specified in the
// updatemessage attribute
if (curControl != null) {
curControl.innerHTML = 'Retrieving cities...';
curControl.className = 'loadingpanel';
}
// Url for retrieving dialog content (specified in url attribute)
var targetUrl = '/ajaxcontrols3/fragments/cityList.jsp';
// Build the POST data sent for the asynchronous request to
// retrieve the update panel content
// The arguments specified in the contained panelargument tags
// are dynamically added to the post data
var postData = 'panelId=cityPanel' + '&state=' + getFormValue('state')
+ '&cityPrefix=' + getFormValue('cityPrefix');
// Initialize XmlHttpRequest object
initializeXmlHttpRequest();
if (req!=null) {
req.onreadystatechange=cityPanel_onServerResponse;
// Set window status
window.status='Retrieving cities...';
// Open server request
req.open('POST',targetUrl,true);
req.setRequestHeader("Content-type",
"application/x-www-form-urlencoded");
req.setRequestHeader("Content-length",
postData.length);
// Send data
req.send(postData);
}
}
|
可选的事件处理程序 onclear、oncleared、onload 和 onloaded 都将通过使用相同的 window['functionName']() JavaScript 语句来进行调用。
发送给异步请求的数据将动态添加到请求中。您将使用 POST 而不是 GET,因为您希望避免任何因查询字符串长度过长和字符串转义而引起的问题。名称/值对将被添加到各个 panelargument 标记的 POST 数据中。值可以是使用 value 属性的静态值,或使用 sourcefield 属性的动态值。
显示更新面板的最后一步是处理服务器响应,这将通过 dialogName_onServerResponse 函数来完成。代码很简单。只需要检查响应,如果成功,则将更新面板中的内容替换为服务器响应。清单 3 显示了该函数。
清单 3. dialogName_onServerResponse JavaScript 函数
function cityPanel_onServerResponse() {
// If loaded
if(req.readyState!=4) return;
// If an error occurred
if(req.status != 200) {
alert('An error occurred retrieving data.');
return;
}
// Get panel
var curControl= document.getElementById('cityPanel_Container');
// If panel not found simply return (this shouldn't happen)
if (curControl == null)
return;
// Update CSS class to the value specified in the cssclass attribute
curControl.className = 'normalpanel';
// Set content of element to the server response
curControl.innerHTML = req.responseText;
// Clear window status
window.status='';
// Invoke the onloaded event handler (this is not rendered
// if there is no event handler specified in the oncleared
// attribute)
window['onCityPanelLoaded']();
}
|
隐藏更新面板
接下来,您将构建 dialogName_onHide 函数。该函数将由 hideUpdatePanel 函数调用。除了清除面板之外,其主要作用还有通过自定义事件处理程序可选择性地执行业务规则。该函数如清单 4 所示。
清单 4. dialogName_onHide JavaScript 函数
function onHide_cityPanel() {
// Invoke the onclear event.
// If true is not returned by the event handler,
// the action is cancelled and the panel remains
// visible
if(!window['onCityPanelClear']()) {
return;
}
// Retrieve the update panel container
var controlName = 'cityPanel_Container';
var curControl= document.getElementById(controlName);
// If control wasn't found, return
if (curControl == null) {
alert(controlName + ' control not found!');
return;
}
// Clear tag
curControl.innerHTML = '';
// Invoke the oncleared event handler (this is not rendered
// if there is no event handler specified in the oncleared
// attribute)
window['onCityPanelCleared']();
}
|
更新面板 TagLib 库定义条目
下一步是在 TagLib 库定义文件中定义 <ajax:updatepanel/> JSP 控件。清单 5 显示了更新面板控件的 TagLib 库定义条目(为每个属性都嵌入了描述性注释)。
清单 5. 更新面板 TagLib 库定义条目
<tag>
<name>updatepanel</name>
<tagclass>com.testwebsite.controls.updatepanel.UpdatePanelTag</tagclass>
<bodycontent>JSP</bodycontent>
<info>Update Panel tag.</info>
<!-- Panel ID-->
<attribute>
<name>id</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Url to fragment-->
<attribute>
<name>url</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Message displayed while retrieving panel content-->
<attribute>
<name>updatemessage</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Loading CSS class name -->
<attribute>
<name>loadingcss</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- CSS class name (Post loading) -->
<attribute>
<name>cssclass</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Event: on clear -->
<attribute>
<name>onclear</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Event: on cleared -->
<attribute>
<name>oncleared</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Event: on loading-->
<attribute>
<name>onload</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Event: post loaded-->
<attribute>
<name>onloaded</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
|
面板参数 TagLib 库定义条目
清单 6 显示了 TagLib 库定义文件中的 <ajax:panelargument/> JSP 控件(为每个属性都嵌入了描述性注释)。
清单 6. 面板参数 TagLib 库定义条目
<tag>
<name>panelargument</name>
<tagclass>com.testwebsite.controls.updatepanel.PanelArgumentTag</tagclass>
<bodycontent>empty</bodycontent>
<info>Contains the argument tag.</info>
<!-- Argument name -->
<attribute>
<name>name</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Source field for argument value -->
<attribute>
<name>sourcefield</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Value for argument (overrides source field value if specified -->
<attribute>
<name>value</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
|
构建弹出对话框 JSP TagLib 控件
通常,Web 开发人员使用 window.open 或 window.showModalDialog 在 Web 应用程序中显示弹出窗口。虽然它们的效果非常好,但是很复杂。首先,引用其他表单字段有时非常麻烦。其次,表示并不像 Document Object Model (DOM) Scripting 和基于 CSS 的对话框那样简单直观。第三,它们无法顺利地在用户悬停在某个项目上时显示更多信息。
通过使用 DOM 脚本和 CSS z-index
属性,您可以模拟某个 Web 页面的对话框。借助 Ajax 和异步技术,您可以构建相当强大和灵活的对话框,因为可以根据需要从服务器异步检索数据。
<ajax:popupdialog/> 的主要目标是创建一个基于 JSP 的弹出对话框控件,该控件结合 DOM 脚本、CSS 和 Ajax
创建可方便添加到任何业务应用程序中的可重用控件。弹出对话框控件包含一些标记,如表 2 所示。
表 2. 弹出对话框控件标记
| 标记 | 描述 |
|---|
| popupdialog | 弹出对话框 |
|---|
| popuparguments | 可选标记,包含一个或多个传递给弹出对话框的参数(popupargument 标记) |
|---|
| popupargument | 弹出对话框的弹出对话框参数 |
|---|
| popupbuttoncontainer | 可选标记,包含一个或多个按钮(popupbutton 标记)。它还可以包含 HTML 用于格式化按钮布局 |
|---|
| popupbutton | 弹出对话框的按钮 |
|---|
图 8 显示了弹出对话框的组件。
图 8. 示例弹出对话框容器
控件技术概览
初次呈现页面时,将为对话框呈现一个 DIV 容器。应用样式 display: none 隐藏弹出对话框。容器将可选择性地包含一个标题面板(由 showtitle 和 title 属性决定)和一个按钮面板(由 <ajax:popupdialog/> 是否包含一个 <ajax:popupbuttoncontainer/> 标记决定)。它还将在对话框内容的对话框容器中呈现 DIV。该元素最初为空,并且将在为相应弹出对话框调用 showDialog 时添加对话框内容。显示弹出对话框时,会发生以下事情:
- 样式更改为
display: block,使弹出对话框可见。
- 检索对话框内容时在弹出对话框中显示
updatemessage。
- 向服务器发出一条异步请求,检索对话框内容。
- 在接收到服务器响应时,显示对话框内容。
另外,还对弹出对话框容器应用了两个重要属性以确保它正确运行。第一个是 z-index: 100;,用于确保对话框容器显示在其他 Web 页面元素之上。第二个是 position: absolute;,用于允许对弹出对话框进行定位。
弹出对话框控件提供了一些可配置的特性,用于定义弹出对话框的行为和外观。表 3 列出了受支持的可配置特性。
表 3. 弹出对话框控件的可配置特性
| 特性 | 描述 |
|---|
| id | 控件 ID |
|---|
| 标题 | 对话框标题 |
|---|
| showtitle | 控制是否显示对话框标题的选项(默认:true) |
|---|
| showclose | 控制是否显示关闭按钮的选项(默认:true) |
|---|
| width | 对话框的宽度 |
|---|
| height | 对话框的高度 |
|---|
| contentheight | 可以滚动的对话框内容窗格的高度 |
|---|
| left | 对话框的左定位 |
|---|
| top | 对话框的上定位 |
|---|
| url | 在显示对话框时检索对话框内容的 URL |
|---|
| updatemessage | 检索内容时显示的消息 |
|---|
构建显示或隐藏弹出对话框的函数
<ajax:page/> 呈现 <ajax:popupdialog/> 控件的两个关键函数:
showDialog — 通过调用相应的 onShow 事件处理程序来显示弹出对话框。
closeDialog — 通过调用相应的 onHide 事件处理程序来隐藏弹出对话框。
类似于更新面板,这些函数的作用基本相同,仅为特定于对话框的实例的 show 和 hide 和对话框函数提供包装器。在讨论更新面板时提到,这种方法抽象如何显示对话框以及如何从 <ajax:popupdialog/> 控件使用者那里异步检索内容的细节。您只需要调用 showDialog 或 closeDialog JavaScript 函数,传递显示或隐藏更新面板的对话框的名称。
显示弹出对话框
下一步是构建特定于对话框的函数来显示弹出对话框。该函数主要由 <ajax:popupdialog/> 标记呈现,包含基于在标记的属性中指定的选项的 JavaScript 代码。例如,设置对话框的标题,构建 POST 数据和 URL,以及发起异步请求。该函数如清单 7 所示。
清单 7. dialogName_onShow JavaScript 函数
function stateDialog_onShow() {
// Get dialog control
var dialogControl = document.getElementById('stateDialog');
dialogControl.style.display = 'block';
// Set title (if showtitle attribute is not false)
var titleControl = document.getElementById('stateDialog_dialogTitleText');
titleControl.innerHTML = 'Select States';
// Get dialog content control
var dialogContentName = 'stateDialog_dialogContent';
var contentControl = document.stateDialog_dialogForm[dialogContentName];
// If dialog content control is not null
if (contentControl != null) {
contentControl.innerHTML = 'null';
}
// Initialize XmlHttpRequest object
initializeXmlHttpRequest();
if (req == null) {
alert('XmlHttpRequest object not initialized.');
}
// Url for retrieving dialog content (specified in url attribute)
var targetUrl = '/ajaxcontrols3/fragments/selectState.jsp';
// Build the POST data sent for the asynchronous request to
// retrieve the dialog content
// The arguments specified in the optional argument tag(s)
// contained in the popuparguments tag
var postData = '';
// The dialogName_getPostData function is rendered by the
// popuparguments tag (if specified).
// Check if the function is defined and if so, invoke the
// function
if (window['stateDialog_getPostData'] != undefined) {
postData = window['stateDialog_getPostData']();
}
// Set server response function
var responseFunction = 'stateDialog_onServerResponse';
req.onreadystatechange = eval(responseFunction);
// Open server request
req.open('POST',targetUrl,true);
req.setRequestHeader("Content-type",
"application/x-www-form-urlencoded");
req.setRequestHeader("Content-length",
postData.length);
// Send data
req.send(postData);
}
|
下一步是处理服务器响应,这需要使用 dialogName_onServerResponse 函数。代码并不难。对话框内容被替换为异步服务器请求返回的 HTML。服务器响应函数如清单 8 所示。
清单 8. dialogName_onServerResponse JavaScript 函数
function stateDialog_onServerResponse() {
// If loaded
if(req.readyState!=4)
return;
// If an error occurred
if(req.status != 200) {
alert('An error occurred retrieving panel content.');
return;
}
// Get response
var responseData = req.responseText;
// Get dialog content pane
var dialogContentName = 'stateDialog_dialogContent';
var contentControl = document.getElementById(dialogContentName);
// If dialog content pane found, set the content
if (contentControl != null) {
contentControl.innerHTML = responseData;
}
}
|
弹出对话框 TagLib 库定义条目
下一步是在 TagLib 库定义文件中定义 <ajax:popupdialog/> JSP 控件。清单 9 显示了弹出对话框控件的 TagLib 库定义条目(为每个属性都嵌入了描述性注释)。
清单 9. 弹出对话框 TagLib 库定义条目
<tag>
<name>popupdialog</name>
<tagclass>com.testwebsite.controls.popupdialog.PopupDialogTag</tagclass>
<bodycontent>JSP</bodycontent>
<info>Popup Dialog tag.</info>
<!-- Popup ID-->
<attribute>
<name>id</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Popup Title-->
<attribute>
<name>title</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Show Title -->
<attribute>
<name>showtitle</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Show Close Button -->
<attribute>
<name>showclose</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Size Width -->
<attribute>
<name>width</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Size Height -->
<attribute>
<name>height</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Size Content Height -->
<attribute>
<name>contentheight</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Position Left -->
<attribute>
<name>left</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Position Top-->
<attribute>
<name>top</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Url to fragment-->
<attribute>
<name>url</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Message displayed while retrieving panel content-->
<attribute>
<name>updatemessage</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
|
弹出参数容器和弹出参数控件
<ajax:popupdialog/> 可以选择性地包含 <ajax:popuparguments/> 标记,发送异步请求的 POST 数据以检索对话框内容。该标记负责呈现 dialogName_getPostData 函数。清单 10 显示了一个示例 dialogName_getPostData 函数,用于返回包含 POST 数据的所有名称/值对的字符串。
清单 10. dialogName_getPostData JavaScript 函数
function cityDetailsDialog_getPostData() {
var data = 'dialogName=cityDetailsDialog'
+ '&zipCode=' + getFormValue('selectedZipCode');
return data;
}
|
每个 popupargument 标记都呈现有各自的名称/值。以下代码片段是为各个参数呈现的:+ '&zipCode=' + getFormValue('selectedZipCode');。稍后将详细讨论 popupargument 标记。
doStartTag 呈现 dialogName_getPostData 函数的开始。<ajax:popuparguments/> 的方法如清单 11 所示。
清单 11. 弹出参数容器 doStartTag 方法
public int doStartTag() throws JspException {
StringBuffer html = new StringBuffer();
String dialogName = this.getDialogName();
html.append("<script type='text/javascript' language='javascript'>");
html.append("function ");
html.append(dialogName);
html.append("_getPostData() {");
html.append("var data = 'dialogName=");
html.append(dialogName);
html.append("'");
JspWriter out = pageContext.getOut();
try {
out.append(html.toString());
} catch (IOException e) {
e.printStackTrace();
}
return PopupDialogArgumentContainerTag.EVAL_BODY_INCLUDE;
}
|
doEndTag 方法关闭 dialogName_getPostData 函数,如清单 12 所示。
清单 12. 弹出参数容器 doEndTag 方法
public int doEndTag() throws JspException {
StringBuffer html = new StringBuffer();
html.append(";");
html.append("return data;");
html.append("}");
html.append("</script>");
// Write output
JspWriter out = pageContext.getOut();
try {
out.append(html.toString());
} catch (IOException e) {
e.printStackTrace();
}
return PopupDialogArgumentContainerTag.EVAL_PAGE;
}
|
下一步是在 TagLib 库定义文件中定义 <ajax:popuparguments/> JSP 控件。清单 13 显示了弹出对话框参数容器的 TagLib 库定义条目(为每个属性都嵌入了描述性注释)。
清单 13. 弹出参数 TagLib 库定义条目
<tag>
<name>popuparguments</name>
<tagclass>com.testwebsite.controls.popupdialog.PopupDialogArgumentContainerTag
</tagclass>
<bodycontent>JSP</bodycontent>
<info>Popup Dialog Argument Container</info>
<!-- Popup Argument Container ID-->
<attribute>
<name>id</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
|
下一步是在 TagLib 库定义文件中定义 <ajax:popupargument/>。弹出参数的 TagLib 库定义条目(为每个属性都嵌入了描述性注释)如清单 14 所示。
清单 14. 弹出参数 TagLib 库定义条目
<tag>
<name>popupargument</name>
<tagclass>com.testwebsite.controls.popupdialog.PopupArgumentTag</tagclass>
<bodycontent>empty</bodycontent>
<info>Contains the argument tag.</info>
<!-- Argument name -->
<attribute>
<name>name</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Source field for argument value -->
<attribute>
<name>sourcefield</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- Value for argument (overrides source field value if specified) -->
<attribute>
<name>value</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
|
弹出按钮容器和按钮控件
<ajax:popupdialog/> 可以选择性地包含一个按钮面板 <ajax:popupbuttoncontainer/> 标记。容器标记将呈现一个名称为 dialogName_dialogButtonContainer 的 DIV 元素。
<ajax:popupbuttoncontainer/> 标记可以包含一个或多个 <ajax:popupbutton/> 标记。弹出对话框控件支持两种按钮 — cancel 和 normal。如果按钮类型是 cancel,那么在单击按钮时将调用 closeDialog。如果按钮类型是 normal,则调用在 onclick 属性中指定的 JavaScript 函数。
按钮呈现在弹出按钮的 doStartTag 方法中。被调用的按钮和事件处理程序的类型的逻辑包含在此方法中,如清单 15 所示。
清单 15. 弹出按钮 doStartTag 方法
public int doStartTag() throws JspException {
StringBuffer html = new StringBuffer();
// Get button container
PopupDialogButtonContainerTag buttonPane =
(PopupDialogButtonContainerTag) this.getParent();
String dialogName = buttonPane.getDialogName();
// Render button
html.append("<input type='button' id='");
html.append(this.getId());
html.append("' class='");
html.append(this.getCssclass());
String buttonLabel = this.getLabel();
if (buttonLabel != null && buttonLabel.length() > 0) {
html.append("' value='");
html.append(buttonLabel);
}
html.append("' onclick='");
// Check button type and render event handler accordingly
String buttonType = this.getType();
if (buttonType == null ||
buttonType.length() < 1 ||
buttonType.equalsIgnoreCase("cancel")) {
html.append("closeDialog(\"");
html.append(dialogName);
html.append("\")");
}
else {
html.append(this.getOnclick());
html.append("(\"");
html.append(dialogName);
html.append("\", document.");
html.append(dialogName);
html.append("_dialogForm)");
}
html.append("'/>");
// Write output
JspWriter out = pageContext.getOut();
try {
out.append(html.toString());
} catch (IOException e) {
e.printStackTrace();
}
return PopupButtonTag.EVAL_BODY_BUFFERED;
}
|
下一步是在 TagLib 库定义文件中定义 <ajax:popupbuttoncontainer/> JSP 控件。按钮面板容器的 TagLib 库定义条目(为每个属性都嵌入了描述性注释)如清单 16 所示。
清单 16. 弹出对话框按钮容器 TagLib 库定义条目
<tag>
<name>popupbuttoncontainer</name>
<tagclass>com.testwebsite.controls.popupdialog.PopupDialogButtonContainerTag
</tagclass>
<bodycontent>JSP</bodycontent>
<info>Popup Dialog Button Container</info>
<!-- Popup Button Container ID-->
<attribute>
<name>id</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
|
接下来,您希望在 TagLib 库定义文件中定义 <ajax:popupbutton/> JSP 控件。按钮的 TagLib 库定义条目(为每个属性都嵌入了描述性注释)如清单 17 所示。
清单 17. 弹出按钮 TagLib library 定义条目
<tag>
<name>popupbutton</name>
<tagclass>com.testwebsite.controls.popupdialog.PopupButtonTag</tagclass>
<bodycontent>empty</bodycontent>
<info>Contains the argument tag.</info>
<!-- Button id -->
<attribute>
<name>id</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<!-- Button label -->
<attribute>
<name>label</name>
<required>false</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<!-- Type of button -->
<attribute>
<name>type</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- onclick Event handler -->
<attribute>
<name>onclick</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<!-- CSS class name -->
<attribute>
<name>cssclass</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
|
测试控件
可以使用三个测试页面来演示新的支持 Ajax 的控件。这些页面如表 4 所示。
表 4. 测试页面
| 页面 | 描述 |
|---|
| SearchLocations.jsp | 使用 <ajax:updatepanel/> 提供对包含城市的数据单元格的实时过滤。城市列表可以按州(下拉选择列表)和城市名称(文本字段)过滤。这两个字段将在各自值发生更改时调用 showUpdatePanel。 |
|---|
| LocationReport.jsp | 使用 <ajax:popupdialog/> 提供一个模拟报表页面。报表标准包括一个州文本字段,单击该字段将显示一个弹出对话框,其中包含带复选框的所有州的列表。当用户单击 Save 按钮时,将建立一个逗号分隔的州列表,并将州文本字段的值设置到逗号分隔列表中。 |
|---|
| NewYorkCityList.jsp | 显示 New York 的所有城市列表。当用户将鼠标悬停在城市列表的某个邮政编码上时,它使用 <ajax:popupdialog/> 显示城市的额外信息。鼠标移开邮政编码时将隐藏弹出对话框。 |
|---|
Search Locations 页面 (SearchLocations.jsp)
Search Locations 页面演示如何使用更新面板控件。图 9 显示了一个示例更新面板。
图 9. 更新面板演示:Search Locations 页面
Search Locations 页面演示如何使用事件处理程序,以及如何将更新面板与两个控件挂在一起(一个 SELECT 和一个 INPUT 文本框)。清单 18 显示了此示例页面的代码。
清单 18. SearchLocations.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="ajax" uri="/WEB-INF/tlds/ajax_controls.tld"%>
<html>
<head>
<title>Locations</title>
<link href="core.css" rel="stylesheet" type="text/css" />
<ajax:page/>
<script type='text/javascript' language='javascript'>
function onCityPanelCleared() {
var msgControl = document.getElementById('statusMessage');
if (msgControl != null) {
msgControl.innerHTML = 'oncleared';
}
}
function onCityPanelClear() {
var msgControl= document.getElementById('statusMessage');
if (msgControl != null) {
msgControl.innerHTML = 'onclear';
}
return true;
}
function onCityPanelLoad() {
var msgControl = document.getElementById('statusMessage');
if (msgControl != null) {
msgControl.innerHTML = 'onload';
}
return true;
}
function onCityPanelLoaded() {
var msgControl= document.getElementById('statusMessage');
if (msgControl != null) {
msgControl.innerHTML = 'onloaded';
}
}
function displayCityPanel(curControl) {
if (!curControl.checked) {
hideUpdatePanel('cityPanel');
}
}
function retrieveCityData() {
showUpdatePanel('cityPanel');
document.all.showCityPanel.checked = 'checked';
}
</script>
</head>
<body>
<b>State: </b>
<select id="state" onchange="retrieveCityData()">
<option value=''> </option>
<option value='AK'> Alaska</option>
<option value='AL'> Alabama</option>
<option value='AR'> Arkansas</option>
...
...
<option value='WI'> Wisconsin</option>
<option value='WV'> West Virginia</option>
<option value='WY'> Wyoming</option>
</select><br/>
<b>City Prefix: </b>
<input type="text" id="cityPrefix" onkeyup="retrieveCityData()"/>
<br/>
<input type="checkbox"
id="showCityPanel"
name="showCityPanel"
checked="checked"
onclick="displayCityPanel(this)" />Show City Panel<br/>
<div><b>Result: </b><span id="statusMessage"></span></div>
<div>
<ajax:updatepanel id="cityPanel"
url="/fragments/cityList.jsp"
updatemessage="Retrieving cities..."
cssclass="normalpanel"
loadingcss="loadingpanel"
onclear="onCityPanelClear"
oncleared="onCityPanelCleared"
onloaded="onCityPanelLoaded"
onload="onCityPanelLoad">
<ajax:panelargument name="state" sourcefield="state"/>
<ajax:panelargument name="cityPrefix" sourcefield="cityPrefix"/>
</ajax:updatepanel>
</div>
</body>
</html>
|
Location 报表 (LocationReport.jsp)
Location 报表演示如何使用弹出对话框允许用户选择多个州(复选框)。当用户选择弹出对话框上的 Save 按钮时,一个文本框将被设置为包含所选州的逗号分隔列表。图 10 显示了弹出对话框。
图 10. 弹出对话框演示:Location 报表
当用户双击 selectedStates 控件时,将使用 showDialog 函数显示弹出对话框。当用户单击 popupdialog 上的 Save 按钮时,将调用 onSaveStates JavaScript 函数,这将建立一个由所选值构成的逗号分隔列表。然后,使用列表填充 selectedStates 文本框。Location 报表的代码如清单 19 所示。
清单 19. LocationReport.jsp
<%@ page language="java" contentType="text/html;
charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="ajax"
uri="/WEB-INF/tlds/ajax_controls.tld"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;
charset=ISO-8859-1">
<title>Search Locations</title>
<link href="core.css" rel="stylesheet"
type="text/css" />
<script type="text/javascript">
function onSaveStates(dialogName, dialogForm) {
var values = '';
var stateList = dialogForm.states;
for (var i = 0; i < stateList.length; i++) {
if (stateList[i].checked) {
if (values.length != 0) {
values += ", ";
}
values += stateList[i].value;
}
}
var targetControl = document.getElementById('selectedStates');
targetControl.value = values;
closeDialog(dialogName);
}
</script>
<ajax:page id="page"/>
</head>
<body>
<h1>Report Criteria</h1>
<table width="80%" >
<colgroup>
<col width="40%"/>
<col width="60%"/>
</colgroup>
<tbody>
<tr>
<td align="right" style="border:none;">
<span style="font-weight:bold">Zip Code:
</span>
</td>
<td align="left" style="border:none;">
<input type="text"
size="50"
id="zipCode" />
</td> </tr>
<tr>
<td align="right" style="border:none;">
<span style="font-weight:bold">States:
</span>
</td>
<td align="left" style="border:none;">
<input type="text"
size="50"
readonly="readonly"
id="selectedStates"
onclick="showDialog('stateDialog')"/>
</td> </tr>
</tbody>
<thead>
<th colspan="2" class="subHeading">
Search Locations</th>
</thead>
<tfoot>
<tr>
<td colspan="2">
<input type="button" class="dialogButton"
value="Run Report" id="runReport"/>
</td> </tr>
</tfoot>
</table>
<ajax:popupdialog url="/ajaxcontrols3/fragments/selectState.jsp"
id="stateDialog"
width="400px" height="28em"
left="0" top="0"
title="Select States"
contentheight="23em">
<ajax:popuparguments id="stateDialogArgs">
<ajax:popupargument name="arg1" value="test1"/>
<ajax:popupargument name="arg2" value="test2"/>
</ajax:popuparguments>
<ajax:popupbuttoncontainer id="stateDialogButtons">
<ajax:popupbutton id="cancelButton"
label="Cancel" type="cancel"/>
<ajax:popupbutton id="saveButton" label="Save"
type="normal" onclick="onSaveStates"/>
</ajax:popupbuttoncontainer>
</ajax:popupdialog>
<h1>Report Results</h1>
<div>Show report results here...</div>
</body>
</html>
|
清单 20 显示了在弹出对话框中显示的页面。代码相对比较简单。从 Location Data Service 检索得到一个地点列表,然后迭代此列表,并为每个州呈现一个复选框。
清单 20. selectState.jsp
<%@ page language="java" contentType="text/html;
charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ page import="com.testwebsite.dal.LocationDataService" %>
<%@ page import="com.testwebsite.dto.LocationDTO" %>
<%@ page import="com.testwebsite.dto.StateDTO" %>
<%@ page import="java.util.Iterator" %>
<%@ page import="java.util.TreeMap" %>
<%@ page import="java.util.Set" %>
<ul style="list-style: none;"
<%
TreeMap<String, StateDTO> stateData =
LocationDataService.getStateData();
// Iterate through all data looking for matching
// cities and add to temporary TreeMap
Set<String> keySet = stateData.keySet();
Iterator<String> stateIter = keySet.iterator();
while (stateIter.hasNext()) {
// Get current state
String curKey = stateIter.next();
StateDTO curState = stateData.get(curKey);
out.println("<li><input type='checkbox'
name='states' value='");
out.println(curState.getAbbreviation());
out.println("'/> ");
out.println(curState.getName());
out.println("</li>");
}
%>
</ul>
|
New York City Listing 页面 (NewYorkCityList.jsp)
New York City Listing 页面演示了另一种利用弹出对话框的方法。许多 Web 应用程序中的一个常见场景就是当用户将鼠标悬停在列表中的某个项目上时显示额外信息。弹出对话框允许仅通过 Ajax 发起请求时检索详细信息。本示例如图 11 所示。
图 11. 示例弹出对话框容器
New York City Listing 页面的代码如 清单
21 所示。本示例中的关键函数是 showCityDialog,它接受一个参数,即用户当前悬停项目的邮政编码。使用标准 CSS 属性和 JavaScript 代码设置对话框位置。然后显示 showDialog 函数。当用户从邮政编码移开时,将调用 hideDialog 函数以隐藏弹出对话框。
清单 21. NewYorkCityList.jsp
<html><head>
<title>New York City Listing</title>
<ajax:page/>
<link href="core.css" rel="stylesheet"
type="text/css" />
</head>
<script type="text/javascript">
function showCityDialog(zipCode) {
var xPosition = 0;
var yPosition = 0;
if (!e) var e = window.event;
if (e.pageX || e.pageY) {
posx = e.pageX;
yPosition = e.pageY;
}
else if (e.clientX || e.clientY) {
xPosition = e.clientX + document.body.scrollLeft
+ document.documentElement.scrollLeft;
yPosition = e.clientY + document.body.scrollTop
+ document.documentElement.scrollTop;
}
var dialogControl = document.getElementById("cityDetailsDialog");
if (dialogControl != null) {
dialogControl.style.top = yPosition;
dialogControl.style.left = xPosition;
}
// Get selected zip code
var zipCodeControl = document.getElementById("selectedZipCode");
if (zipCodeControl != null) {
zipCodeControl.value = zipCode;
showDialog('cityDetailsDialog');
}
}
</script>
<body>
<table cellpadding="0" cellspacing="0" width="60%">
<tbody> <%
TreeMap<Integer, LocationDTO> locData =
LocationDataService.getLocationData();
// Temp map to ensure only unique cities are added.
HashMap tempMap = new HashMap();
// 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);
String curState = curLocation.getState();
String curCity = curLocation.getCity();
String lowerCurCity = curCity.toLowerCase();
if (!tempMap.containsKey(curCity) &&
curState.equalsIgnoreCase("NY")) {
tempMap.put(curCity, curCity);
out.println("<tr>");
out.println("<td><a href='#'
style='text-decoration:none;color'
onMouseOver=\"showCityDialog('" +
curLocation.getZipCode() +
"')\" >");
out.println(curLocation.getZipCode());
out.println("</td>");
out.println("<td>");
out.println(curLocation.getCity());
out.println("</td>");
out.println("<td>");
out.println(curLocation.getCounty());
out.println("</td>");
out.println("</tr>");
}
} %>
</tbody>
<thead> <tr>
<th colspan="3"><b>Cities</b></th></tr>
<tr>
<th class="subHeading"> <b>Zip Code</b></th>
<th class="subHeading">
<b>City</b></th>
<th class="subHeading">
<b>County</b></th>
</tr> </thead>
</table>
<input type="hidden" id="selectedZipCode"
value=""/>
<ajax:popupdialog url="/ajaxcontrols3/fragments/cityDetails.jsp"
id="cityDetailsDialog" width="400px"
left="0" top="0" title="City Information"
height="10.5em" contentheight="8.5em">
<ajax:popuparguments id="stateDialogArgs">
<ajax:popupargument name="zipCode"
sourcefield="selectedZipCode"/>
</ajax:popuparguments>
</ajax:popupdialog>
</body></html>
|
清单 22 显示了弹出对话框内容的 JSP 代码。它相当清楚。从 Location Data Service 检索城市的详细信息,并显示相应的记录。
清单 22. cityDetails.jsp
<%@ page language="java" contentType="text/html;
charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ page import="com.testwebsite.dal.LocationDataService" %>
<%@ page import="com.testwebsite.dto.LocationDTO" %>
<%@ page import="com.testwebsite.dto.StateDTO" %>
<%@ page import="java.util.Iterator" %>
<%@ page import="java.util.TreeMap" %>
<%@ page import="java.util.Set" %>
<%
TreeMap<Integer, LocationDTO> locData = LocationDataService.getLocationData();
String zipCodeStr = request.getParameter("zipCode");
Integer zipCode = new Integer(zipCodeStr);
LocationDTO curLocation = locData.get(zipCode);
if (curLocation != null) {
out.println("<div id='cityDetails'>");
// Render zip code
out.println("<div>");
out.println("<span style='font-weight:bold;color:
#00628B'>Zip Code: </span>");
out.println("<span style='color: #00628B'>");
out.println(curLocation.getZipCode());
out.println("</span>");
out.println("</div>");
// Render city name
out.println("<div>");
out.println("<span style='font-weight:bold;color:
#00628B'>City: </span>");
out.println("<span style='color: #00628B'>");
out.println(curLocation.getCity());
out.println("</span>");
out.println("</div>");
// Render county name
out.println("<div>");
out.println("<span style='font-weight:bold;color:
#00628B'>County: </span>");
out.println("<span style='color: #00628B'>");
out.println(curLocation.getCounty());
out.println("</span>");
out.println("</div>");
// Render state
out.println("<div>");
out.println("<span style='font-weight:bold;color:
#00628B'>State: </span>");
out.println("<span style='color: #00628B'>");
out.println(curLocation.getState());
out.println("</span>");
out.println("</div>");
// Render longitude
out.println("<div>");
out.println("<span style='font-weight:bold;color:
#00628B'>Longitude: </span>");
out.println("<span style='color: #00628B'>");
out.println(curLocation.getLongitude());
out.println("</span>");
out.println("</div>");
// Render latitude
out.println("<div>");
out.println("<span style='font-weight:bold;color:
#00628B'>Latitude: </span>");
out.println("<span style='color: #00628B'>");
out.println(curLocation.getLatitude());
out.println("</span>");
out.println("</div>");
out.println("</div>");
}
%>
|

 |

|
结束语
在本文中,您学习了一些异步通信和 JSP TagLib 控件技巧,包括如何构建复合控件,如何为自定义控件添加客户端事件钩子,以及如何利用 Ajax 使 Web 页面更加动态。本文以本系列前两篇文章描述的 Ajax 通信技巧为基础。Ajax 允许您构建高度动态、用户友好并且可伸缩的 Web 应用程序。通过开发 JSP TagLib 控件,您可以缩短构建业务应用程序所需的时间。
您可以进一步扩展这些控件:
- 向弹出对话框控件添加额外表示选项,比如定位弹出对话框(例如,使其居中或相对另一个控件对它定位)。
- 向弹出对话框和更新面板控件添加动画,比如淡出、滑入等。
- 向弹出对话框和更新面板控件添加加载动画(在等待检索内容时),比如沙漏。
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| 本文的源代码 | ArticleCodeSample.zip | 2560KB | HTTP |
|---|
参考资料 学习
获得产品和技术
关于作者  | 
|  | Brian J. Stewart 目前是 Aqua Data Technologies 的一位首席顾问。他是这家公司的创始人,该公司关注内容管理、XML 技术和企业客户端/服务器与 Web 系统。他架构并开发了基于 J2EE 和 .NET 平台的企业解决方案。 |
对本文的评价
|