Sys.UI.Data.DataNavigator与Sys.UI.Data.SortBehavior
2006-09-28 16:28 by 老赵, 2807 visits这次我想讨论与分析的内容是Sys.UI.Data.DataNavigator与Sys.UI.Data.SortBehavior。在这之前,可能感兴趣的朋友们需要先了解一下Atlas Client Script中Data Control的使用方法。可以先学习一些别的文章,例如官方说明《Start > Quickstart Developer Tasks > Performing Data Access》,而Dflying兄的Atlas介绍系列也实属精品。
Sys.UI.Data.DataNavigator和Sys.UI.Data.SortBehavior的作用都是配合DataView进行数据操作:一个操作分页功能,一个操作排序功能。事实上这两个类都是属于辅助类,它们的功能是更加方便了开发,而提供的额外功能却不多。
Sys.UI.Data.SortBehavior使用了DataView的自带的属性与方法为DataView的内容进行了排序。当一个SortBehavior被应用到了一个控件时,它会根据控件的点击来决定DataView的排序顺序(在ASC和SESC之间切换),并且根据当前的排序顺序为control应用不同的样式(sortAscendingCssClass或sortDescendingCssClass)。如果没有这个控件,我们可能就需要配合使用setProperty,invokeMethod进行较为复杂的应用了。换个角度说,如果某个功能比较复杂并且需要复用的话,我们可以像SortBehavior一样封装起一系列操作。
Atlas中的控件设计并不随意,而是实实在在地“有用”。
但是,与Sys.UI.Data.SortBehavior相比,Sys.UI.Data.DataNavigator就有些古怪了。
Sys.UI.Data.DataNavigator提供了DataView的分页功能,不过无论是上一页也好,最后一页也罢,它的实现其实只是简单的使用了DataView的一个函数:set_pageIndex。配合get_pageIndex获得当前的页数,实现这些功能自不必说。不过非常有趣(或者继续使用上面的说法:古怪)的是触发DataNavigator那些功能的方式。DataNavigator没有提供任何的方法来触发它的那些操作,它的那些操作都是定义在DataNavigator的onBubbleEvent方法中。它的onBubbleEvent里判断了Sys.UI.Button对象传给它的Sys.UI.CommandEventArgs对象中commandName属性的值,然后进行不同的操作。
onBubbleEvent是我觉得在Atlas中比较奇特的玩意儿之一。它是定义在Sys.UI.Control类中的虚函数,直接或间接继承Sys.UI.Control的类可以对它进行重载。不过在代码中并不会调用onBubbleEvent,如果需要bubble event,那么就需要调用定义在Sys.UI.Control类中的raiseBubbleEvent方法。熟悉ASP.NET Server Control开发的朋友们应该可以发现这和ASP.NET中System.Web.UI.Control的OnBubbleEvent和RaiseBubbleEvent非常的类似,只可惜在Javascript中无法定义protected方法,只得把它们暴露在外了。
可以看出Atlas有多么深厚的ASP.NET背景。与ASP.NET一样,Atlas中的raiseBubbleEvent方法也不是个虚函数,它的定义无法改变,我们来看一下它的代码:
var currentTarget = this.get_parent();
while (currentTarget) {
if (currentTarget.onBubbleEvent(source, args)) {
return;
}
currentTarget = currentTarget.get_parent();
}
}
从当前控件开始,调用onBubbleEvent方法,如果返回false,则继续向parent传递,这样event就被bubble了。这是很标准的raiseBubbleEvent功能实现。
但是事情没有这么简单。在raiseBubbleEvent中可以看出,代码是通过parent属性获得上一级控件的引用。parent也是个无法重载的非虚成员,它定义在Sys.UI.Control中。不知道为什么,在官方的Client Library中缺少这个属性。我们来看一下它的实现。
if (_parent) {
return _parent;
}
else {
var parentElement = this.element.parentNode;
while (parentElement) {
if (parentElement.control) {
return parentElement.control;
}
parentElement = parentElement.parentNode;
}
return null;
}
}
可以看出这个属性的逻辑,如果曾经定义了parent,那么就返回parent引用,如果没有的话,那么就沿着DOM向父级元素找,找到第一个具有Atlas控件对象的元素。因此可以发现,parent并不需要被强制定义。为了证实这一点,在这里我借用一下Dflying兄在其文章《使用ASP.NET Atlas PageNavigator控件实现客户端分页导航》中编写的例子,我将对其进行简单的修改。
在Dflying兄的例子里有如下的定义:
<div id="pageNavigator">
<input type="button" id="btnFirstPage" value="<<" />
<input type="button" id="btnPrevPage" value="<" />
<span id="lblPageNumber"></span> / <span id="lblPageCount"></span>
<input type="button" id="btnNextPage" value=">" />
<input type="button" id="btnLastPage" value=">>" />
</div>
<script type="text/xml-script">
<page xmlns:script="http://schemas.microsoft.com/xml-script/2005">
<components>
...
<dataNavigator id="pageNavigator" dataView="view" />
<button id="btnFirstPage" parent="pageNavigator" command="FirstPage" />
<button id="btnPrevPage" parent="pageNavigator" command="PreviousPage">
...
</button>
<button id="btnNextPage" parent="pageNavigator" command="NextPage">
...
</button>
<button id="btnLastPage" parent="pageNavigator" command="LastPage" />
...
</components>
</page>
</script>
在这里,dataNavigator是那些<input type="button" />的父结点,而在Atlas Xml Scripts中也为每个button指定了自身的parent属性。我们先尝试着去除上方HTML定义的元素父子关系,保留下方Script不变:
<input type="button" id="btnFirstPage" value="<<" />
<input type="button" id="btnPrevPage" value="<" />
<span id="lblPageNumber"></span> / <span id="lblPageCount"></span>
<input type="button" id="btnNextPage" value=">" />
<input type="button" id="btnLastPage" value=">>" />
运行,一切正常。可以发现作为dataNavigator的元素无需为那些button的父结点。
接着,我们保留HTML元素的父子关系,在下方撤销在Atlas Scripts中的parent定义,如下:
<page xmlns:script="http://schemas.microsoft.com/xml-script/2005">
<components>
...
<dataNavigator id="pageNavigator" dataView="view" />
<button id="btnFirstPage"
<button id="btnPrevPage"
...
</button>
<button id="btnNextPage"
...
</button>
<button id="btnLastPage"
...
</components>
</page>
</script>
运行,依旧一切正常。可见,Atlas会自动在DOM树中寻找作为dataNavigator的control。感兴趣的朋友也可以尝试着将上述两者都去除,这样分页功能就会失效了。
不过,我的疑惑也就出现了……既然这样,为什么DataNavigator要设计成为一个控件?很明显,它只是提供了一个分页功能,而不具备任何显示作用!我们可以在任意一个地方定义任意一个元素作为所谓的“DataNavigator”,还是能够实现分页。从这一点上说,我们完全可以定义个类似于DataNavBehavior的东西来实现相同的效果。还有,为什么要使用bubble event的方式来作为实现DataNavigator的功能呢?纵观所有的Atlas控件,只有button一个类调用了raiseBubbleEvent方法,而在PageNavigator中直接把args参数作为CommandEventArgs对象来处理,这表明它已经作为一个特定实现而出现了。
有时候我想,DataNavigator的这个设计,莫非只是为了“演示”一下raiseBubbleEvent和onBubbleEvent的方式?事实上,DataNavigator只是集中了分页的功能,通过一个独立的对象来管理了操作,照这样说,我们可以为它提供相关方法或属性,然后依靠Button来invokeMethod或者setProperty。另外,熟悉ASP.NET Server Control开发的朋友们可以知道,在ASP.NET中RaiseBubbleEvent和OnBubbleEvent的作用是为了配合PostBack模型,在复合控件中将子控件的事件向父控件传递的一种方式。而在Client端,根本不需要这样的做法,所有的控件都是顶级元素,都能被直接访问并且注册事件,这样raiseBubbleEvent和onBubbleEvent方法似乎也就没有什么必要性了。
目前DataNavigator的设计,似乎只有在资源消耗上小一些。
不过现在,在Client端存在raiseBubbleEvent和onBubbleEvent的必要性,它们能否带来开发概念和方式上的改变,以及如何正确地使用它们,这些事情还在我脑子里激烈地进行着斗争,可能我以后会单独写一篇文章来仔细讨论这点。
似乎这片文章已经完成使命了,不过,事实上,在这里我还想提一下DataNavigator的一个bug。DataNavigator的部分代码如下:
2 Sys.UI.Data.DataNavigator.initializeBase(this, [associatedElement]);
3
4 ...
5
6 this.getDescriptor = function() {
7 var td = Sys.UI.Data.DataControl.callBaseMethod(this, 'getDescriptor');
8
9 td.addProperty('dataView', Object);
10
11 return td;
12 }
13 Sys.UI.Data.DataNavigator.registerBaseMethod(this, 'getDescriptor');
14
15 this.onBubbleEvent = function(source, args) {
16 ...
17 }
18 Sys.UI.Control.registerBaseMethod(this, 'onBubbleEvent');
19 }
20 Sys.UI.Data.DataNavigator.registerClass('Sys.UI.Data.DataNavigator', Sys.UI.Control);
看看第5行和第18行的代码,是不是觉得很奇怪?还好,由于DataNavigator是继承了Sys.UI.Control类,因此在运行效果上没有什么问题。幸甚!
哎,人气好差。到底是我真的写的不好,还是大家搞Atlas的热情退了呢?难以想象以前的火爆样啊……