分享

借助 Ajax 自动保存 JSF 表单: 第 3 部分

 风之飞雪 2014-03-19

系列简介

本系列 的三篇文章演示了一个独立的 Web 应用程序,每篇文章都对其添加一些增强。本节将对这个示例应用程序进行简要回顾。

第 1 部分 首先给出一个典型的 JSF 表单 SupportForm.jsp 并提供一组可重用的 JavaScript 功能,可以通过 Ajax 获取、编码和提交表单数据,从而使您能够自动、定期并透明地保存用户输入。您可以从示例应用程序的 AutoSaveScript.js 文件获得 JavaScript 函数的源代码。第 1 部分还解释了如何构建一个 JSF 阶段侦听器 AutoSaveListener 来处理 Ajax 请求。第 2 部分对侦听器类进行了修改。

第 2 部分 中,您了解了如何将当前 JSF 视图数据保存到数据存储库中。DataMapRepository 类是一个包含数据地图的 Map。每个数据地图保存单个表单实例的用户输入。对数据存储库的所有访问都是通过一个名为 RepositoryWrapper 的线程安全类实现的。servlet 上下文侦听器名为 DataMapPersistence,用于在停机之前将存储库序列化到文件中,并在启动后恢复存储库状态。第 2 部分还提供了一个 servlet 过滤器 BrowserIdFilter,用于识别跨浏览器会话的匿名用户。

developerWorks Ajax 资源中心

请访问 Ajax 资源中心,这里几乎囊括了关于 Ajax 编程模型的所有信息,包括各种文章、教程、论坛、博客、wikis、活动和新闻。

本文(本系列的第三篇也是最后一篇)将对 SupportForm.jsp 页面进行修改,以便在用户关闭和重新打开浏览器后能够重新恢复页面数据。ViewRestorer 类提供 JSF 侦听器方法来处理恢复请求。如果您对 JSF 请求处理生命周期并不熟悉,那么就不容易理解第 3 部分中的示例代码。第 1 部分 对这种 JSF 机制进行了简要的概述,并且本系列的所有文章尽可能详细地描述了所使用的 JSF 特性。您也可通过 JSF 规范获得对请求处理生命周期的完整说明。

为恢复请求构建处理程序

本节将提供用于恢复 JSF 表单数据的 Java 方法。这些方法将与一些 JavaScript 功能和 JSF 组件相结合,本文稍后会介绍这些内容。

恢复当前视图的数据

第 2 部分 展示了如何通过遍历 JSF 组件树并获取输入组件的值来保存当前视图的表单数据。这个操作是在 DataMapRepository 类的 saveValues() 方法中执行的,这个类还包含了一个可以恢复输入组件值的方法。对于实现 EditableValueHolder 界面的每个组件,restoreValues() 方法(如清单 1 所示)将从提供的数据地图中获得之前保存的值,设置 JSF 组件的 value 属性,并清除 submittedValue 属性:

清单 1. 恢复输入组件的值
package autosave;
...
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
...
public class DataMapRepository ... {
    ...
    public static void restoreValues(UIComponent comp,
            Map<String, Object> dataMap) {
        if (comp == null || dataMap == null)
            return;
        if (comp instanceof EditableValueHolder) {
            // Input component. Get its value from the data map
            // and clear any submitted value
            EditableValueHolder evh = (EditableValueHolder) comp;
            evh.setValue(dataMap.get(comp.getId()));
            evh.setSubmittedValue(null);
        }
        // Iterate over the children of the current component
        Iterator children = comp.getChildren().iterator();
        while (children.hasNext()) {
            UIComponent child = (UIComponent) children.next();
            // Recursive call
            restoreValues(child, dataMap);
        }
    }

}

第 2 部分 还演示了 AutoSaveListener 类的 saveCurrentView() 方法,它用于处理表单自动保存请求。本节将讨论示例应用程序的 ViewRestorer 类,该类提供了一个 restoreCurrentView() 方法和两个 JSF 事件侦听器,可处理恢复当前 JSF 视图数据的请求,简称为恢复请求

