内容


使用 JavaScript 让 XForms 变得更健壮

先决条件

本文只基于 XForms 和 JavaScript。针对安装在 Mozilla Firefox 2.0 之上的 Mozilla XForms 插件 进行测试。使用的是标准的 XForms 和 JavaScript,因此应该在这两个标准技术的其他实现上运行。没有使用服务器端的技术。

典型的表示例

我们来看一个典型的 XForms 示例。它展示了如何创建表示 XML 文档中重复节点的表。尤其展示了如何使用 XForms 执行聚集计算以及如何使用 XForms 添加或删除模型数据的节点,同步地自动保存视图。查看清单 1 中的完整源代码。

清单 1. 典型的 XForms 表示例
<?xml version="1.0" encoding="UTF-8"?>
<xhtml:html xmlns:ev="http://www.w3.org/2001/xml-events" 
xmlns:xforms="http://www.w3.org/2002/xforms" 
xmlns:xhtml="http://www.w3.org/1999/xhtml" 
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <xhtml:head>
        <xhtml:title>Demonstration of table with 
                                      column total</xhtml:title>
        <xf:model id="my-model" xmlns="http://www.w3.org/1999/xhtml"
 xmlns:xf="http://www.w3.org/2002/xforms">
            <xf:instance id="my-data" src="my-data.xml" xmlns=""/>
            <xf:bind calculate="sum(../Item/Amount)" nodeset="/Data/Total"/>
            <xf:submission action="my-data.xml" id="update-from-local-file"
 instance="my-data" method="get" replace="instance"/>
            <xf:submission action="my-data.xml" id="view-xml-instance"
 method="get"/>
            <xf:submission action="my-data.xml" id="save-to-local-file"
 method="put"/>
        </xf:model>
    </xhtml:head>
    <xhtml:body>
        <xf:group ref="/Data" xmlns="http://www.w3.org/1999/xhtml" 
xmlns:xf="http://www.w3.org/2002/xforms">
            <xf:label/>
            <xf:repeat id="repeatItem" nodeset="Item" 
xmlns="http://www.w3.org/1999/xhtml">
                <xf:input class="item-description" id="description-input" 
ref="Description" xmlns="http://www.w3.org/1999/xhtml">
                    <xf:label/>
                </xf:input>
                <xf:input class="item-amount" ref="Amount" 
xmlns="http://www.w3.org/1999/xhtml">
                    <xf:label/>
                </xf:input>
            </xf:repeat>
            <xhtml:div id="sum">
            <xf:output ref="/Data/Total" xmlns="http://www.w3.org/1999/xhtml">
                <xf:label/>
            </xf:output>
            </xhtml:div>
            <xf:trigger id="insertbutton" xmlns="http://www.w3.org/1999/xhtml">
                <xf:label>Add Item</xf:label>
                <xf:action ev:event="DOMActivate">
                    <xf:insert at="last()" nodeset="Item[last()]" 
position="after"/>
                    <xf:setvalue ref="Item[last()]/Description" value="''"/>
                    <xf:setvalue ref="Item[last()]/Amount" value="0"/>
                    <xf:setfocus control="description-input"/>
                </xf:action>
            </xf:trigger>
            <xf:trigger id="delete" xmlns="http://www.w3.org/1999/xhtml">
                <xf:label>Delete Item</xf:label>
                <xf:delete at="index('repeatItem')" ev:event="DOMActivate" 
nodeset="Item[index('repeatItem')]"/>
            </xf:trigger>
            <xf:submit submission="update-from-local-file" 
xmlns="http://www.w3.org/1999/xhtml">
                <xf:label>Reload</xf:label>
            </xf:submit>
            <xf:submit submission="save-to-local-file" 
xmlns="http://www.w3.org/1999/xhtml">
                <xf:label>Save</xf:label>
            </xf:submit>
            <xf:submit submission="view-xml-instance" 
xmlns="http://www.w3.org/1999/xhtml">
                <xf:label>View XML Instance</xf:label>
            </xf:submit>
        </xf:group>
    </xhtml:body>
</xhtml:html>

您可能注意到了,在源代码中模型中的数据来自外部 XML 文件。该文件如清单 2 所示。

清单 2. 示例的 XML 数据
<?xml version="1.0" encoding="UTF-8"?>
<Data>
   <Item>
      <Description>Furniture</Description>
      <Amount>1000</Amount>
   </Item>
   <Item>
      <Description>Dock</Description>
      <Amount>2000</Amount>
   </Item>
   <Item>
      <Description>Boat</Description>
      <Amount>3000</Amount>
   </Item>
   <Item>
      <Description>Lawn equipment</Description>
      <Amount>4000</Amount>
   </Item>
   <Item>
      <Description>Hot tub</Description>
      <Amount>5000</Amount>
   </Item>
   <Total>15000</Total>
