本文翻译自: http://dojotoolkit.org/documentation/tutorials/1.7/data_modeling/
MVC,即模型(Model),视图(View)和控制Controller),是当今应用开发的主导模式。这里,我们要从Dojo的基础功能出发,来看看Dojo是如何支持MVC应用的。通过这篇文章我们会了解到如何通过Dojo的object stores 和 Stateful objects(有状态的对象)来构建MVC应用,以及如何基于这些模型实现我们的显示层和控制层。
MVC应用的数据建模(基于Dojo)
MVC模式是当今应用开发的主导模式。该模式主要目的是组织严密的,易于管理的的代码结构。Dojo控件库自身的代码结构就是高度基于MVC模式的,同时也能很好的支持那些基于MVC模式的应用。一个好的MVC应用的核心是它能拥有一个完美的数据模式。接下来我们会介绍一下如何通过Dojo的Object stores和stateful objects(有状态对象)去构建一个包含view和controller的完美模式。
Model
Model层是MVC中的“M”,数据层指的是那些应用中需要访问和处理的核心数据信息,他是一个应用的中心,viewer和controller层则主要用来作为用户和Model(数据)层之间交互的一个桥梁。Model层则封装了存储和验证的过程。
Dojo的object store完美的担当了Dojo应用中Model的这一个角色。store的接口就是按照分离出数据层的思想来设计的。不同的存储媒介基于相同的store接口。Stores本身也支持扩展更为强大的功能。接下来我们来看看如何构建一个基本的store。我们将使用一个JsonRest的store,同时缓存住我们取到的数据:
require(["dojo/store/JsonRest","dojo/store/Memory","dojo/store/Cache","dojo/store/Observable"],function(JsonRest,Memory,Cache,Observable){ masterStore = new JsonRest({ target: "/Inventory/",idProperty: "name" }); cacheStore = new Memory({ idProperty: "name" }); inventoryStore = Cache(masterStore,cacheStore)
现在,我们的“inventoryStore”就成了我们的数据层,我们可以通过“get()”取数据,“query()”查询数据,“put()”修改数据。store这里封装了对真实数据的存储过程,包括与服务器(server)端的交互。
@H_403_27@ @H_403_27@ @H_403_27@
我们的视图层(viewer)便可以开始查询结果了:
results = inventoryStore.query("some-query"); viewResults(results); // pass the results on to the view function viewResults(results){ var container = dom.byId("container"); // results object provides a forEach method for iteration results.forEach(addRow); function addRow(item){ var row = domConstruct.create("div",{ innerHTML: item.name + " quantity: " + item.quantity },container); } }
我们的
viewResults
在这里就是充当了数据层的一个viewer。我们还能够通过dojo/string的substitute方法实现简单的模板化视图。
@H_403_27@ @H_403_27@ @H_403_27@
function addRow(item){ var row = domConstruct.create("div",{ innerHTML: string.substitute(tmpl,item); },container); }@H_403_27@ @H_403_27@ @H_403_27@
数据绑定
MVC里面比较重要的一块就是view层应该监听数据层的改动,然后及时的反应在界面上。这种方式避免了用controller去监听数据层的无畏的资源开销。Controller只需要更新model层,viewer会自动将该改动反映到应用的界面上。我们能够通过dojo/store/Observable来实现这一点。
masterStore = Observable(masterStore); ... inventoryStore = Cache(masterStore,cacheStore);@H_403_27@ @H_403_27@ @H_403_27@现在我们的viewer能够通过observe的方式来监听查询结果的变化了
function viewResults(results){ var container = dom.byId("container"); var rows = []; results.forEach(insertRow); results.observe(function(item,removedIndex,insertedIndex){ // this will be called any time a item is added,removed,and updates if(removedIndex > -1){ removeRow(removedIndex); } if(insertedIndex > -1){ insertRow(item,insertedIndex); } },true); // we can indicate to be notified of object updates as well function insertRow(item,i){ var row = domConstruct.create("div",{ innerHTML: item.name + " quantity: " + item.quantity }); rows.splice(i,container.insertBefore(row,rows[i])); } function removeRow(i){ domConstruct.destroy(rows.splice(i,1)[0]); } }@H_403_27@ @H_403_27@ @H_403_27@
现在我们viewer已经能够即时的反映model数据的变化了,与此同时,我们的controller相关代码也能够基于用户的操作来对model数据作出相应的修改。Controller可以通过put()
,add()
,和 remove()
来修改数据。通常来讲,Controller相关代码主要用来处理事件,比如:当用户点击add按钮时,我们便创建一个新的数据对象。
on(addButton,"click",function(){ inventoryStore.add({ name: "Shoes",category: "Clothing",quantity: 40 }); });@H_403_27@ @H_403_27@ @H_403_27@该行为会直接更新我们的应用界面(viewer),我们无需再用额外的代码去直接操作我们的界面了。这个时候我们的Controller就只需要基于用户的操作来修改model数据了。此刻,Model数据层的存储和view层的更新渲染就与我们的逻辑完全隔离开来了
数据模型进阶
之前我们用到的store都非常简单,没有包含任何逻辑(可能服务端会包含一些逻辑和验证)。我们其实可以在不影响其他模块的同时对store加入一些额外的功能。
验证
验证功能便可作为store的一个扩展,这个扩展对JsonRestStore来说非常简单,因为所有的更新都会调用put()
方法(add()
会调用 put()
),我们只需要扩展一下put方法即可。
var oldPut = inventoryStore.put; inventoryStore.put = function(object,options){ if(object.quantity < 0){ throw new Error("quantity must not be negative"); } // now call the original oldPut.call(this,object,options); };@H_403_27@ @H_403_27@ @H_403_27@此时,验证逻辑已经被加入了。
inventoryStore.put({ name: "Donuts",category: "Food",quantity: -1 });@H_403_27@ @H_403_27@ @H_403_27@由于quantity的值小于0,所以此时该方法会抛出异常。
Hierarchy层次结构
如同我们给我们的数据模型加入逻辑功能一样,我们也为元数据加入了一些特有的含义,其中之一就是层次结构。object store定义了getChildren()
方法是我们能够实现我们的父---子结构。存放这种父---子结构有很多种方式。
stored objects可以存放一个指向其所有子对象的数组引用。这种做法适用于小的,顺序的列表数据。同样,objects也可以存放一个指向其父对象的引用,这种做法伸缩性更强。
为了实现第二种数据结构,我们可以加入getChildren()
方法。在如下示例中,我们的构造的层次结构来源于我们的含有独立子对象的实例,我们创建getChildren()
方法用于找到所有category属性值为该父对象名称的对象集合,这些对象就是该父对象的所有子对象。这就是将父/子关系定义为子对象的一个属性的解决方案。
inventoryStore.getChildren = function(parent,options){ return this.query({ category: parent.name },options); };现在,我们可以不用管数据的内部结构,而直接通过
getChildren()
获取父对象的所有子节点,检索子节点方式如下:
@H_403_27@
@H_403_27@
@H_403_27@
require(["dojo/_base/Deferred"],function(Deferred){ Deferred.when(inventoryStore.get("Food"),function(foodCategory){ // retrieved the food category object,now get it's children inventoryStore.getChildren(foodCategory).forEach(function(food){ // handle each item in the food category }); });至此,我们能取得子节点的对象,接下来我们来看看如何修改他们。我们知道category定义了数据的层次结构,如果我们改变某个对象元素的层次结构,我们只需要修改category的属性值即可。 @H_403_27@ @H_403_27@ @H_403_27@
donut.category = "Junk Food"; inventoryStore.put(donut);Dojo stores的一个核心思想就是提供统一的数据操作接口。如果我们想简单的通过设定父对象的方式来定义对象的层次结构,我们可以在
put()
方法里使用
options
参数集里的
parent
属性。
@H_403_27@
@H_403_27@
@H_403_27@
inventoryStore.put = function(object,options){ if(options.parent){ object.category = parent; } // ... };现在我们可以设置父对象了。 @H_403_27@ @H_403_27@ @H_403_27@
inventoryStore.put(donut,{parent: "Junk Food"});
@H_403_27@ @H_403_27@ @H_403_27@
有序的Store
默认情况下,一个store通常是一群无序对象的集合。尽管如此,我们还是可以实现store的有序排列,尤其是对象集合中已经存在预留的隐含的序列属性时。实现有序store的第一个工作就是在调用query()
方法后返回一个有序的store(当且仅当没有其他排序设定的影响)。实现它往往不需要对store作扩展,你只需要返回相应顺序的数据序列即可。
有序的Store通常还有一个需求,就是其元素能够前后移动,甚至直接到最前或者最后等等。我们可以通过在
put()
方法的options参数中加入before属性来实现。
inventoryStore.put = function(object,options){ if(options.before){ // we set the reference object's name in the object's "insertBefore" // so the server can put the object in the right order object.insertBefore = options.before.name; } // ... };服务器现在可以通过insertBefore属性来做排序了。我们的controller层的代码可以开始移动我们的对象了(我们使用事件代理并假设节点的
itemId
和
beforeId
已经在创建时设定了):
@H_403_27@
@H_403_27@
@H_403_27@
require(["dojo/on"],function(on){ on(moveUpButton,".move-up:click",function(){ // |this| in event delegation is the node // matching the given selector inventoryStore.put(inventoryStore.get(this.itemId),{ before: inventoryStore.get(this.beforeId) }); });
@H_403_27@ @H_403_27@ @H_403_27@
事物
事物是应用程序里面比较重要的一块,通常用于把一系列应用的逻辑操作绑定到一起,一次性执行。他的作用之一就是集合一系列操作然后通过单一请求一次性提交,示例如下:
require(["dojo/_base/lang"],function(lang){ lang.mixin(inventoryStore,{ transaction: function(){ // start a transaction,create a new array of operations this.operations = []; var store = this; return { commit: function(){ // commit the transaction,sending all the operations in a single request return xhr.post({ url:"/Inventory/",// send all the operations in the body postData: JSON.stringify(store.operations) }); },abort: function(){ store.operations = []; } }; },put: function(object,options){ // ... any other logic ... // add it to the queue of operations this.operations.push({action:"put",object:object}); },remove: function(id){ // add it to the queue of operations this.operations.push({action:"remove",id:id}); } });基于上述代码,我们可以利用事物来构建我们的自定义操作: @H_403_27@ @H_403_27@ @H_403_27@
removeCategory: function(category){ // atomically remove entire category and the items within the category var transaction = this.transaction(); var store = this; this.getChildren(category).forEach(function(item){ // remove each child store.remove(item.name); },this).then(function(){ // now remove the category store.remove(category.name); // all done,commit the changes transaction.commit(); }); }
@H_403_27@ @H_403_27@ @H_403_27@
数据绑定: dojo/Stateful
Dojo对集合层次和实体层次的数据模型有一个清晰的界限,Dojo的store提供了集合层次的模型,接下来我们来看看实体层次的对象模型。Dojo对独立的对象也使用了统一的接口。这里我们可以使用dojo/Stateful
的接口与对象交互。接口很简单,主要有三个方法:
get(name)
- 检索属性值set(name,value)
- 设置属性值watch(name,listener)
- 注册一个监听属性变化的callback方法(不设第一个参数则表示监听所有属性改动)
和之前介绍的viewer绑定数据一样,他能及时反映相应的数据变化。首先,我们先创建一个viewer,绑定到一个对象:
<form id="itemForm"> Name: <input type="text" name="name" /> Quantity: <input type="text" name="quantity" /> </form>然后,我们绑定到HTML: @H_403_27@ @H_403_27@ @H_403_27@
function viewInForm(object,form){ // copy initial values into form inputs for(var i in object){ updateInput(i,null,object.get(i)); } // watch for any future changes in the object object.watch(updateInput); function updateInput(name,oldValue,newValue){ var input = query("input[name=" + name + "]",form)[0]; if(input){ input.value = newValue; } } }现在我们可以通过store里面的一个对象初始化form: @H_403_27@ @H_403_27@ @H_403_27@
require(["dojo/Stateful","dojo/_base/Deferred"],function(Stateful,Deferred){ Deferred.when(store.get("Donut"),function(item){ item = new Stateful(item); // wrap with stateful viewInForm(item,dom.byId("itemForm")); });@H_403_27@ @H_403_27@ @H_403_27@
现在我们如果通过controller代码修改这个对象,在viewer上就会马上反映出来。
item.set("quantity",4);
在这个例子中,我们可能也想加入
onchange
事件用于监听用户输入进而修改对象本身,以达到一个双向的绑定。(object的改动会反映到form上,同样form的改动也会影响object)。Dojo的form manager(dojox.form.manager)同样也提供了很多高级的交互功能。
@H_403_27@
@H_403_27@
@H_403_27@
接下来我们还要记住将修改了的对象传回store里面,如下:
on(saveButton,function(){ inventoryStore.put(currentItem); // save the current state of the Stateful item });@H_403_27@ @H_403_27@ @H_403_27@
总结
通过使用Dojo的store架构和stateful(有状态的)接口,我们便有了构建我们MVC应用的利器。Viewers能监听数据的变化。Controllers能通过统一的接口来操作数据而不用知道数据的特殊结构,同时也不需要额外的代码来操作viewer的变化。集合和实体的接口边界清晰。所有这些都能帮助您构建您自己的MVC应用。