清单 2 展示了 restoreCurrentView() 方法,它通过包装器 bean 访问存储库,以获取当前用户/视图组合的数据地图。然后,它将调用 restoreValues() 方法恢复输入组件的值。之后,restoreCurrentView() 调用 faces 上下文的 renderResponse() 方法,通知 JSF 框架实现 Render Response 阶段。在后文中您将看到,renderResponse() 调用将与 immediate 属性结合使用。

清单 2. 恢复当前 JSF 视图的数据
package autosave;

import java.util.Map;
...
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;

public class ViewRestorer implements java.io.Serializable {

    public void restoreCurrentView() {
        // Get the faces context of the current request.
        FacesContext ctx = FacesContext.getCurrentInstance();
        // Get the root component of the current view.
        UIViewRoot root = ctx.getViewRoot();
        // Get the managed bean instance wrapping the data repository.
        RepositoryWrapper wrapper = RepositoryWrapper.getManagedBean(ctx);
        // Get the data map for the current context.
        Map<String, Object> dataMap = wrapper.getDataMap(ctx);
        // Use the data map to restore the values of the JSF components.
        DataMapRepository.restoreValues(root, dataMap);
        // Signal the JSF framework to go to the Render Response phase.
        ctx.renderResponse();
    }
    ...
}

实现 JSF 侦听程序

ViewRestoreractionListener()valueChangeListener() 方法(如 清单 3 所示)可在 JSF 组件的 actionListenervalueChangeListener 属性内使用,可触发对当前视图进行恢复。下一节将演示如何使用这些侦听器方法。

清单 3. 可以恢复事件的 JSF 侦听器
package autosave;
...
import javax.faces.event.ActionEvent;
import javax.faces.event.ValueChangeEvent;

public class ViewRestorer implements java.io.Serializable {
    ...    
    public void actionListener(ActionEvent e) {
        restoreCurrentView();
    }
    
    public void valueChangeListener(ValueChangeEvent e) {
        restoreCurrentView();
    }
    ...
}

如果存储库包含当前视图和当前用户的数据地图,isCurrentViewRestorable() 方法将返回 true(如 清单 4 所示):

清单 4 . 检验是否可以恢复当前视图的数据
package autosave;
...
public class ViewRestorer implements java.io.Serializable {
    ...    
    public boolean isCurrentViewRestorable() {
        FacesContext ctx = FacesContext.getCurrentInstance();
        RepositoryWrapper wrapper = RepositoryWrapper.getManagedBean(ctx);
        return wrapper.hasDataMap(ctx);
    }
    
}

清单 5 演示如何将 ViewRestorer 类配置为 faces-config.xml 中的托管 bean,以便使 SupportForm.jsp 页面的 JSF 组件可以使用侦听器方法:

清单 5. 配置视图恢复程序
<faces-config>
    ...
    <managed-bean>
        <managed-bean-name>viewRestorer</managed-bean-name>
        <managed-bean-class>autosave.ViewRestorer</managed-bean-class>
        <managed-bean-scope>request</managed-bean-scope>
    </managed-bean>
    ...
</faces-config>

发送恢复请求

SupportForm.jsp 页面使用 AutoSaveScript.js 文件的 setAutoSaving() 函数激活表单自动保存功能,该文件在 第 1 部分 中提到过。本节将演示如何修改 JSF 页面从而保存当前视图的数据。

对恢复请求使用隐藏的触发器

ViewRestorer bean 的 valueChangeListener() 方法触发 ValueChangeEvent 的最简单的方法就是向 SupportForm.jsp 页面添加一个隐藏的元素。清单 6 演示了必须添加到 JSF 页面的代码:

清单 6. 对恢复请求使用隐藏的触发器
<h:form id="supportForm">
    <h:inputHidden id="restoreTrigger" value="default"
            valueChangeListener="#{viewRestorer.valueChangeListener}"
            immediate="true"/>
    ...
</h:form>

您可能想知道我们为什么使用隐藏的组件触发表单恢复。为什么不在呈现时直接恢复表单?要理解这些问题的答案,您必须了解 JSF 框架构建组件树的方式,这些组件树的值必须进行恢复。请记住,用户曾经离开了应用程序(或者浏览器崩溃),现在用户使用一个 GET 请求返回到表单页面。