</Data>

运行示例

简单地在 Web 浏览器中打开示例即可运行它。您应该看到类似图 1 所示的内容。

图 1. 典型的 XForms 示例
典型的 XForms 示例
典型的 XForms 示例

尝试此示例,注意,您可以使用显示的 Add ItemDelete Item 按钮添加和删除行。例如,如果您单击一次 Delete Item 按钮,您应看到类似图 2 的内容。

图 2. 删除了一个项目
删除了一个项目
删除了一个项目

注意,不仅是删除了顶部的项目,而且项目总数也重新进行了计算。这很好地说明了 XForms 的强大功能。再单击 Delete Item 按钮四次,您应看到类似图 3 的内容。

图 3. 删除了所有项目
删除了所有项目
删除了所有项目

这不怎么好看,但是可以单击 Add Item 按钮开始重新输入数据,对吧?但结果是在这种情况下单击 Add Item 按钮不起任何作用。

问题出在哪儿?

为什么 Add Item 在这种情况下不起作用?如果您查看 源代码,Add Item 按钮导致向模型中进行插入,使用 XForms 插入命令添加一条记录。而那条记录刚好是一个 “Item” 节点,因此 Add Item 操作接着为 Item 的 Description 和 Amount 节点设置一些默认值。Add Item 通过实际上克隆结构中的最后一个节点以定义插入节点的类型。nodeset="Item[last()]" 正是实现此操作。这正是 “bug” 的来源。如果您消除了所有项目,那么就没有节点可供克隆因而也就无法执行插入。因此 Add Item 按钮会失效。当然,现在的问题是,如何修正这个问题?

修正

正如软件工程中的大多数问题一样,有很多方法可以解决此处描述的问题。这里要阐述的解决方案是使用 JavaScript。您的策略是更改删除的方式。在 清单 1 中您会发现 Delete Item 按钮使用了 XForms 删除命令。您将使用对一个 JavaScript 函数的调用替代对此命令的调用。该函数需要与 XForm 模型交互。您将只修改对 Delete Item 按钮的调用,因此无需更改 Add Item 按钮。我们来查看一下 JavaScript 解决方案。

JavaScript

正如前面提到的,方法是只更改 Delete Item 按钮,而不是 Add Item 按钮。因此要想使 Add Item 按钮起作用,决不能删除模型中的所有数据。因此您将跟踪周围的项目个数,当您删除至最后一个时,不要继续删除。只是使用默认内容替代该项目的内容,好像您执行了 Add Item 按钮一样。我们来查看一下清单 3 中的 JavaScript 代码。

清单 3. JavaScript deleteItem() 函数
<xhtml:script type="text/javascript">
                 //<![CDATA[
                      function deleteItem(){
                           var model = document.getElementById("my-model");
                           var instance = model.getInstanceDocument("my-data");
                           var dataElement = instance.getElementsByTagName("Data")[0];
                           var itemElements = dataElement.getElementsByTagName("Item");
                           var cnt = itemElements.length;
                           if (cnt > 1){
                                dataElement.removeChild(itemElements[cnt-1]);
                           } else {
                                // last element so just set its data to default vals
                                var descripElement = 
itemElements[0].getElementsByTagName("Description")[0];
                                descripElement.childNodes[0].nodeValue = "";
                                var amtElement = 
itemElements[0].getElementsByTagName("Amount")[0];
                                amtElement.childNodes[0].nodeValue = "0";
                           }
                           model.rebuild();
                           model.recalculate();
                           model.refresh();
                      }
                 //]]>
            </xhtml:script>

下面介绍其工作原理。您需要从 JavaScript 中访问 XForms 模型。所幸的是,使用 JavaScript 的 DOM API 即可轻松地实现访问。XForms 模型是页面 DOM 的一部分,因此您只需使用 document.getElementById() 方法,正如您访问 HTML div 或 HTML 输入字段那样。

当您使用 document.getElementById() 访问 XForms 模型时,如 清单 3 所示,您得到的是 nsIXFormsModelElement。此对象中含有几个非常有用的方法,包括上面使用的 getInstanceDocument() 方法。这样您就可以访问 清单 1 中定义的 XForms 实例。这是一个 DOM 对象,表示 清单 2 中的 XML 文档。因此在 JavaScript 代码中,您只需让 DOM 获取 Item 元素即可。您确定模型中有多少个 Item 并将其存储在 cnt 变量中。很明显,此处要处理两个用例。第一个是您拥有不止一个 Item,第二个是您只有一个 Item。在第一个用例中,您需要删除项目。

