UML软件工程组织

 

 

简化 Ajax 和 Java 开发,第 2 部分: 使用约定最小化设置和配置
 
2008-08-07 作者:Andrei Cioroianu 出处:IBM
 
本文内容包括:
为了适应不同的应用程序需求和开发样式,大部分 Web 框架都在尽力变得灵活和可扩展。不幸的是,这有时候会增加复杂性和处理开销,还会产生很大的配置文件。本文将展示如何使用 JSP 标准标记库(JSTL)和 JSP 标记文件实现数据绑定、页面导航和样式约定,从而简化开发和维护。您将了解如何构建带有动态属性的定制 JSP 标记,使快速更改应用程序变得更加容易。此外,本文最后一节包含了一个使用 Ajax 提交 Web 表单的示例。

首先,如果您希望实现约定来最小化配置,则必须控制框架生成的 HTML,并调整 Web 组件使之适用于您的应用程序。已经有一些高度可定制的 Web 框架,比如 JavaServer Faces (JSF),但它们的组件有时不容易定制。例如,如果想要更改 JSF 组件生成的 HTML,通常情况下需要对组件的呈现程序进行重新编码,并实现一个新的定制标记。如果只需在 JSP 文件中更改 HTML,则会简单很多。本文将展示开发人员可以创建基于 JSP 的组件来管理框架。

使用 JSP 标记文件构建 Web 组件

JSP 标记文件是简化 Web 组件开发的理想解决方案,因为它们允许使用 JSP 语法创建定制标记库。此外,标记文件可以像 JSP 页面一样部署,而且不需要标记库描述符(Tag Library Descriptor,TLD),因为它们使用由 JSP 标准定义的命名和设置约定,该标准还提供了在 JSP 标记文件中声明标记属性的指令。

更改时,应用服务器会重新编译并加载一个 JSP 标记文件,而无需重新启动应用程序,这使开发和测试都变得非常简单。JSP 标记文件很快,因为它们受自动生成的 Java™ 类支持,与将 JSP 页面转换为 Servlet 类很相似。

本文将演示如何使用 JSP 标记文件和 JSTL 构建可定制 Web 组件,而无需使用 JSF。主要目标是使更改动态生成 HTML 的代码更加容易,控制处理 HTTP(或 Ajax)请求的方式,并实现简化开发的约定。

本文提供的所有示例都可以组合到一个微型框架中,可以使用它代替 Struts 或 JSF 构建 Web 表单。如果您正开始使用 Java 开发 Web 应用程序,那么您会喜欢上它的简单易用,而且无需学习任何新知识,因为这个框架的标记具有与 HTML 标记相同的名称和属性。

经验丰富的开发人员将会发现,这个框架对必须充分利用 Ajax 和 DHTML 的应用程序很有用。您无需局限于任何应用程序模型,可以更改框架的 250 行 JSP 代码来生成在 Web 浏览器中产生最佳结果的 HTML,也能够以适合应用程序的任何方式自由处理 HTTP 请求。

此外,没有需要管理的特定于框架的配置文件和额外的类。每个页面都可以使用一个普通旧式 Java 对象(plain old Java object,POJO)作为一个数据模型,或者如果能够用 JSP 代码而不是 Java 代码轻松完成数据处理的话,您甚至可以使用一个 Map 对象来代替 JavaBean 实例。

将表单元素绑定到 JavaBean 属性

Web 框架必须提供的一个主要功能是将 UI 组件绑定到数据模型的属性。这意味着当 Web 页面被请求时,框架必须从 JavaBean 对象获取数据并将其包含到 HTML 表单中。用户提交表单时,框架必须获取请求参数并将更新值回存到数据模型中。例如,JSF 框架会让您使用 value 属性指定输入组件的数据绑定(如清单 1 所示):

清单 1. JSF 数据绑定
 
<%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %>
...
<h:inputTextarea value="#{dataModel.address}" rows="3" cols="30"/>

必须在一个 XML 文件中为 JSF 配置数据模型,如清单 2 所示:

清单 2. 数据模型的 JSF 配置
 
<managed-bean>
    <managed-bean-name>dataModel</managed-bean-name>
    <managed-bean-class>formsdemo.AddressBean</managed-bean-class>
    <managed-bean-scope>request</managed-bean-scope>
</managed-bean>

本节演示如何使用 JSTL 和 JSP 标记文件实现相同的输入组件。您可能想知道为什么 JSF 框架中的组件可用时还要这样做。如果将本文中的 textarea.tag 文件与 JSF 框架中实现等价组件的 Java 类的源代码相比较,您就会完全了解 JSP 标记文件带来的好处。然后,请考虑一下您需要在 JSF 框架或另一个第三方库未提供的其他组件上投入多少工作吧。

使用 JSTL 处理 Web 表单