在上面描述的场景中处理 GET 请求时,JSF 框架很可能不能在请求处理生命周期的第 1 阶段(Restore View)恢复 JSF 组件树。即使 JSF 实现可以这样做,请求将不具备参数,因为用户刚刚返回到应用程序中。

根据 JSF 规范,如果视图不能被恢复或者请求没有包含查询参数或 POST 数据,JSF 实现必须在 Restore View 阶段调用 faces 上下文的 renderResponse() 方法。因此,请求处理将跳至最后一个阶段(Render Response),该阶段将生成 HTML 输出并同时构建组件树,因为之前没有恢复树。这意味着应用程序在处理 GET 请求的过程中没有机会处理组件树,而且输出已经被发送给用户浏览器。

发送 POST 请求以恢复 JSF 表单

上述问题的解决方法就是让 JSF 框架处理 GET 请求并随后发送新的 POST 请求,这将允许应用程序处理 JSF 组件树,恢复组件值。POST 请求可通过表单对象的 JavaScript submit() 方法发送。这一次不能使用 XMLHttpRequest,因为您需要在发出 POST 请求后刷新页面。

如果查看表单页面的 HTML 输出,您将注意到 JSF 为隐藏的元素生成了 supportForm:restoreTrigger ID。因此,您需要使用像 getFormElement() 这样的 JavaScript 函数(如 清单 7 所示)在 Web 页面中查找表单元素对象:

清单 7. 查找使用 JSF 组件呈现的表单元素
function getFormElement(formId, elemId) {
    return document.getElementById(formId + ":" + elemId);
}

清单 8 展示了另一个 JavaScript 函数,它将发送恢复表单数据的请求。在调用 supportForm 对象的 submit() 方法之前,submitRestoreRequest() 函数将修改隐藏元素的值,以在服务器端处理请求时触发 ViewRestorer bean 的 valueChangeListener() 方法。您可以在 SupportForm.jsp 文件中找到 submitRestoreRequest() 函数。

清单 8. 提交请求以恢复表单数据
function submitRestoreRequest() {
    var restoreTrigger
        = getFormElement("supportForm", "restoreTrigger");
    restoreTrigger.value = "restore";
    var supportForm = document.getElementById("supportForm");
    supportForm.submit();
}

检验表单是否可恢复

使用 submitRestoreRequest() 发送到服务器的恢复请求将使用 POST 方法,因为这是所有 JSF 表单指定的 HTML 方法。要避免发生无限循环,只有在响应 GET 请求而生成当前页面时发送恢复请求。此外,存储库必须包含当前用户/表单组合的保存数据。isRestorable() 函数将检验这两个条件,它将对一个客户端 JavaScript 表达式求值,该表达式将包含两个在服务器端计算的 JSP/JSF EL 表达式的值(参见 清单 9):

清单 9. 验证是否可以发送恢复请求
function isRestorable() {
    return "${pageContext.request.method}".toUpperCase() == "GET"
        && <h:outputText value="#{viewRestorer.currentViewRestorable}"/>;
}

假设在发送 GET 请求后生成了页面,并且 ViewRestorer bean 的 isCurrentViewRestorable() 方法在服务器端返回了 true,生成的 JavaScript 函数的表达式的值为 true(参见 清单 10):

清单 10. 发出 GET 请求后生成的 JavaScript 代码
function isRestorable() {
    return "GET".toUpperCase() == "GET"
        && true;
}

如果使用 submitRestoreRequest() 发送恢复请求或者用户单击 Submit 按钮,服务器将收到一个 POST 请求并且 isRestorable() 将返回 false,如 清单 11所示:

清单 11. 发出 GET 请求后生成的 JavaScript 代码
function isRestorable() {
    return "POST".toUpperCase() == "GET"
        && true;
}

了解如何处理恢复请求

本节将解释 immediate 属性的作用以及如何使用示例应用程序的 ViewRestorer 类的 restoreCurrentView() 方法的 renderResponse() 调用。

使用 JSF 组件的 immediate 属性

