本文首发于:http://www.ibm.com/developerworks/cn/web/1203_zhouxiang_dojoxwire/
作为 Dojo 这样一个强大且全面的 web 开发控件库中的一部分,它提供了很多简单且功能完善的 API。通过这些 API,我们能够很方便的实现数据的绑定,包括简单数据的绑定、复杂数据的绑定、绑定转换器、动作或者事件的绑定以及各种数据绑定适配器,如:文本型适配器、表格型适配器、树型适配器等等。它不仅支持脚本类型的接口 API:“dojox.wire.ml”,还支持声明式的 API,即:以 Dojo 的 Widget 的方式直接在 HTML 中声明来进行数据绑定的操作。这篇文章将重点介绍 Dojo 的这种数据访问和绑定的功能,以及我们如何基于 Dojo 的 Wire 工具包实现自己的数据访问和绑定。
Dojox 的 Wire 工具包简介
Dojox 的 Wire 工具包是 Dojo 中用于数据操作的一组工具集合,它主要用于数据绑定和服务调用。它提供了一组非常强大的 API 让用户能十分方便地访问、更新复杂数据以及向后台服务传值和取值。它也提供了一组声明式绑定数据用法,用于完善先前的只支持编程方式 API 的不足。
Dojox 的 Wire 工具包的基础接口
我们先来看看 Wire 包里面的 create 接口:
清单 1. 接口“dojox.wire.create”
var wire = dojox.wire.create({}); t.assertTrue(wire instanceof dojox.wire.Wire); wire = dojox.wire.create({property: "a"}); t.assertTrue(wire instanceof dojox.wire.Wire); wire = dojox.wire.create({attribute: "a"}); t.assertTrue(wire instanceof dojox.wire.DataWire); wire = dojox.wire.create({path: "a"}); t.assertTrue(wire instanceof dojox.wire.XmlWire); wire = dojox.wire.create({children: "a"}); t.assertTrue(wire instanceof dojox.wire.CompositeWire); wire = dojox.wire.create({columns: "a"}); t.assertTrue(wire instanceof dojox.wire.TableAdapter); wire = dojox.wire.create({nodes: "a"}); t.assertTrue(wire instanceof dojox.wire.TreeAdapter); wire = dojox.wire.create({segments: "a"}); t.assertTrue(wire instanceof dojox.wire.TextAdapter); wire = dojox.wire.create({wireClass: "dojox.wire.DataWire"}); t.assertTrue(wire instanceof dojox.wire.DataWire); |
可见,通过 Create 接口,我们可以创建出我们所需要的相应 Wire 对象。通过以上实例,我们可以了解一下 Dojo 所支持的所有类型的 Wire 对象:Wire、DataWire、XmlWire、CompositeWire、TableAdapter、TreeAdapter、TextAdapter、DataWire。我们会在接下来的章节中一一介绍这些对象的用途,用法和使用技巧。
接下来我们再来看看另一个基础接口“transfer”:
清单 2. 接口transfer
var source = {a: "A"}; var target = {}; dojox.wire.transfer( {object: source,property: "a"}, {object: target,property: "a"}); t.assertEqual(source.a,target.a); |
这里的 transfer 接口其实起到了一个数据复制的作用,即将“source”的属性“a”的值复制到“target”的属性“a”上。
再来看看“connect”接口:
清单 3. 接口connect
var trigger = {transfer: function() {},transferArgument: function() {}}; var source = {a: "A"}; var target = {}; dojox.wire.connect({scope: trigger,event: "transfer"}, {object: source, {object: target,property: "a"}); trigger.transfer(); t.assertEqual(source.a,target.a); |
其实“connect”的接口功能与“transfer”基本一样,但是,它是更深层次的数据复制,它定义了触发这个数据复制行为的方法,即:只有调用该方法才会触发数据复制的行为。这里的“trigger”对象的“transfer”方法就是这种方法。
还可以通过注册 topic 的方式来使用 connect 接口:
清单 4. 接口connect 基于 topic
target = {}; dojox.wire.connect({topic: "transfer"}, {object: source,property: "a"}); dojo.publish("transfer"); t.assertEqual(source.a,target.a); |
可以看到,这里可以基于 Dojo 的 publish 方法来使用 connect 接口。
还有,connect 接口还支持参数传递:
清单 5. 接口connect 使用参数
target = {}; dojox.wire.connect({scope: trigger,event: "transferArgument"}, {property: "[0].a"},property: "a"}); trigger.transferArgument(source); t.assertEqual(source.a,target.a); target = {}; dojox.wire.connect({topic: "transferArgument"},property: "a"}); dojo.publish("transferArgument",[source]); t.assertEqual(source.a,target.a); |
可见,不论是基于对象方法的 connect 接口,还是基于 topic 的 connect 接口,都可以传递参数。这里的“[0]”便是指代的第一个传入参数。
另外,如果需要取消这种关联,使用“disconnect”接口即可:
清单 6. 接口disconnect
dojox.wire.disconnect(connection) |
Wire 工具进阶
这一章我们主要来介绍一下稍微复杂一点的 Wire 接口,Dojo 主要将其封装在“dojox.wire.Wire”这个对象里。
清单 7. 数据的关联 Wire
var source = {a: "A",b: {c: "B.C"}}; var sourceWire = new dojox.wire.Wire({object: source,property: "a"}); source.a = "B"; t.assertEqual("B",sourceWire.getValue()); sourceWire.setValue("C"); t.assertEqual("C",source.a); |
可以看到,“sourceWire”这个对象就像一根电线一样,连接着“source”对象的“a”属性,随时可以取得和设置该属性的值:“sourceWire.getValue()”和“sourceWire.setValue()”。这种功能很适合用于复杂的对象,我们不用每次都深入该复杂对象的内部,去访问或修改其内部某个属性的值,而只需要建立一个外部变量与该内部属性的关联(就好象牵了一根线锁定了该属性)就可以了,以后对该属性的所有操作都可以通过这个外部变量来完成。
再来看看稍微复杂一点的例子:
清单 8. 复杂数据关联 Wire
target = {}; value = new dojox.wire.Wire({object: source,property: "b.c"}).getValue(); new dojox.wire.Wire({object: target,property: "b.c"}).setValue(value); t.assertEqual(source.b.c,target.b.c); source = {a: ["A"]}; target = {}; value = new dojox.wire.Wire({object: source,property: "a[0]"}).getValue(); new dojox.wire.Wire({object: target,property: "a[0]"}).setValue(value); t.assertEqual(source.a[0],target.a[0]); |
这里我们主要想说明的是,关联的属性值可以是 2 级甚至更多(“b.c”或“b.c.d.e.x.x.x.x.x”),同时,对于数组类型的属性值,同样可以关联(dojox.wire.Wire({object: source,property:"a[0]"}))。
再来看一组更为新颖的例子:
清单 9. 复杂数据关联 Wire 基于函数
// by getter/setter source = {getA: function() { return this._a; },_a: "A"}; target = {setA: function(a) { this._a = a; }}; value = new dojox.wire.Wire({object: source,property: "a"}).getValue(); new dojox.wire.Wire({object: target,property: "a"}).setValue(value); t.assertEqual(source._a,target._a); // by get/setPropertyValue source = {getPropertyValue: function(p) { return this["_" + p]; },_a: "A"}; target = {setPropertyValue: function(p,v) { this["_" + p] = v; }}; value = new dojox.wire.Wire({object: source,target._a); |
从例子中可以看出,这里不论是 source 还是 target 都没有属性“a”,取而代之的是属性“_a”。但是这里我们关联的属性还是“a”,因为“setA”和“getA”方法的存在。同样,“getPropertyValue”,“setPropertyValue”也有同样的功能。所以这里我们可以看出,数据的关联还可以通过方法来完成,这种方式也会促使我们的代码变得更加规范。
让我们再深入一点,其实 Dojo 的 wire 还支持直接的类型转换:
清单 10. 复杂数据关联 Wire 类型转换
//例 1 var source = {a: "1"}; var string = new dojox.wire.Wire({object: source,property: "a"}).getValue(); t.assertEqual("11",string + 1); var number = new dojox.wire.Wire( {object: source,property: "a",type: "number"}).getValue(); t.assertEqual(2,number + 1); //例 2 var source = {a: "1"}; var converter = {convert: function(v) { return v + 1; }}; var string = new dojox.wire.Wire( {object: source,converter: converter}).getValue(); t.assertEqual("11",string); //例 3 var source = {a: "1"}; var converter = {convert: function(v) { return v + 1; }}; var number = new dojox.wire.Wire( {object: source, type: "number",converter: converter.convert}).getValue(); t.assertEqual(2,number); //例 4 dojo.declare("dojox.wire.tests.programmatic.Wire.Converter",null,{ convert: function(v){ return v + 1; } }); var source = {a: "1"}; var number = new dojox.wire.Wire({ object: source, property: "a", converter: "dojox.wire.tests.programmatic.Wire.Converter"}).getValue(); t.assertEqual(2,number); |
例 1:直接将字符串转换为数字类型。
例 2,例 3:通过转换函数“converter.convert”进行转换,这里可以通过函数对关联的值作任何操作,事实上已经超出了类型转换的范畴了。
例 4:可以声明专门的 Dojo 类来完成数据转换功能,这种做法比较符合面向对象的设计思路。
混合类型的 Wire
混合类型的 Wire 其实和基本的 Wire 类似,不同的是它支持一次性关联多个属性。我们先来看看如下示例代码:
清单 11. 混合型的 Wire(对象模式)
var source = {a: "A",b: "B"}; var target = {}; var children = {x: {property: "a"},y: {property: "b"}}; var value = new dojox.wire.CompositeWire( {object: source,children: children}).getValue(); t.assertEqual(source.a,value.x); t.assertEqual(source.b,value.y); new dojox.wire.CompositeWire({object: target,children: children}).setValue(value); t.assertEqual(source.a,target.a); t.assertEqual(source.b,target.b); |
注意这里的“children”变量,对于混合类型的 Wire(CompositeWire),它的“getValue”方法拿到的是一个关联数据的对象,这里为“value”,value 的“x”和“y”值分别对应着关联数据(source)的“a”和“b”属性值。
这里我们是通过对象来做数据关联,同样也可以通过数组,参考如下代码:
清单 12. 混合型的 Wire(数组模式)
target = {}; children = [{property: "a"},{property: "b"}]; value = new dojox.wire.CompositeWire({object: source,value[0]); t.assertEqual(source.b,value[1]); new dojox.wire.CompositeWire({object: target,target.b);
|
大家可以看到,这里的“children”为数组,与之前对象方式不同,其关联的数据也通过数组的下标运算符取得:“value[0]”,“value[1]”。
这两种类型的混合型 Wire 均可以通过另一种方式来使用:
清单 13. 混合型的 Wire 新模式
target = {}; value = new dojox.wire.CompositeWire({children: children}).getValue(source); t.assertEqual(source.a,value.y); new dojox.wire.CompositeWire({children: children}).setValue(value,target); t.assertEqual(source.a,target.b); target = {}; value = new dojox.wire.CompositeWire({children: children}).getValue(source); t.assertEqual(source.a,value[1]); new dojox.wire.CompositeWire({children: children}).setValue(value,target.b);
|
不知道大家是否注意到,这里的混合型 Wire 对象已经变成一个纯的 Wire 对象了,在它的构造器里面我们找不到数据源(source)了,数据源在这里是通过参数传递给 Wire 对象:“(new dojox.wire.CompositeWire({children:children}).getValue(source))”
由此可见,其实 Wire 并不是依赖于数据源而存在的,它可以随时切换数据源对象以便即时取得相应数据源的内部值。就好象一只蚊子,可以随时扎在不同的人身上的不同位置。
基于复杂数据源的 DataWire
之前我们所看到的 Wire 都是针对一些简单的对象类型,接下来我们会看到建立在复杂数据源上面的 Wire,即 DataWire:
清单 14. 基于复杂数据源的DataWire
var store = new dojox.data.XmlStore(); var item = store.newItem({tagName: "x"}); new dojox.wire.DataWire({dataStore: store,object: item,attribute: "y"}).setValue("Y"); var value = new dojox.wire.DataWire( {dataStore: store,attribute: "y"}).getValue(); t.assertEqual("Y",value); // 嵌套属性 new dojox.wire.DataWire({dataStore: store,attribute: "y.z"}).setValue("Z"); value = new dojox.wire.DataWire( {dataStore: store,attribute: "y.z"}).getValue(); t.assertEqual("Z",value); |
通过这个例子我们可以看到,其实它的用法与之前介绍的 Wire 和 CompositeWire 基本类似,只是它是建立在一个 store 对象的某个 item 上面。这里它对 tagName 为“x”的 item 加入了“y”属性和“z”属性,并分别赋予其“Y”值和“Z”值。
这种功能非常实用,我们不用再每次去深入到 store 内部查找和获取某个内部属性的值,直接通过 DataWire 来操作即可。
同样,它也能直接设置和获取嵌套属性,见后半段代码,这里对 item 的“y.z”属性赋值和取值,其实就是“item.y.z”的取值和赋值。Wire 支持这种嵌套属性的直接生成和赋 / 取值。
这里需要强调的是,对数据关联到 DataWire 对象所做出的任何修改都是永久性的,也就是说数据源也会被改变,Wire 和 CompositeWire 也都是如此,所以大家一定要慎用。如果你仅仅是想克隆一份数据,请千万别用 Wire 的方式。
基于 Path 的 XMLWire
我们的 Wire 还支持通过 Path(类似于 XPATH)的方式来定位和操作数据,这是一个非常强大的功能,尤其是针对复杂数据源。
清单 15. XMLWire 基本用法
var object = {}; var wire = dojox.wire.create({object: object,property: "element"}); new dojox.wire.XmlWire({object: wire,path: "/x/y/text()"}).setValue("Y"); var value = new dojox.wire.XmlWire({object:object,property: "element",path: "y/text()"}).getValue(); t.assertEqual("Y",value); |
从上上述示例中我们可以看到一个熟悉的标识:“path: "/x/y/text()"”,是不是很像 XPATH ?不错,这里就是 Path 定位。我们给 object 这个对象加上了“x”元素,同时在 x 元素下面加上了“y”元素,然后给它赋值“Y”。取值的时候直接通过 Path:“"y/text()"”即可取得刚刚设定的值。完全符合XPATH 的操作模式。这种方式很适用于复杂数据的快速定位。
XMLWire 不仅支持赋值,还支持对于属性的操作和基于索引(index)的定位,见如下示例:
清单 16. XMLWire 基本用法进阶
// attribute new dojox.wire.XmlWire( {object: object,path: "y/@z"}).setValue("Z"); value = new dojox.wire.XmlWire({object: wire,path: "/x/y/@z"}).getValue(); t.assertEqual("Z",value); // with index var document = object.element.ownerDocument; var element = document.createElement("y"); element.appendChild(document.createTextNode("Y2")); object.element.appendChild(element); value = new dojox.wire.XmlWire( {object: object.element,path: "y[2]/text()"}).getValue(); t.assertEqual("Y2",value); |
与之前 Path 的操作方式一样,加上“@”即可实现对于属性的关联:“path: "y/@z"”。
同样,通过数组下标运算符,可以实现通过索引(index)的定位:“"y[2]/text()"”。
数据适配器 Adapter
适配器其实也有点类似于之前介绍的数据关联(Wire),它们都是 CompositeWire 的一种扩展。但是它们不再是简单的关联,而是会把关联的数据转换成一种新的形势展现出来。Dojo 提供三种适配器:TextAdapter,TableAdapter,TreeAdapter。我们先来看看 TextAdapter。
清单 17. 简单的TextAdapter
var source = {a: "a",b: "b",c: "c"}; var segments = [{property: "a"},{property: "b"},{property: "c"}]; var value = new dojox.wire.TextAdapter({object: source,segments: segments}).getValue(); t.assertEqual("abc",value); |
可以看到,这里的 TextAdapter 将所有关联的属性“a”,“b”,“c”做了一个连接并最终变为“abc”。它主要适用于取值,对于赋值“setValue”它是不支持的。
当然,它还可以做一些连接上的设置:
清单 18. 简单的TextAdapter(连接符)
var source = {a: "a",{property: "c"}]; var value = new dojox.wire.TextAdapter( {object: source,segments: segments,delimiter: "/"}).getValue(); t.assertEqual("a/b/c",value); |
这里我们给它连接符(分隔符)“delimiter: "/"”,所得到的结果就是“"a/b/c"”。
再来看看 TableAdapter:
清单 19. TableAdapter 示例
var source = [ {a: "A1",b: "B1",c: "C1"}, {a: "A2",b: "B2",c: "C2"}, {a: "A3",b: "B3",c: "C3"} ]; var columns = {x: {property: "a"},y: {property: "b"},z: {property: "c"}}; var value = new dojox.wire.TableAdapter({object: source,columns: columns}).getValue(); t.assertEqual(source[0].a,value[0].x); t.assertEqual(source[1].b,value[1].y); t.assertEqual(source[2].c,value[2].z); |
还记得我们之前的 CompositeWire 的例子吗? CompositeWire 支持对象和数组两种模式的数据关联,而这里是两种同时存在。它有点类似于表格模式的匹配,所以被称为“TableAdapter”。注意其访问方式(“value[1].y”等等)与 source 一致。注意:这里的 TableAdapter 也不支持修改操作“setValue”。
最后,我们来看看 TreeAdapter,它也是最为复杂的一种适配器(adapter):
清单 20. TreeAdapter 示例
var source = [ {a: "A1",c: "C3"} ]; var nodes = [ {title: {property: "a"},children: [ {node: {property: "b"}}, {title: {property: "c"}} ]} ]; var value = new dojox.wire.TreeAdapter({object: source,nodes: nodes}).getValue(); t.assertEqual(source[0].a,value[0].title); t.assertEqual(source[1].b,value[1].children[0].title); t.assertEqual(source[2].c,value[2].children[1].title); |
这里我们还是使用 TableAdapter 的数据源,但是关联之后对该数据源的访问方式变了:“value[0].title”,“value[1].children[0].title”,“value[2].children[1].title”。这里我们可以看出,对原始表格型数据的访问已经变成对树型数据源的访问。其实从这个例子我们可以看出,对于很多种数据格式,我们都可以通过 Wire 或者 Adapter 将其转换为我们想要的格式。注意:这里的 TableAdapter 同样不支持修改操作“setValue”。
声明性的数据绑定
之前介绍的都是通过脚本的方式来创建关联,接下来我们来看看如何通过纯声明的方式来建立关联。Dojo 将这些接口都包装在“dojox.wire.ml”包内。
基础接口
先来看一个简单的接口 Action:“dojox.wire.ml.Action”:
清单 21. Action 接口示例
dojox.wire.ml.tests.markup.Action = { transfer: function(){}, source: {a: "A",b: "B"} }; <div dojoType="dojox.wire.ml.Action" trigger="dojox.wire.ml.tests.markup.Action" triggerEvent="transfer"> <div dojoType="dojox.wire.ml.Transfer" source="dojox.wire.ml.tests.markup.Action.source.a" target="dojox.wire.ml.tests.markup.Action.target.a"></div> <div dojoType="dojox.wire.ml.Transfer" source="dojox.wire.ml.tests.markup.Action.source.b" target="dojox.wire.ml.tests.markup.Action.target.b"></div> </div> dojox.wire.ml.tests.markup.Action.target = {}; dojox.wire.ml.tests.markup.Action.transfer(); t.assertEqual(dojox.wire.ml.tests.markup.Action.source.a, dojox.wire.ml.tests.markup.Action.target.a); t.assertEqual(dojox.wire.ml.tests.markup.Action.source.b, dojox.wire.ml.tests.markup.Action.target.b); |
注意中间的一段 HTML 代码,这里我们通过 HTML 直接声明了数据的关联,它的触发器(trigger)为“dojox.wire.ml.tests.markup.Action”, 触发事件为“transfer”。然后通过“dojox.wire.ml.Transfer”类将 source 的“a”和“b”分别和 target 的“a”和“b”进行关联。一旦“dojox.wire.ml.tests.markup.Action”对象调用“transfer”方法,则 source 和 target 随即建立关联。其作用同基本的Wire(dojox.wire.Wire)一样。
同样,声明方式的关联也支持通过 topic 方式建立关联,这里不做赘述。
接下来我们看看更为复杂的情况,Action 可以基于条件来选择是否实现关联。参见如下示例:
清单 22. Action 接口进阶(ActionFilter)
<div dojoType="dojox.wire.ml.Action" triggerTopic="transferFilter"> <div dojoType="dojox.wire.ml.ActionFilter" required="dojox.wire.ml.tests.markup.Action.required" message="no required" error="dojox.wire.ml.tests.markup.Action.error"></div> <div dojoType="dojox.wire.ml.Transfer" source="dojox.wire.ml.tests.markup.Action.source.a" target="dojox.wire.ml.tests.markup.Action.target.a"></div> </div> <div dojoType="dojox.wire.ml.Action" triggerTopic="transferFilterNumber"> <div dojoType="dojox.wire.ml.ActionFilter" required="dojox.wire.ml.tests.markup.Action.value" requiredValue="20" type="number"> </div> <div dojoType="dojox.wire.ml.Transfer" source="dojox.wire.ml.tests.markup.Action.source.a" target="dojox.wire.ml.tests.markup.Action.target.a"></div> </div> <div dojoType="dojox.wire.ml.Action" triggerTopic="transferFilterBoolean"> <div dojoType="dojox.wire.ml.ActionFilter" required="dojox.wire.ml.tests.markup.Action.value" requiredValue="true" type="boolean"> </div> <div dojoType="dojox.wire.ml.Transfer" source="dojox.wire.ml.tests.markup.Action.source.a" target="dojox.wire.ml.tests.markup.Action.target.a"></div> </div> |
参考如上 3 段代码,均为有条件的关联:
1.“dojox.wire.ml.tests.markup.Action.required”必须赋值,否则不关联,错误消息(message)为“no required”。
2.“dojox.wire.ml.tests.markup.Action.value”值必须为数字 20。
3.“dojox.wire.ml.tests.markup.Action.value”值必须为布尔值 true。
我们可以看一个测试案例来了解一下有条件的关联是如何工作的:
清单 23. Action 接口进阶(ActionFilter)工作模式
<div dojoType="dojox.wire.ml.Action" triggerTopic="transferFilterString"> <div dojoType="dojox.wire.ml.ActionFilter" required="dojox.wire.ml.tests.markup.Action.value" requiredValue="executeThis"> </div> <div dojoType="dojox.wire.ml.Transfer" source="dojox.wire.ml.tests.markup.Action.source.a" target="dojox.wire.ml.tests.markup.Action.target.a"></div> </div> dojox.wire.ml.tests.markup.Action.target = {}; dojox.wire.ml.tests.markup.Action.value = null; dojo.publish("transferFilterString"); t.assertEqual(undefined,dojox.wire.ml.tests.markup.Action.target.a); dojox.wire.ml.tests.markup.Action.value = "executeThis"; dojo.publish("transferFilterString"); t.assertEqual(dojox.wire.ml.tests.markup.Action.source.a, dojox.wire.ml.tests.markup.Action.target.a); |
可见,在我们没有设置“dojox.wire.ml.tests.markup.Action.value”的值为“executeThis”的时候,即便是我们触发转换 topic 也无济于事,“dojox.wire.ml.tests.markup.Action.target.a”的值保持为“undefined”。只有当其被赋值“executeThis”之后,关联才被建立起来。
再来看一个基于显示声明数据(HTML Data)的 Action:
清单 24. Action 接口与 Data 协同
dojox.wire.ml.tests.markup.Data = {}; <div dojoType="dojox.wire.ml.Data" id="Data1"> <div dojoType="dojox.wire.ml.DataProperty" name="a" value="A"></div> <div dojoType="dojox.wire.ml.DataProperty" name="b" type="number" value="1"></div> <div dojoType="dojox.wire.ml.DataProperty" name="c" type="boolean" value="true"></div> <div dojoType="dojox.wire.ml.DataProperty" name="d" type="object"> <div dojoType="dojox.wire.ml.DataProperty" name="a" value="DA"></div> <div dojoType="dojox.wire.ml.DataProperty" name="b" value="DB"></div> </div> <div dojoType="dojox.wire.ml.DataProperty" name="e" type="array"> <div dojoType="dojox.wire.ml.DataProperty" value="E1"></div> <div dojoType="dojox.wire.ml.DataProperty" value="E2"></div> </div> <div dojoType="dojox.wire.ml.DataProperty" name="f" type="element" value="x"> <div dojoType="dojox.wire.ml.DataProperty" name="text()" value="F"></div> <div dojoType="dojox.wire.ml.DataProperty" name="@y" value="G"></div> </div> </div> <div dojoType="dojox.wire.ml.Action" triggerTopic="transfer"> <div dojoType="dojox.wire.ml.Transfer" source="Data1.a" target="dojox.wire.ml.tests.markup.Data.target.a"></div> <div dojoType="dojox.wire.ml.Transfer" source="Data1.b" target="dojox.wire.ml.tests.markup.Data.target.b"></div> <div dojoType="dojox.wire.ml.Transfer" source="Data1.c" target="dojox.wire.ml.tests.markup.Data.target.c"></div> <div dojoType="dojox.wire.ml.Transfer" source="Data1.d" target="dojox.wire.ml.tests.markup.Data.target.d"></div> <div dojoType="dojox.wire.ml.Transfer" source="Data1.e" target="dojox.wire.ml.tests.markup.Data.target.e"></div> <div dojoType="dojox.wire.ml.Transfer" source="Data1.f" target="dojox.wire.ml.tests.markup.Data.target.f"></div> <div dojoType="dojox.wire.ml.Transfer" source="Data1.f.@y" target="dojox.wire.ml.tests.markup.Data.target.g"></div> </div> |
这里的代码可能有点多,其实主要就是两个对象:
通过“dojox.wire.ml.Data”显示声明了一个数据源“Data1”,大家可以稍微浏览一下这个数据源,它是一个大型的对象类型数据,里面包含了布尔,字符串,对象,数组等等子类型。
通过“dojox.wire.ml.Action”显示关联“dojox.wire.ml.Data”数据源到“dojox.wire.ml.tests.markup.Data”变量上。
通过以上两步的操作,我们已经可以随时触发数据的关联并开始相应的数据操作了。参考使用方式如下:
清单 25. Action 接口与 Data 协同测试案例
dojox.wire.ml.tests.markup.Data.target = {}; dojo.publish("transfer"); t.assertEqual("A",dojox.wire.ml.tests.markup.Data.target.a); t.assertEqual(1,dojox.wire.ml.tests.markup.Data.target.b); t.assertEqual(true,dojox.wire.ml.tests.markup.Data.target.c); t.assertEqual("DA",dojox.wire.ml.tests.markup.Data.target.d.a); t.assertEqual("DB",dojox.wire.ml.tests.markup.Data.target.d.b); t.assertEqual("E1",dojox.wire.ml.tests.markup.Data.target.e[0]); t.assertEqual("E2",dojox.wire.ml.tests.markup.Data.target.e[1]); t.assertEqual("F",dojox.wire.ml.tests.markup.Data.target.f); t.assertEqual("G",dojox.wire.ml.tests.markup.Data.target.g); |
相信大家通过前面的例子也能够看到,声明式 Wire 中用于数据关联匹配的对象为“dojox.wire.ml.Transfer”。之前大家也看到了一些简单的 Transfer 对象,接下来我们再来看几个稍微复杂一点的 Transfer:
清单 26. 复杂Transfer
<div dojoType="dojox.wire.ml.Action" triggerTopic="transferData"> <div dojoType="dojox.wire.ml.Transfer" source="dojox.wire.ml.tests.markup.Transfer.source.a" target="dojox.wire.ml.tests.markup.Transfer.item" targetStore="dojox.wire.ml.tests.markup.Transfer.store" targetAttribute="y"></div> <div dojoType="dojox.wire.ml.Transfer" source="dojox.wire.ml.tests.markup.Transfer.item" sourceStore="dojox.wire.ml.tests.markup.Transfer.store" sourceAttribute="y" target="dojox.wire.ml.tests.markup.Transfer.target.a"></div> </div> <div dojoType="dojox.wire.ml.Action" triggerTopic="transferXml"> <div dojoType="dojox.wire.ml.Transfer" source="dojox.wire.ml.tests.markup.Transfer.source.a" target="dojox.wire.ml.tests.markup.Transfer.element" targetPath="y/text()"></div> <div dojoType="dojox.wire.ml.Transfer" source="dojox.wire.ml.tests.markup.Transfer.element" sourcePath="y/text()" target="dojox.wire.ml.tests.markup.Transfer.target.a"></div> <div dojoType="dojox.wire.ml.Transfer" source="dojox.wire.ml.tests.markup.Transfer.source.b" target="dojox.wire.ml.tests.markup.Transfer.element" targetPath="y/@z"></div> <div dojoType="dojox.wire.ml.Transfer" source="dojox.wire.ml.tests.markup.Transfer.element" sourcePath="y/@z" target="dojox.wire.ml.tests.markup.Transfer.target.b"></div> </div> |
以上两段代码分别为 Data 数据类型的显示 Wire 和 XML 类型的显示 Wire,大家可以仔细看看里面的参数设置,是不是有一种似曾相识的感觉,不错,这里的设置与我们之前介绍的 DataWire 和 XMLWire 基本一致,不同的仅仅是这里通过 HTML 显示声明这种关联。
我们再来看看一下两个示例:
清单 27. 复杂Transfer 进阶
<div dojoType="dojox.wire.ml.Transfer" triggerTopic="transferTable" source="dojox.wire.ml.tests.markup.Transfer.source.c" target="dojox.wire.ml.tests.markup.Transfer.target.a"> <div dojoType="dojox.wire.ml.ColumnWire" column="b" property="d"></div> <div dojoType="dojox.wire.ml.ColumnWire" column="c" property="e"></div> </div> <div dojoType="dojox.wire.ml.Transfer" triggerTopic="transferTree" source="dojox.wire.ml.tests.markup.Transfer.source.c" target="dojox.wire.ml.tests.markup.Transfer.target.a"> <div dojoType="dojox.wire.ml.NodeWire" titleProperty="d"> <div dojoType="dojox.wire.ml.NodeWire" titleProperty="e"></div> </div> </div> |
是不是又有一种似曾相识的感觉?不错,它就是我们之前介绍的 TableAdapter 和 TreeAdapter 的 HTML 版本。这里的“ColumnWire”和“NodeWire”对象正是对应着之前脚本模式下面的“columns”参数和“nodes”参数。注意:并非只有 Action 可以定义起始触发事件,Transfer 本身也可以定义起始触发事件。
使用进阶
接下来我们会介绍一些更为新颖的用法,先从 Invocation 入手,Invocation(dojox.wire.ml.Invocation)是一个专门用来触发事件或者发布 topic 的 widget,来看看如下一组示例:
清单 28. Invocation 示例
dojox.wire.ml.tests.markup.Invocation = { invoke: function(p1,p2){return p1 + p2;}, invokeError: function(p){throw new Error(p);}, parameters: {a: "A",b: "B",c: "C"} }; <div dojoType="dojox.wire.ml.Invocation" triggerTopic="invokeMethod" object="dojox.wire.ml.tests.markup.Invocation" method="invoke" parameters= "dojox.wire.ml.tests.markup.Invocation.parameters.a, dojox.wire.ml.tests.markup.Invocation.parameters.b" result="dojox.wire.ml.tests.markup.Invocation.result"></div> dojo.publish("invokeMethod"); t.assertEqual("AB",dojox.wire.ml.tests.markup.Invocation.result); |
看中间那段代码,这里定义了一个触发器,一旦我们发布 topic -- “invokeMethod”,系统便会调用“dojox.wire.ml.tests.markup.Invocation”对象的“invoke”方法,并将“parameters”后面的参数当作函数实参传入,并将结构返回到“dojox.wire.ml.tests.markup.Invocation.result”对象中。这种触发器在操作一些大型数据源时非常有用。
接下来我们来看一个使用 Invocation 的 DataStore 示例:
清单 29. 基于Invocation 的DataStore 的操作
//code block 1 dojox.wire.ml.tests.markup.DataStore = { request: {onComplete: function(){},onError: function(){}} }; //code block 2 <div dojoType="dojox.wire.ml.DataStore" id="DataStore1" storeClass="dojox.data.XmlStore" url="DataStore.xml"></div> //code block 3 <div dojoType="dojox.wire.ml.Invocation" triggerTopic="invokeFetch" object="DataStore1" method="fetch" parameters="dojox.wire.ml.tests.markup.DataStore.request"> </div> //code block 4 <div dojoType="dojox.wire.ml.Transfer" trigger="dojox.wire.ml.tests.markup.DataStore.request" triggerEvent="onComplete" source="arguments[0]" sourceStore="DataStore1.store" target="dojox.wire.ml.tests.markup.DataStore.target"> <div dojoType="dojox.wire.ml.ColumnWire" column="a" attribute="x"></div> <div dojoType="dojox.wire.ml.ColumnWire" column="b" attribute="y"></div> <div dojoType="dojox.wire.ml.ColumnWire" column="c" attribute="z"></div> </div> |
可能这段代码不是很容易理解,我们来一一解读:
1. 先看第一段代码,我们定义了一个“DataStore”对象,它包含“request”属性及其中的“onComplete”和“onError”方法。
2. 再来看第二段,这里是定义了一个XML 数据源“DataStore.xml”,标识(id)为:DataStore1。
3. 第三段则定义了一个 Invocation,当我们触发“invokeFetch”的 topic 时,便会触发 DataStore1 通过“fetch”方法去有条件的去服务端去数据,并以“dojox.wire.ml.tests.markup.DataStore.request”作为传入参数。当数据返回后,便会调用“dojox.wire.ml.tests.markup.DataStore.request”对象的“onComplete”方法,并将返回值作为函数的实参传入。
4. 最后定义了一个 Transfer 对象,这个对象是当“dojox.wire.ml.tests.markup.DataStore.request”对象的“onComplete”方法调用时便会触发关联,将“DataStore1.store”取值的返回值的第 0 个参数“arguments[0]”作为数据源,关联到“dojox.wire.ml.tests.markup.DataStore.target”对象上,通过表格关联的方式。
所以,我们可以看到,看似非常复杂的逻辑流程,其实是可以通过清晰的 HTML 定义来实现的。
通过如下代码可以了解该关联的触发和验证过程:
清单 30. DataStore 操作的触发和验证
dojo.connect(dojox.wire.ml.tests.markup.DataStore.request,"onComplete",function(){ t.assertEqual("X1",dojox.wire.ml.tests.markup.DataStore.target[0].a); t.assertEqual("Y2",dojox.wire.ml.tests.markup.DataStore.target[1].b); t.assertEqual("Z3",dojox.wire.ml.tests.markup.DataStore.target[2].c); d.callback(true); }); dojo.connect(dojox.wire.ml.tests.markup.DataStore.request,"onError",function(error){ d.errback(error); }); dojo.publish("invokeFetch"); |
最后,我们来介绍一下最为复杂的一种新用法:“Service”,它其实是 RPC 的一种应用(有兴趣的读者可以先去看看 Dojo 的 RPC 包“dojox.rpc”了解一下),来看一下在 Wire 是如何使用它的:
清单 31. Service 示例
//code block 1 dojox.wire.ml.tests.markup.Service = { query: {name: "a"} }; //code block 2 <div dojoType="dojox.wire.ml.Service" id="Service1" url="Service/XML.smd"></div> //code block 3 <div dojoType="dojox.wire.ml.Invocation" id="Invocation1" triggerTopic="invokeGetXml" object="Service1" method="get" parameters="dojox.wire.ml.tests.markup.Service.query"> </div> //code block 4 <div dojoType="dojox.wire.ml.Transfer" trigger="Invocation1" triggerEvent="onComplete" source="arguments[0].item.name" target="dojox.wire.ml.tests.markup.Service.target.a"></div> //code block 5 dojo.connect(dijit.byId("Invocation1"),function(result){ t.assertEqual("a",dojox.wire.ml.tests.markup.Service.target.a); var o = result.toObject(); t.assertEqual("a",o.item.name); // test XmlElement.toObject() t.assertEqual("b",o.item.data); // test XmlElement.toObject() d.callback(true); }); dojo.connect(dijit.byId("Invocation1"),function(error){ d.errback(error); }); dojo.publish("invokeGetXml"); |
这段代码与之前的 DataStore 操作的是非常相似的,我们来一一解读:
1. 第一段代码我们声明了一个 Service 变量,用于之后的查询参数。
2. 第二段代码我们声明了一个 Service,指向“Service/XML.smd”数据源(smd 实为一段数据取值方式的描述),标识为“Service1”。
3. 声明一个 Invocation 用于调用“Service1”的“get”方法来取值,并以之前定义的变量“dojox.wire.ml.tests.markup.Service.query”作为传入参数。
4. 定义 Transfer 对象,以实现当“onComplete”被调用时关联数据源“arguments[0].item.name”到目标数据“dojox.wire.ml.tests.markup.Service.target.a”上。
5. 最后,第五段代码用来验证之前的声明式关联的正确与否。
Service 还支持参数的传递,你可以通过参数来决定使用哪个 smd 文件。
Wire 应用实例
最后,我们通过一个实例来看看 Wire 的用途,下面是一个输入框同步的案例,见下图:
图 1. 同步输入框(actionchaining)
由图可见,在编辑第一个输入框时,第二个和第三个输入框随即变化,这是一个动作链,在 Dojo 的 Wire 包的支持下,只需要做一些简单的配置即可实现。参考如下代码:
清单 32. 同步输入框代码示例
//code block 1 <div dojoType="dijit.form.TextBox" id="inputField" value="" size="50" intermediateChanges="true"></div> <div dojoType="dijit.form.TextBox" id="targetField1" value="" disabled="true" size="50"></div> <div dojoType="dijit.form.TextBox" id="targetField2" value="" disabled="true" size="50"></div> //code block 2 <div dojoType="dojox.wire.ml.Data" id="data"> <div dojoType="dojox.wire.ml.DataProperty" name="tempData" value=""> </div> <div dojoType="dojox.wire.ml.DataProperty" name="value" value="value"> </div> </div> //code block 3 <div dojoType="dojox.wire.ml.Action" id="action1" trigger="inputField" triggerEvent="onChange"> <div dojoType="dojox.wire.ml.Transfer" source="inputField.value" target="data.tempData"></div> <div dojoType="dojox.wire.ml.Invocation" id="targetCopy" object="targetField1" method="set" parameters="data.value,data.tempData"></div> </div> //code block 4 <div dojoType="dojox.wire.ml.Action" id="action2" trigger="targetCopy" triggerEvent="onComplete"> <div dojoType="dojox.wire.ml.Transfer" source="targetField1.value" target="targetField2.value"></div> </div> |
我们来一一解读:
1. 第一段代码主要是三个输入框的声明:inputField,targetField1,targetField2。
2. 第二段是一个声明式数据,标识为“data”,有两个属性:“tempData”和“value”,其值分别为空值和“value”。
3. 第三段便开始声明动作链了,这里声明了动作链中的第一个动作,标识为“action1”。它的触发源为“inputField”,即第一个输入框。触发动作为“onChange”,这个方法相信大家不陌生了。然后我们重点来看看它的 Transfer:在用户输字符入后,会触发输入框的“onChange”事件,进而实现这里的数据关联,即:输入值“inputField.value”会立即写入临时数据“data.tempData”中。然后,“onChange”时间还会启动这里的 Invocation,使得“targetField1”这个 widget 调用“set”方法,第一个实参为“data.value”变量(其值为 value),第二个实参则为“data.tempData”,也就是刚才的输入值,相当于调用了“targetField1.set('value',data.tempData)”。所以通过这个关联,我们实现了第一个输入框和第二个输入框内容的同步。
4. 第四段声明了动作链中的第二个动作,即当之前的 Invocation 对象“targetCopy”的“onComplete”调用时,会触发第二个输入框(targetField1)和第三个输入框(targetField2)的同步。“onComplete”会在 targetField1 的 set 方法调用完毕且返回后,立即调用。
至此,我们的动作链便构造完成。当然,Dojo 里面还有很多关于 Wire 的应用实例,这里由于篇幅的限制,不在进一步深入,有兴趣的读者可以自己深入了解一下 Dojo 的 Wire 包里面的案例,还有它的“demo”目录下的实例。
Dojo 的 Wire 工具包主要用在针对大型数据(如大型JSON,XML 等等)的维护和管理上。在很多情况下,我们只需要操作这个大型数据的某一小块,比如我们只需要获取或修改产品数据里面的价格,而不需要访问任何其它产品信息(产量,重量,体积等等),而这个时候我们又只能取得该大型产品数据,检索出我们要的价格信息并作相应的修改。如果我们用 Wire 事先绑定可能经常用到的产品价格数据,我们不就拥有了直接通往关键信息的钥匙吗?
结束语
这篇文章介绍了 Dojo 开发中关于 Wire 的工具包,从基本的 Wire 接口作为切入点,进而谈及到复杂的 Wire,混合类型的 Wire(CompositeWire),复杂数据源的 Wire 到基于 Path 的 XMLWire。然后有介绍了基于 CompositeWire 的 Adapter,用于格式化检索的数据。然后,深入的介绍了一下声明性的数据 Wire,从 Action,Transfer 两个方向说明了声明型的 Wire 的使用方式,并基于 Action 和 Transfer 阐述了一下 Invocation,DataStore 以及 Service 的使用方式,并和脚本方式的 Wire 做了比较。最后基于一个应用实例说明了 Action 链的构造和使用方式。这些内容我们可以在开发过程中多关注一下,以尽可能多的完善我们的 Web 应用。