清单 3 展示了一个包含一个 <textarea> 元素的简单 Web 表单,该元素的值是从一个 AddressBean 实例获取的。<jsp:useBean> 标记创建 JavaBean 对象并将其放到 JSP request 作用域中。address 属性的值被包含在带有 JSTL 标记 <c:out> 的 Web 页面中。

清单 3. JSP 页面使用 JSTL 对数据绑定进行编码
 
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<jsp:useBean id="dataModel" scope="request" class="formsdemo.AddressBean"/>

<form method="POST">
    <c:if test="${pageContext.request.method == 'POST' && !empty param.address}">
        <c:set target="${dataModel}" property="address" value="${param.address}"/>
    </c:if>
    <textarea name="address" rows="3" cols="30"
        ><c:out value="${dataModel.address}"/></textarea>
    <br><input type="submit" value="Submit"/>
</form>

当用户单击 Submit 按钮时,Web 浏览器将用户的输入发回给相同的页面,因为 <form> 元素没有 action 属性。然后,应用服务器执行 JSP 页面,该页面使用 JSTL 标记 <c:set> 将 address 参数的值存储到 dataModel bean 中。

构建 <df:textarea> 组件

如果比较清单 1 和清单 3,您将会注意到,JSF 页面仅使用了一行代码来定义文本区域组件,而基于 JSTL 的页面需要 5 行代码来将 <textarea> 元素绑定到 JavaBean 属性。通过将 JSTL 代码移动到输出 <textarea> 元素的可重用标记文件中,就可以 将此代码片段缩减为一行。

textarea.tag 文件(见清单 4)仅声明用于数据绑定约定的 name 属性(表单元素和带有该元素的值的 JavaBean 属性必需具有相同的名称)。任何其他属性(比如 rows 或 cols)都将存储在一个 Map 对象中,该对象的 dynAttr 标识符由 <%@tag%> 指令的 dynamic-attributes 属性来指定。这些属性在一个受 JSTL 标记 <c:forEach> 控制的循环中输出。任何 "、&、< 和 > 字符都被 JSTL 函数 fn:escapeXml() 使用 &quot;、&amp;、&lt; 和 &gt; 替换。

清单 4. textarea.tag 文件
 
<%@ attribute name="name" required="true" rtexprvalue="true" %>
<%@ tag dynamic-attributes="dynAttr" body-content="scriptless" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

<c:if test="${pageContext.request.method == 'POST' && !empty param[name]}">
    <c:set target="${dataModel}" property="${name}" value="${param[name]}"/>
</c:if>

<textarea name="${name}"
    <c:forEach var="attr" items="${dynAttr}">
        ${attr.key}="${fn:escapeXml(attr.value)}"
    </c:forEach>
><c:out value="${dataModel[name]}"/></textarea>

清单 5 中显示的 Web 页面为包含标记文件的 JSP 库声明 df 前缀。然后,该页面使用 <df:textarea> 组件生成 <textarea> 元素,该元素会让用户输入 dataModel 对象的 address 属性的值。

清单 5. 使用标记文件的 JSP 页面
 
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="df" tagdir="/WEB-INF/tags/dynamic/forms" %>

<jsp:useBean id="dataModel" scope="request" class="formsdemo.AddressBean"/>

<form method="POST">
    <df:textarea name="address" rows="3" cols="30"/>
    <br><input type="submit" value="Submit"/>
</form>

使用外观和样式约定

在我以前的文章 “增强 JSF 页面的外观”(参见 参考资料)中,我展示了一种设置标准 JSF 组件的默认属性的技术。我使用了一个定制 JSF 组件来遍历视图树并设置每个组件的 styleClass 属性,这非常适合于呈现单个 HTML 元素的简单 JSF 组件。

生成较大的 HTML 片段(比如一个树或表)的非标准 JSF 组件可能不允许设置任何 HTML 元素的样式。假设 JSF 组件使用 Java 代码生成 HTML,如果要更改 HTML 元素的样式,您的惟一选择就是对 JSF 组件的呈现程序进行重新编码。但是,如果使用 JSP 标记文件和 JSTL,将能够全权访问输出 HTML 的 JSP 代码,这意味着您可以根据应用程序的需要对代码进行更改和调整。

将可重用代码片段移动到独立的 JSP 标记文件中

前一节展示了 <df:textarea> 组件,该组件使用 JSTL 生成与 JavaBean 属性绑定的 <textarea> 元素。如果想要构建额外的 Web 组件,最好把公共的代码片段放到可重用的标记文件中。

输出动态属性的 <c:forEach> 循环可以移动到一个名为 attrList.tag 的独立标记文件中(参见 清单 6)。除了使代码更加紧凑,此更改还允许您实现对所有组件都有用的特性,比如将默认样式类添加到 HTML 标记。

attrList.tag 声明一个名为 tag 的属性,该属性和 skin 参数结合起来构建包含在 class 属性内部的类名称。<c:forEach> 属性输出给定的 map 包含的所有属性,但 class 属性除外,它是在循环之后添加。