前面一节中使用的 <h:inputHidden> 组件将其 immediate 属性设置为 true,因此可以在 JSF 请求处理生命周期的早期调用侦听器方法。更准确地说,调用将发生在 Apply Request Values 阶段。也可针对一些命令按钮将 immediate 属性设为 true,这些命令按钮的操作方法应该在 Apply Request Values 阶段调用而不是等到 Invoke Application 阶段调用。如果需要向 JSF 表单添加一个 Restore 按钮,您可以使用一个 <h:commandButton> 标记,其 immediate 属性为 true(请参见 清单 12):

清单 12. 恢复表单数据的命令按钮
<h:form id="supportForm">
    ...
    <h:commandButton id="restoreButton" value="Restore"
            actionListener="#{viewRestorer.actionListener}"
            immediate="true"/>
    ...
</h:form>

最后,将在 Apply Request Values 阶段调用 ViewRestorer bean 的 valueChangeListener()actionListener() 方法,因为 SupportForm.jsp 页面的 restoreTriggerrestoreButton 组件将 immediate 属性设置为 true

跳过某些 JSF 请求处理阶段

前面一节讨论的 renderResponse() 调用与本节讨论的 renderResponse() 调用没有任何关系。JSF 框架在 Restore View 阶段执行前一个调用。而示例应用程序代码在 JSF 请求处理生命周期的 Apply Request Values 阶段执行后一种调用。

ViewRestorer 类的两个侦听器方法调用同一个类的 restoreCurrentView() 方法,而后者将调用 faces 上下文的 renderResponse() 方法。因此,请求处理将从 Apply Request Values 阶段直接跳跃到 Render Response 阶段,跳过了 Process Validations、Update Model Values 和 Invoke Application 阶段。

让我们分析一下 ViewRestorer 类的 restoreCurrentView() 方法的 renderResponse() 调用。首先,当使用 submitRestoreRequest() 函数提交表单以触发 ViewRestorervalueChangeListener() 时,应用程序必须忽略所有已提交的数据。这就是 DataMapRepository 类的 restoreValues() 方法清除每个输入组件 submittedValue 属性的原因。丢失值一般会引起很多验证错误,但是不会出现在示例应用程序中,因为由于 renderResponse() 调用,没有对恢复请求执行 Process Validations 阶段。

使用 renderResponse() 跳过 Process Validations、Update Model Values 和 Invoke Application 阶段意味着您不用担心恢复请求的任何副作用。因此,在验证阶段后侦听阶段事件的 AutoSaveListener 类不会受到这些请求的影响,因此无需改变该类。此外,执行恢复请求后,将不会更新应用程序的数据模型并且也不会调用操作方法。如您所见,对恢复和自动保存请求应用了相同的规则,这些请求可以透明地进行处理,而不会打断应用程序的逻辑。

允许用户控制表单保存和恢复

隐藏的 restoreTrigger 组件和 submitRestoreRequest() JavaScript 函数提供了触发表单恢复的方法。服务器端代码执行其余的任务,生成一个包含自动保存数据的恢复过的表单。然而,还可以添加一些按钮,以便用户可以控制保存或恢复表单的时机。在真实的应用程序中,如果希望保持用户界面简洁并能够透明、自动地保存和恢复表单,则不需要添加这些按钮。

然而,您可能希望对真实的应用程序扩展自动保存功能,以便您的用户可以像桌面程序保存/加载文档一样保存/加载 Web 表单。在本例中,用户可以恢复 Web 表单、修改一些数据并重新向 Web 服务器提交表单。假设您拥有一个可以提交每月发票的表单。其中一些输入 — 例如发票日期、发票额以及发票行 — 每月都会改变,但是可以重用输入的供应商和购买者信息,从而减少输入表单数据所需的时间。

添加一个 Save 按钮

通过上节的介绍,您已经了解了如何添加 Restore 按钮。表单还可以包含一个 Save 按钮,其代码如 清单 13 所示。Save 按钮不是通过 actionactionListener 属性指定服务器端方法,而是在用户单击按钮时使用 onclick 属性调用 submitSaveRequest() JavaScript 函数。

清单 13. 保存表单数据的命令按钮
<h:form id="supportForm">
    ...
    <h:commandButton id="saveButton" value="Save"
            onclick="return submitSaveRequest()"/>
    ...
</h:form>