使用 JavaScript 删除项目

那么如何使用 JavaScript 而不是 XForms 删除控件删除一条 Item 记录呢?解决方案极其简单。先前调用的 getInstanceDocument() 方法给您提供一个真正的 DOM 对象。因此您可以对此对象执行任何对其他 DOM 对象执行的操作。它支持全部的 DOM API 功能。因此您简单地对 DOM 元素使用 removeChild() 方法。删除 index (cnt - 1) 处的 Item,因为您的元素数组(调用 getElementsByTagName("Item") 获得)是 0-indexed。这里没有什么神奇的 XForms API,只有简单的 DOM 编程。

删除最后一个项目

您已经重新生成了使用 XForms 删除命令能够正常完成的逻辑。这样做的目的是为了更优雅地处理删除最后一个项目时的情况。现在查看一下这个临界情况,比如 cnt == 1

正如前面提到的,此处的关键在于您并不希望实际删除最后一个项目。如果删除,则 Add Item 动作中的 XForms 插入命令将不再有效。当然,您也可以重新编码,但是尽量避免这样做。

因此不是删除最后一个项目,而是使用一个空白 Item 替代该项目的内容。这个 Item 的类型与您单击 Add Item 按钮时得到的 Item 类型相同。例如,它的 Description 为一个空字符串,而它的 Amount 为 0。为此,您再次使用 DOM API。您只访问最后一个 Item 元素,然后访问该元素的 Description 和 Amount 元素。我们将其文本节点的值分别设为空字符串和 0。

让视图和模型同步

此时,您已经处理了 Delete Item 的两个用例。还有一点工作要做。通常情况下,当您使用 XForms 命令修改 XForms 模型时,所有的重新计算和视图刷新都是自动完成的。使用 JavaScript 访问这些内容时情况却并非如此。您需要手动完成这些工作。

所幸的是,在 deleteItem() 函数开始部分,您引用的 nsIXFormsModelElement 对象含有几个有用的方法可以帮助解决问题。首先使用的是它的 rebuild() 方法。此方法让它重建模型中数据的内部表示。它实际上让模型对象与 DOM 同步。这完全不会影响视图,只是对模型有影响。

接下来您需要使用模型的 recalculate() 方法。原因在于您要使用模型中的 XForms bind-calculate 命令跟踪表中 amount 的总数。当您调用 recalculate() 方法时会刷新该计算。通用,这只是让模型与 DOM 同步。完全不影响视图。

既然模型和数据已经同步了,您可以重新绘制视图。为此,您对模型调用 refresh() 方法。此方法导致绑定到模型的所有控件被刷新。对于删除一行而言,这将使该行消失,对于删除最后一行,这将导致最后一行的数据更改为指定的空白数据。在这两种情况下,这将使显示的项目总数基于当前数据得到更新。

触发 JavaScript

既然已经编写了智能的 JavaScript 函数处理删除,只需修改 XForm,使它在调用到 Delete Item 按钮时调用此 JavaScript 就可大功告成了。为此,只需修改 Delete Item 的 XForms 声明,如清单 4 所示。

清单 4. 新的 XForm delete item 控件
<xf:trigger id="delete" xmlns="http://www.w3.org/1999/xhtml">
                <xf:label>Delete Item</xf:label>
                <xf:load ev:event="DOMActivate"
 resource="javascript:deleteItem()"/>
            </xf:trigger>

注意,您只是将 XForms delete 命令替换为引用 deleteItem() JavaScript 函数的 load 命令。这是最后一个需要修改的地方。现在我们来运行一下修改后的示例。

运行修改后的示例

简单地将示例加载到浏览器中。显示的内容与 图 1 中原来显示的一样。但是,当您删除至最后一行并单击 Delete Item 后,您应看到图 4 所示的结果。

图 4. 删除最后一行
删除最后一行
删除最后一行

现在最后一行没有消失。而是变为默认值。如果单击 Add Item 按钮,您将看见类似图 5 的内容。

图 5. 删除最后一个 Item 后添加 Item
删除最后一个 item 后添加 item
删除最后一个 item 后添加 item

Add Item 按钮仍然有效,没有什么变化。

结束语

您已经了解了如何使用 JavaScript 创建更智能的 delete item 动作。更重要的是,您了解了如何使用 JavaScript 访问和修改 XForms 模型数据,重新计算 XForms 绑定的计算,以及刷新 XForms 视图。您可以想像几种其他方法,使 Add Item 和 Delete Item 动作提供越来越复杂的功能。希望您也能够发现如何使用这些技术改进您自己的基于 XForms 的应用程序。


下载资源


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=247986
ArticleTitle=使用 JavaScript 让 XForms 变得更健壮
publish-date=08132007