清单 6. attrList.tag 文件
 
<%@ attribute name="tag" required="true" rtexprvalue="true" %>
<%@ attribute name="map" required="true" rtexprvalue="true"
    type="java.util.Map" %>
<%@ tag body-content="empty" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

<c:if test="${!empty initParam.skin}">
    <c:set var="classAttr" value="${initParam.skin}_${tag}"/>
</c:if>

<c:forEach var="attr" items="${map}">
    <c:if test="${attr.key != 'class'}">
        ${attr.key}="${fn:escapeXml(attr.value)}"
    </c:if>
    <c:if test="${attr.key == 'class'}">
        <c:set var="classAttr" value="${classAttr} ${attr.value}"/>
    </c:if>
</c:forEach>

<c:if test="${!empty classAttr}">
    class="${fn:escapeXml(classAttr)}"
</c:if>

skin 参数在 Web 应用程序的 web.xml 文件中指定(参见清单 7):

清单 7. 在 web.xml 中配置外观参数
 
<web-app ...>
    <context-param>
        <param-name>skin</param-name>
        <param-value>default</param-value>
    </context-param>
</web-app>

更新 <df:textarea> 组件

清单 8 展示了更改后的 textarea.tag 文件,它使用 <dfu:attrList> 输出 <textarea> 元素的动态属性:

清单 8. textarea.tag 的更新版本
 
<%@ attribute name="name" required="true" rtexprvalue="true" %>
<%@ tag dynamic-attributes="dynAttr" body-content="scriptless" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="dfu" tagdir="/WEB-INF/tags/dynamic/forms/util" %>

<c:if test="${pageContext.request.method == 'POST' && !empty param[name]}">
    <c:set target="${dataModel}" property="${name}" value="${param[name]}"/>
</c:if>

<textarea name="${name}" <dfu:attrList tag="textarea" map="${dynAttr}"/>
><c:out value="${dataModel[name]}"/></textarea>

样式规则可以在 CSS 文件中定义。例如,如果想要更改 <df:textarea> 产生的每个 <textarea> 元素的边框,只需编写一个像清单 9 中那样的样式规则:

清单 9. 定义 <textarea> 元素的默认样式
 