清单 14 展示了 SupportForm.jsp 页面的 submitSaveRequest() 函数。这个 JavaScript 函数可以借助 AutoSaveScript.js 文件的 submitAllForms() 函数向服务器发送表单数据。本系列的 第 1 部分 提供了 AutoSaveScript.js 文件的代码。然后,submitSaveRequest() 将返回 false,这样 Save 按钮的 onclick 表达式将返回 false,这说明 Web 浏览器不能向服务器提交表单。

清单 14. 提交当前用户输入以保存在服务器中
function submitSaveRequest() {
    submitAllForms();
    return false;
}

如果从 onclick 属性删除 return 关键字,或者返回的是 true 而不是 false,表单数据将被提交两次,第一次由 submitAllForms() 函数使用 Ajax 提交,而第二次由 Web 浏览器提交,它将 Save 按钮作为普通的 Submit 按钮处理。浏览器的提交将导致页面刷新并且很可能出现验证错误。通过返回 onclick 属性为 false,Web 浏览器将不再提交表单数据并且只通过 submitAllForms() 函数保存用户输入,而不需要重新刷新页面。

添加一个 Auto-Save 复选框

表单页面还可以包含一个复选框,它允许用户对当前页面启用或禁用自动保存功能(参见 清单 15)。setAutoSaving() 函数将在 Auto-Save Form 复选框的 onclick 属性内进行调用。该复选框是在 SupportForm.jsp 文件中由 <h:selectBooleanCheckbox> 创建。AutoSaveScript.js 文件的 setAutoSaving() 函数使用 JavaScript API 的 setInterval() 函数指示 Web 浏览器,要求它在每次超过指定的微秒数时调用 submitAllForms()。如果 0 被传递给 setAutoSaving() 函数,那么将禁用自动保存功能。

清单 15. 启用和禁用自动保存功能的复选框
<h:form id="supportForm">
    ...
    <h:selectBooleanCheckbox id="autoSaveCheckbox"
            onclick="setAutoSaving(this.checked ? autoSaveInterval : 0)"/>
    <h:outputLabel value="Auto-Save Form" for="autoSaveCheckbox"/>
    ...
</h:form>

autoSaveInterval 变量在 SupportForm.jsp 中被设置为 10000

客户端初始化

在 Web 浏览器加载页面后,SupportForm.jsp<body> 标记使用 onload 属性调用 init() 函数(如 清单 16 所示)。这个函数将检查表单数据是否可恢复,然后要求用户确认是否希望恢复自动保存的表单。如果用户单击确认对话框的 OK 按钮,init() 将调用 submitRestoreRequest()。否则,如果选择了 autoSaveCheckboxinit() 将调用 setAutoSaving() 函数。

清单 16. 获得用户同意恢复表单数据
var autoSaveInterval = 10000;
    
function init() {
    if (isRestorable())
        if (confirm("Do you want to restore the auto-saved form?")) {
            submitRestoreRequest();
            return;
        }
    var autoSaveCheckbox
        = getFormElement("supportForm", "autoSaveCheckbox");
    if (autoSaveCheckbox.checked)
        setAutoSaving(autoSaveInterval);
}

结束语

在这份 共分三部分的系列文章 中,您完成了以下功能:

  • 在基于 JSF 的 Web 应用程序中实现了表单自动保存功能。
  • 使用 Ajax 无需刷新 Web 页面就可保存表单。
  • 在服务器端管理表单数据。
  • 恢复 JSF 表单的数据。

您学习了大量 JavaScript 和 JSF 技巧,比如:

  • 使用 JavaScript 编写和提交表单数据。
  • 删除 XMLHttpRequest 对象以避免 Web 浏览器发生内存泄漏。
  • 使用 JSF 框架处理 Ajax 请求。
  • 设置浏览器 ID 以跨浏览器会话识别匿名用户。
  • 递归式遍历 JSF 组件树。
  • 使用 JSF 组件的 immediateonclick 属性。
  • 调用 faces 上下文的 renderResponse()responseComplete() 方法。

如果您希望实现表单自动保存功能或类似功能,尽可在您自己的 Ajax/JSF 应用程序中重用本系列附带的示例代码。

回页首

下载

描述 名字 大小
本文的示例程序 wa-aj-jsf3.zip 9KB

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多