.default_textarea
    { border-color: #A0A0A0; border-style: solid; border-width: thin; }

编写表单处理和页面导航规则

JSF 框架有一个复杂的请求处理生命周期,它可以划分为 6 个不同的阶段。很少有开发人员能够完全理解它并进行定制。对于高流量的 Ajax 应用程序,需要使用一种更简单的 HTTP 请求处理机制,以避免过高的 CPU 开销。本节将展示如何借助 JSTL 和 JSP 标记文件轻松实现这一点。

创建 <df:form> 组件

form.tag 文件(如清单 10 所示)生成一个 <form> 元素,并使用 JSTL 设置嵌套标记访问的变量,在本文的后续内容中将会看到。无需像之前的 JSP 示例一样使用 dataModel 对象,标记文件拥有一个 model 属性,该属性像 JSP 标记文件的其他任何属性一样被放置到 page 作用域中。

form.tag 文件使用 JSTL 标记 <c:set> 将它的一些属性复制到 request 作用域中,以便其他标记文件可以使用 formName、formModel 和 formAction 变量获取 name、model 和 action 属性的值。此外,formPost 变量指示 HTTP 方法是否为 POST,<jsp:useBean> 是否创建一个 HashMap 实例来存储任何处理错误。

清单 10. form.tag 文件
 
<%@ attribute name="name" required="true" rtexprvalue="true" %>
<%@ attribute name="action" required="false" rtexprvalue="true" %>
<%@ attribute name="method" required="false" rtexprvalue="true" %>
<%@ attribute name="model" required="true" rtexprvalue="true"
    type="java.lang.Object"%>
<%@ tag dynamic-attributes="dynAttr" body-content="scriptless" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="dfu" tagdir="/WEB-INF/tags/dynamic/forms/util" %>

<c:set var="formName" scope="request" value="${name}"/>
<c:set var="formModel" scope="request" value="${model}"/>
<c:if test="${!empty action}">
    <c:set var="formAction" scope="request" value="${action}"/>
</c:if>
<c:set var="formPost" scope="request"
    value="${fn:toUpperCase(pageContext.request.method) == 'POST'}"/>
<jsp:useBean id="formErrors" scope="request" class="java.util.HashMap"/>

<form name="${name}" method="POST" <dfu:attrList tag="form" map="${dynAttr}"/>>
    <jsp:doBody/>
</form>

<c:if test="${formPost && empty formErrors && !empty formAction}">
    <c:set var="forwardURL" value="${formAction}"/>
</c:if>

<c:remove var="formName" scope="request"/>
<c:remove var="formModel" scope="request"/>
<c:remove var="formAction" scope="request"/>
<c:remove var="formPost" scope="request"/>
<c:remove var="formErrors" scope="request"/>

<c:if test="${!empty forwardURL}">
    <jsp:forward page="${forwardURL}"/>
</c:if>

<form> 元素将拥有与 <df:form> 标记相同的 HTML 属性,但 method(始终为 POST)和 action(省略以便将数据发送回相同的页面)除外。<form> 元素也会包含 <jsp:doBody> 生成的输出,该元素执行 <df:form> 与 </df:form> 之间的 JSP 代码(参见清单 11)。如果 HTTP 方法为 POST 并且没有出现错误,form.tag 文件将请求转发到其 URL 在 action 属性中指定的页面,以供进一步处理。在这之前,标记文件使用 JSTL 标记 <c:remove> 将其变量从 request 作用域删除。

清单 11. 在 JSP 页面中使用 <df:form>
 
<df:form name="..." model="${...}" action="...">
    ...
    <df:textarea .../>
    ...
</df:form>

处理用户错误和应用程序异常

之前展示的 textarea.tag 文件不处理在设置与表单元素绑定的 JavaBean 属性时可能发生的任何异常。这个问题可以使用 setProp.tag 文件解决(如清单 12 所示),如果出现转换错误(比如 NumberFormatException),该文件会使用 JSTL 标记 <c:catch> 捕捉 JavaBean 属性的设置方法或者甚至是 <c:set> 标记可能抛出的任何异常:

清单 12. setProp.tag 文件
 
<%@ attribute name="name" required="true" rtexprvalue="true" %>
<%@ attribute name="array" required="false" rtexprvalue="true"
    type="java.lang.Boolean" %>
<%@ attribute name="bool" required="false" rtexprvalue="true"
    type="java.lang.Boolean" %>
<%@ tag body-content="empty" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="dfu" tagdir="/WEB-INF/tags/dynamic/forms/util" %>

<c:set var="propValue" value="${null}"/>
<c:if test="${formPost && empty formErrors[name]}">
    <c:if test="${!array && !bool && !empty param[name]}">
        <c:set var="propValue" value="${param[name]}"/>
    </c:if>
    <c:if test="${!array && bool}">
        <c:set var="propValue" value="${!empty param[name]}"/>
    </c:if>
    <c:if test="${array && fn:length(paramValues[name]) > 0}">
        <c:set var="propValue" value="${paramValues[name]}"/>
    </c:if>
</c:if>

<c:if test="${propValue != null}">
    <c:catch var="exception">
        <c:set target="${formModel}" property="${name}" value="${propValue}"/>
    </c:catch>
    <c:if test="${exception != null}">
        <dfu:addError name="${name}" msg="${exception.message}"
            exception="${exception}"/>
    </c:if>
</c:if>

setProp.tag 文件用于设置拥有 <c:set> 接受的类型(比如 String、int、float 和 boolean)的属性和索引属性,后者可能是 String 数组。如果发生了类型转换错误或者抛出异常,addError.tag 文件(参见清单 13)就会将错误消息放入 formErrors 映射中:

清单 13. addError.tag 文件
 
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<%@ attribute name="name" required="true" rtexprvalue="true" %>
<%@ attribute name="msg" required="true" rtexprvalue="true" %>
<%@ attribute name="exception" required="false" rtexprvalue="true"
    type="java.lang.Throwable" %>

<c:if test="${!empty formErrors[name]}">
    <c:set var="msg" value="${formErrors[name]}; ${msg}"/>
</c:if>
<c:set target="${formErrors}" property="${name}" value="${msg}"/>

<c:if test="${exception != null}">
    <% ((Throwable) jspContext.getAttribute("exception")).printStackTrace(); %>
</c:if>

textarea.tag 文件的最终版本(如清单 14 所示)使用 <dfu:setProp> 获取 JavaBean 属性。此外,标记文件会让您在 JSP 页面中 <df:textarea> 与 </df:textarea> 之间指定一个默认值。如果 bean 属性的值为空,则标记文件使用 <jsp:doBody> 从 JSP 页面获取定制标记的内容。

清单 14. textarea.tag 的最终版本
 
<%@ attribute name="name" required="true" rtexprvalue="true" %>
<%@ tag dynamic-attributes="dynAttr" body-content="scriptless" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="dfu" tagdir="/WEB-INF/tags/dynamic/forms/util" %>

<dfu:setProp name="${name}"/>

<c:set var="textareaValue" value="${formModel[name]}"/>
<c:if test="${empty textareaValue}">
    <jsp:doBody var="textareaValue"/>
</c:if>

<textarea name="${name}" <dfu:attrList tag="textarea" map="${dynAttr}"/>
><c:out value="${textareaValue}"/></textarea>

开发更多的 UI 组件

迄今为止,您已经看到了如何创建自己的文本区域和表单组件,如何处理 HTTP 请求。本节将会展示一些附加组件,比如列表、输入字段、复选框、单选和提交按钮,您可以使用它们构建功能全面的 Web 表单。

创建 <df:select> 和 <df:option> 组件

在 JSP 页面中,<df:option> 标记将会嵌入到 <df:select> 中,就像在 HTML 的 <option> 和 <select> 元素中一样。因此,select.tag 文件(参见清单 15)将其 name 和 multiple 属性复制到 request 作用域中,从而可以在 option.tag 中访问它们。然后,select.tag 文件使用 <dfu:setProp> 设置与表单元素同名的模型属性。如果 multiple 属性为 true,则该属性的类型应该为一个数组。

清单 15. select.tag 文件
 
<%@ attribute name="name" required="true" rtexprvalue="true" %>
<%@ attribute name="multiple" required="false" rtexprvalue="true"
    type="java.lang.Boolean" %>
<%@ tag dynamic-attributes="dynAttr" body-content="scriptless" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="dfu" tagdir="/WEB-INF/tags/dynamic/forms/util" %>

<c:set var="selectName" scope="request" value="${name}"/>
<c:set var="selectMultiple" scope="request" value="${multiple}"/>

<dfu:setProp name="${name}" array="${multiple}"/>

<select name="${name}"
    <c:if test="${multiple}">
        multiple
    </c:if>
    <dfu:attrList tag="select" map="${dynAttr}"/>
>
    <jsp:doBody/>
</select>

<c:remove var="selectName" scope="request"/>
<c:remove var="selectMultiple" scope="request"/>

在 JSP 页面中,<df:select> 标记可以拥有动态属性,比如 id 或 class,这些元素通过 <dfu:attrList> 传递到输出的 <select> 元素。JSP 标记 <jsp:doBody> 执行 <df:select> 的内容,该内容通常由几个 <df:option> 标记组成。

在生成 <option> 元素之前,option.tag 文件(如清单 16 所示)设置 optionLabel 和 optionValue 变量。只有当选项的值与模型(该模型具有与 <select> 元素相同的名称)的值相等时,<dfu:isSelected> 标记(用于 option.tag 中)才会输出由 selected 字符串组成的标记正文。因此,数据模型的属性决定了由 <df:select> 和 <df:option> 产生的列表的初始选择。

清单 16. option.tag 文件
 
<%@ attribute name="value" required="false" rtexprvalue="true" %>
<%@ tag dynamic-attributes="dynAttr" body-content="scriptless" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="dfu" tagdir="/WEB-INF/tags/dynamic/forms/util" %>

<jsp:doBody var="optionLabel"/>

<c:set var="optionValue" value="${value}"/>
<c:if test="${empty optionValue}">
    <c:set var="optionValue" value="${optionLabel}"/>
</c:if>

<option
    <c:if test="${!empty value}">
        value="${fn:escapeXml(value)}"
    </c:if>
    <dfu:isSelected name="${selectName}" value="${optionValue}"
        array="${selectMultiple}">
        selected
    </dfu:isSelected>
    <dfu:attrList tag="option" map="${dynAttr}"/>
><c:out value="${optionLabel}"/></option>

清单 17 展示了 isSelected.tag 文件,该文件从 option.tag 调用。如果 array 属性为 false,则 isSelected.tag 文件将 value 属性与 formModel[name] 进行比较。如果 array 属性为 true,则要求 formModel[name] 是一个数组,并且 isSelected.tag 将每个元素与 value 属性进行比较。

清单 17. isSelected.tag 文件
 
<%@ attribute name="name" required="true" rtexprvalue="true" %>
<%@ attribute name="value" required="true" rtexprvalue="true" %>
<%@ attribute name="array" required="false" rtexprvalue="true"
    type="java.lang.Boolean" %>
<%@ tag body-content="scriptless" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

<c:set var="selected" value="false"/>

<c:if test="${!array}">
    <c:set var="selectedValue" value="${formModel[name]}"/>
    <c:if test="${!empty selectedValue}">
        <c:if test="${selectedValue == value}">
            <c:set var="selected" value="${true}"/>
        </c:if>
    </c:if>
</c:if>

<c:if test="${array}">
    <c:set var="selectedValues" value="${formModel[name]}"/>
    <c:if test="${fn:length(selectedValues) > 0}">
        <c:forEach var="selectedValue" items="${selectedValues}">
            <c:if test="${selectedValue == value}">
                <c:set var="selected" value="${true}"/>
            </c:if>
        </c:forEach>
    </c:if>
</c:if>

<c:if test="${selected}">
    <jsp:doBody/>
</c:if>

isSelected.tag 的 <jsp:doBody> 标记执行 option.tag 中位于 <dfu:isSelected> 和 </dfu:isSelected> 之间的 JSP 代码,在本例中主要输出 selected 字符串。<dfu:isSelected> 组件也用于 input.tag 中,用于验证是否应该选择一个单选按钮。

构建 <df:input> 组件

input.tag 文件(参见清单 18)输出一个 <input> 元素。根据 type 属性的值的不同,标记文件执行不同的 JSP 代码片段,并设置与 HTML 元素同名的模型属性。

如果 type 为 text、password 或 hidden,当 HTTP 方法是 PSOT 时,<dfu:setProp> 标记将请求参数存储到 JavaBean 对象中。接下来,无论 HTTP 方法是 GET 还是 POST,都使用 formModel[name] 获取 bean 属性的值并将其存储到 inputValue 变量中。

对于 radio 按钮,用之前展示的 <dfu:isSelected> 标记比较 value 属性和与单选按钮同名的模型属性的值。如果这两个值一致,则 inputChecked 变量为 true。如果 type 属性为 checkbox,则要求 bean 属性的类型为 boolean。

清单 18. input.tag 文件
 
<%@ attribute name="name" required="true" rtexprvalue="true" %>
<%@ attribute name="type" required="true" rtexprvalue="true" %>
<%@ attribute name="value" required="false" rtexprvalue="true" %>
<%@ attribute name="action" required="false" rtexprvalue="true" %>
<%@ tag dynamic-attributes="dynAttr" body-content="empty" %>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="dfu" tagdir="/WEB-INF/tags/dynamic/forms/util" %>

<c:set var="inputType" value="${fn:toLowerCase(type)}"/>
<c:set var="inputValue" value="${value}"/>
<c:set var="inputChecked" value="${false}"/>

<c:choose>
    <c:when test="${inputType=='text' || inputType=='password' || inputType=='hidden'}">
        <dfu:setProp name="${name}"/>
        <c:set var="inputValue" value="${formModel[name]}"/>
    </c:when>
    <c:when test="${inputType == 'radio'}">
        <dfu:setProp name="${name}"/>
        <dfu:isSelected name="${name}" value="${value}">
            <c:set var="inputChecked" value="${true}"/>
        </dfu:isSelected>
    </c:when>
    <c:when test="${inputType == 'checkbox'}">
        <dfu:setProp name="${name}" bool="true"/>
        <c:if test="${formModel[name]}">
            <c:set var="inputChecked" value="${true}"/>
        </c:if>
    </c:when>
    <c:when test="${inputType=='submit'}">
        <c:if test="${formPost && !empty param[name] && !empty action}">
            <c:set var="formAction" scope="request" value="${action}"/>
        </c:if>
    </c:when>
</c:choose>

<input name="${name}" type="${type}"
    <c:if test="${!empty inputValue}">
        value="${fn:escapeXml(inputValue)}"
    </c:if>
    <c:if test="${inputChecked}">
        checked
    </c:if>
    <dfu:attrList tag="input_${type}" map="${dynAttr}"/>
>

对于 submit 按钮,input.tag 文件接受 action 属性,该属性用于指定一个页面的 URL。当单击 submit 按钮时,该 URL 应该替换表单的操作。因此,一个表单可以有多个提交按钮,每个按钮可以将请求发送到不同页面,以生成 HTML 响应。这与在配置文件中指定导航规则(如在 JSF 框架下)相比要简单得多。

使用 Ajax 提交 Web 表单

在我的另一篇文章 “使用 XMLHttpRequest 提交 JSF 表单”(参见 参考资料)中,我展示了如何使用 Ajax 发送 Web 表单的数据。这篇文章还包含一个名为 SupportForm.jsp 的 JSF 示例。此处提供了一个类似的例子,便于您将 JSF 文章中的表单和基于本文 JSP 标记文件的 Web 表单进行比较。

创建 JavaBeans 或 Java Maps 支持的表单

SupportBean 类(见清单 19)是一个简单的 bean,包含一些和示例 Web 表单的元素绑定的属性:

清单 19. SupportBean 类
 
package formsdemo;

public class SupportBean implements java.io.Serializable {
    private String name;
    private String email;
    private String versions[];
    private String platform;
    private String browser;
    private boolean crash;
    private String problem;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    ...
}

清单 20 展示了 SupportForm.jsp 页面,该页面使用本文前一节提供的标记文件库。Web 组件拥有与它们生成的 HTML 元素相同的属性。<df:form> 标记拥有额外的 model 属性,该属性指定 JavaBean 对象(提交的表单数据将存储在这里)。

清单 20. SupportForm.jsp 示例
 
                
<%@ taglib prefix="df" tagdir="/WEB-INF/tags/dynamic/forms" %>

<jsp:useBean id="supportBean" scope="request" class="formsdemo.SupportBean"/>

<html>
<head>
    <title>Support Form</title>
    <link rel="stylesheet" href="forms.css" type="text/css">
</head>
<body>

    <h1>Support Form</h1>

    <df:form name="supportForm" model="${supportBean}"
            action="SupportConfirm.jsp">

        <p>Name: <br>
        <df:input name="name" type="text" size="40"/>

        <p>Email: <br>
        <df:input name="email" type="text" size="40"/>

        <p>Versions: <br>
        <df:select name="versions" multiple="true" size="5">
            <df:option>2.0.1</df:option>
            <df:option>2.0.0</df:option>
            <df:option>1.1.0</df:option>
            <df:option>1.0.1</df:option>
            <df:option>1.0.0</df:option>
        </df:select>

        <p>Platform: <br>
        <df:input name="platform" type="radio" value="Windows"/> Windows <br>
        <df:input name="platform" type="radio" value="Linux"/> Linux <br>
        <df:input name="platform" type="radio" value="Mac"/> Mac <br>

        <p>Browser: <br>
        <df:select name="browser" size="1">
            <df:option value=""></df:option>
            <df:option value="IE">IE</df:option>
            <df:option value="Firefox">Firefox</df:option>
            <df:option value="Netscape">Netscape</df:option>
            <df:option value="Mozilla">Mozilla</df:option>
            <df:option value="Opera">Opera</df:option>
            <df:option value="Safari">Safari</df:option>
        </df:select>

        <p><df:input name="crash" type="checkbox"/> Causes browser crash

        <p>Problem: <br>
        <df:textarea name="problem" rows="10" cols="40"/>

        <p><df:input name="submit" type="submit" value="Submit"/>

    </df:form>

</body>
</html>

SupportForm.jsp 中使用的 <df:form> 标记将 HTTP 请求转发到 SupportConfirm.jsp 页面(如清单 21 所示),该页面的 URL 在 action 属性中指定。该确认页面输出数据模型的属性。

清单 21. SupportConfirm.jsp 页面
 
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<html>
<head>
    <title>Support Confirmation</title>
</head>
<body>

    <h1>Support Confirmation</h1>

    <p>Your information was received.

    <p>Name: <c:out value="${supportBean.name}"/><br>
    <p>Email: <c:out value="${supportBean.email}"/><br>
    <p>Versions: <c:forEach var="version" items="${supportBean.versions}">
        <c:out value="${version}"/></c:forEach><br>
    <p>Platform: <c:out value="${supportBean.platform}"/><br>
    <p>Browser: <c:out value="${supportBean.browser}"/><br>
    <p>Causes browser crash: <c:out value="${supportBean.crash}"/>
    <p>Problem: <c:out value="${supportBean.problem}"/><br>

</body>
</html>

JSP EL 和 JSTL 标记(比如 <c:set>)访问 JavaBean 的属性和 Map 实例的元素时使用相同的语法。因此,可以使用一个 java.util.HashMap 来取代前面的例子中的 formsdemo.SupportBean 对象。惟一需要更改的是 <jsp:useBean> 标记中的类名称(参见清单 22):

清单 22. SupportForm2.jsp 示例
 
<jsp:useBean id="supportBean" scope="request" class="java.util.HashMap"/>
...
<df:form name="supportForm" model="${supportBean}" action="SupportConfirm.jsp">
    ...
</df:form>

将 Ajax 代码添加到 Web 表单

“使用 XMLHttpRequest 提交 JSF 表单”(参见 参考资料)完整地描述了 AutoSaveScript.js 文件(可以在源代码归档文件中找到)。JavaScript 文件包含本文重用的一些函数。getFormData() 函数(如清单 23 所示)接受一个 form 对象并返回编码在一个字符串中的表单数据:

清单 23. AutoSaveScript.js 的 getFormData() 函数
 
function getFormData(form) {
    var dataString = "";

    function addParam(name, value) {
        dataString += (dataString.length > 0 ? "&" : "")
            + escape(name).replace(/\+/g, "%2B") + "="
            + escape(value ? value : "").replace(/\+/g, "%2B");
    }

    var elemArray = form.elements;
    for (var i = 0; i < elemArray.length; i++) {
        var element = elemArray[i];
        var elemType = element.type.toUpperCase();
        var elemName = element.name;
        if (elemName) {
            if (elemType == "TEXT"
                    || elemType == "TEXTAREA"
                    || elemType == "PASSWORD"
                    || elemType == "HIDDEN")
                addParam(elemName, element.value);
            else if (elemType == "CHECKBOX" && element.checked)
                addParam(elemName, element.value ? element.value : "On");
            else if (elemType == "RADIO" && element.checked)
                addParam(elemName, element.value);
            else if (elemType.indexOf("SELECT") != -1)
                for (var j = 0; j < element.options.length; j++) {
                    var option = element.options[j];
                    if (option.selected)
                        addParam(elemName,
                            option.value ? option.value : option.text);
                }
        }
    }
    return dataString;
}

AutoSaveScript.js 文件的 submitFormData() 函数(参见清单 24)有两个参数:一个 form 对象和一个 callback 函数。前者的数据被 Ajax 发送到服务器,后者被调用来处理 Ajax 响应:

清单 24. AutoSaveScript.js 的 submitFormData() 函数
 
function submitFormData(form, callback) {
    var xhr;
    if (window.ActiveXObject)
        xhr = new ActiveXObject("Microsoft.XMLHTTP");
    else if (window.XMLHttpRequest)
        xhr = new XMLHttpRequest();
    else
        return null;
        
    var method = form.method ? form.method.toUpperCase() : "GET";
    var action = form.action ? form.action : document.URL;
    var data = getFormData(form);

    var url = action;
    if (data && method == "GET")
        url += "?" + data;
    xhr.open(method, url, true);
    
    function submitCallback() {
        if (callback && xhr.readyState == 4 && xhr.status == 200)
            callback(xhr);
        ...
    }
    xhr.onreadystatechange = submitCallback;

    xhr.setRequestHeader("Ajax-Request", "Auto-Save");
    if (method == "POST") {
        xhr.setRequestHeader("Content-Type",
            "application/x-www-form-urlencoded");
        xhr.send(data);
    } else
        xhr.send(null);
    
    return xhr;
}

清单 25 展示了 submitAllForms() 函数,它发送当前 Web 页面包含的所有表单数据。此外,此函数从内存中删除以前的 XMLHttpRequest 对象,以防止 Web 页面中的内存泄漏。

清单 25. AutoSaveScript.js 的 submitAllForms() 函数
 
var autoSaveXHR = new Array();

function submitAllForms(callback) {
    var formArray = document.forms;
    for (var i = 0; i < formArray.length; i++) {
        if (autoSaveXHR[i]) {
            var oldXHR = autoSaveXHR[i];
            oldXHR.onreadystatechange = function() { };
            oldXHR.abort();
            delete oldXHR;
        }
        autoSaveXHR[i] = submitFormData(formArray[i], callback);
    }
}

SupportForm3.jsp 页面(如清单 26 所示)是前面提供的 Web 表单的一个修改版本。此 JSP 页面将 AutoSaveScript.js 导入其页眉并定义一个回调函数,该函数在一个独立窗口中显示 Ajax 响应。该表单还包含第二个标记为 Submit with Ajax 的按钮,这个按钮调用 submitSupportForm() 函数返回 false,因此该表单不会被 Web 浏览器重新提交。

清单 26. SupportForm3.jsp 示例
 
<%@ taglib prefix="df" tagdir="/WEB-INF/tags/dynamic/forms" %>

<jsp:useBean id="supportBean" scope="request" class="formsdemo.SupportBean"/>

<html>
<head>
    ...
    <script type="text/javascript" src="AutoSaveScript.js">
    </script>
    <script type="text/javascript">
        function ajaxCallback(xhr) {
            confirmWindow=window.open("", "_blank",
                "menubar=no, resizable=yes, scrollbars=yes, width=600, height=600");
            confirmWindow.document.write(xhr.responseText);
            confirmWindow.document.close();
        }

        function submitSupportForm() {
            submitAllForms(ajaxCallback);
            return false;
        }
    </script>
</head>
<body>
    ...
    <df:form name="supportForm" model="${supportBean}"
            action="SupportConfirm.jsp">
        ...
        <df:input name="ajaxSubmit" type="submit" value="Submit with Ajax"
            onclick="return submitSupportForm()"/>
    </df:form>
</body>
</html>

结束语

在本文中,学习了如何构建使用约定简化 Web 开发的轻量型组件。只需不到 10K 的 JSP 代码就可以实现一组标记文件,这些文件生成基本的表单元素:列表、文本字段、单选按钮、复选框和提交按钮。这些组件功能丰富,但缺少一些基本功能,比如数据验证和错误报告。请随时关注本系列的下一篇文章,它将展示如何创建 JSP 标记文件来在服务器端执行验证,以及在客户端生成执行验证的 JavaScript 代码。

下载

描述 名字 大小 下载方法
本文的示例应用程序
wa-aj-simplejava2.zip
16KB

参考资料

  • 参与论坛讨论
  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文
  • 增强 JSF 页面的外观”(Andrei Cioroianu,developerWorks,2008 年 1 月)展示了如何实现标准 JSF 组件的默认样式。这篇文章是 “联合使用 CSS、JavaScript 和 JSF 精心打造 Ajax 应用程序” 系列的第一部分。
  • 使用 XMLHttpRequest 提交 JSF 表单”(Andrei Cioroianu,developerWorks,2007 年 8 月)包含 SupportForm.jsp 示例的基于 JSF 的版本,并且详细描述了 AutoSaveScript.js 文件的 JavaScript 函数。这篇文章是 “借助 Ajax 自动保存 JSF 表单” 系列的第一部分。
  • developerWorks Web 开发专区 包含大量的 Web 2.0 开发工具和信息。
  • developerWorks Ajax 资源中心 包含不断增加的大量 Ajax 内容以及有用资源,可以让您立即开始开发 Ajax 应用程序。
  • 访问 JSP Technology 主页 获得有关 JavaServer Pages 的更多信息。
 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号