代码改变世界

ASP.NET AJAX客户端编程之旅(四)——以组件的思想开发Ajax应用:客户端组件初探

2008-07-28 11:03  T2噬菌体  阅读(5370)  评论(7编辑  收藏  举报

摘要

      本文将首先介绍ASP.NET AJAX的组件编程技术,然后剖析一段ASP.NET AJAX客户端组件的源代码,借此来搞清楚客户端组件到底是怎么回事。最后介绍各种组件及其关系。而在下一篇中,将介绍行为组件、绑定技术和xml-script。

 

以组件的思想重新审视Ajax客户端开发

      在进入主题之前,我想和大家一起再来看一下本文章系列第一篇中的一个Demo:ASPNETAJAXTest。我现在将其中的客户端代码重新贴在这里。

 

Default.aspx:

 1<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
 2
 3<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
 4<html xmlns="http://www.w3.org/1999/xhtml">
 5<head runat="server">
 6    <title>Untitled Page</title>
 7</head>
 8<body>
 9    <form id="form1" runat="server">
10        <asp:ScriptManager ID="ScriptManager1" runat="server">
11            <Scripts>
12                <asp:ScriptReference Path="~/ajax.js" />
13            
</Scripts>
14            <Services>
15                <asp:ServiceReference Path="~/SayHelloService.asmx" />
16            </Services>
17        </asp:ScriptManager>
18        <div>
19            <input id="btnSayHello" type="button" value="SayHello" onclick="btnSayHello_onClick()" />
20            <div id="result"></div>
21        </div>
22    </form>
23</body>
24</html>

 

ajax.js:

 1//单击btnSayHello时调用的JS函数
 2function btnSayHello_onClick()
 3{
 4    SayHelloService.SayHello(OnSucceeded,OnFailded);
 5}

 6
 7//成功时的回调函数
 8function OnSucceeded(reusltText)
 9{
10    $get("result").innerHTML=reusltText;
11}

12
13//失败时的回掉函数
14function OnFailded(error)
15{
16    $get("result").innerHTML="调用失败。错误信息:"+error.get_message();
17}

 

      这是一种我们很熟悉的JavaScript编程模式:以DOM操作为基础。在这种编程模式下,所有页面元素被看成一个树状的DOM元素集合,不论是取得数据还是改变页面元素的属性,都要使用相应的DOM操作。例如我们要取得某个文本框中的值,则首先使用document.getElementById(在ASP.NET AJAX框架里可以缩写为$get)方法取得这个文本框的DOM引用,然后获取其value属性的值。

      这种编程模式,很容易给我们造成困惑,例如文本框、复选框、提交按钮、普通按钮的DOM表示都是“input”元素,另外还要使用如innerHTML这样不是很直观的名字设置div或span的值。其实对于开发人员,尤其是长期从事服务器端开发的人员来说,我们更希望将文本框看做TextBox,将复选框看做CheckBox,将按钮看做Button,将div或span看做Label,而且,我们更希望使用label1.text这样的语句设置div中的文本,而不是使用innerHTML。

      幸运的事,ASP.NET AJAX的开发人员显然考虑到了这一点,现在ASP.NET AJAX框架允许我们使用类似服务器端那种组件式的编程模式来进行客户端编程。

下面我们来看一下使用组件思想重新编写的这个应用。首先看一下aspx页面:

 

Default.aspx:

 1<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
 2
 3<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
 4<html xmlns="http://www.w3.org/1999/xhtml">
 5<head runat="server">
 6    <title>Untitled Page</title>
 7</head>
 8<body>
 9    <form id="form1" runat="server">
10        <asp:ScriptManager ID="ScriptManager1" runat="server">
11            <Scripts>
12                <asp:ScriptReference Assembly="Microsoft.Web.Preview" Name="PreviewScript.js" />
13                <asp:ScriptReference Path="~/ajax.js" />
14            
</Scripts>
15            <Services>
16                <asp:ServiceReference Path="~/SayHelloService.asmx" />
17            </Services>
18        </asp:ScriptManager>
19        <div>
20            <input id="btnSayHello" type="button" value="SayHello" />
21            <div id="result"></div>
22        </div>
23    </form>
24</body>
25</html>

 

      这里有两处变化,一是在ScriptManager控件里增加了对PreviewScript.js的引用。这里要注意,客户端组件的内容并不包含在ASP.NET AJAX1.0正式版里,而是包换在ASP.NET AJAX Futures CTP部分。所以,要使用这些功能,首先要添加对Microsoft.Web.Preview.dll文件的引用,这个文件在ASP.NET AJAX Futures CTP的安装目录里,然后要在页面中添加对PreviewScript.js文件的引用。

      第二个变化是这里的btnSayHello已经没有了onclick属性,那么如何知道单击这个按钮的时候需要执行何种代码呢?答案在js文件里。

 

ajax.js:

 1var btnSayHello;
 2var lblResult;
 3
 4Sys.Application.add_init(onPageInit);
 5
 6function onPageInit()
 7{
 8    btnSayHello=new Sys.Preview.UI.Button($get("btnSayHello"));
 9    btnSayHello.initialize();
10    lblResult=new Sys.Preview.UI.Label($get("result"));
11    lblResult.initialize();
12    btnSayHello.add_click(btnSayHello_onClick);
13}

14
15function btnSayHello_onClick()
16{
17    SayHelloService.SayHello(OnSucceeded,OnFailded);
18}

19
20function OnSucceeded(resultText)
21{
22    lblResult.set_text(resultText);
23}

24
25function OnFailded(error)
26{
27    lblResult.set_text("调用失败。错误信息:"+error.get_message());
28}

 

      我们看到,应用组件编程思想后,JavaScript发生了巨大的改变。不着急,我们一步一步解析这个文件。

      最顶上是定义了两个全局变量,这两个变量将分别存储对btnSayHello和result的引用。之所以要定义成全局变量,是方便在整个文件中调用两个控件。

      Sys.Application.add_init(onPageInit);的作用是告诉页面当页面初始化时执行名为onPageInit的函数。

      onPageInit是一个自定义函数,主要完成客户端控件的创建、初始化工作。以下面两行代码为例:

            btnSayHello=new Sys.Preview.UI.Button($get("btnSayHello"));
            btnSayHello.initialize();

       第一行我想大多数人一看就知道是什么意思,因为那个“new”实在太亲切了,这正是创建一个Button的实例,并将其赋给btnSayHello变量。其中Sys.Preview.UI.Button是Button的完全限定名,而大多数控件的构造函数都需要一个参数,用来指出这个控件要关联到的DOM元素。而第二行是必须的,在实例化一个控件后,最好马上调用initialize方法,避免一些奇怪的情况发生。

            btnSayHello.add_click(btnSayHello_onClick);

       上面这行代码是将控件的单击事件与btnSayHello_onClick这个函数关联起来。这里我要专门说一下ASP.NET AJAX客户端控件的属性及事件的设置方法。

      ASP.NET AJAX框架规定,在获取一个控件的属性时,应使用“控件名.get_属性名()”这种方法,而设置时则是“控件名.set_属性名()”。为一个控件的某个事件添加监听函数时,应该用“控件名.add_事件名()”,移除时用“控件名.remove_事件名()”。这是ASP.NET AJAX强制执行的命名规范,所有的客户端控件都遵从这个规则,以后我们在开发自己的客户端组件时,也应该遵从这个规则。

      知道了以上知识,很多代码就很好理解了。例如“lblResult.set_text(resultText);”就是将lblResult控件的text属性设置为resultText。剩下的代码我就不解释了,应该没问题了。

 

剖析客户端组件源代码

      看了上文,不知各位朋友是否有点云里雾里。那么下面,我们一起来剖析一段ASP.NET AJAX客户端组件的源代码,看看客户端组件到底是什么。这里要剖析的是Button控件的源代码,这段代码位于PreviewScript.debug.js里,代码如下:

 

  1Sys.Preview.UI.Button = function Sys$Preview$UI$Button(associatedElement) {
  2    /// <param name="associatedElement" domElement="true"></param>
  3    var e = Function._validateParams(arguments, [
  4        {name: "associatedElement", domElement: true}
  5    ]);
  6    if (e) throw e;
  7
  8    Sys.Preview.UI.Button.initializeBase(this, [associatedElement]);
  9}

 10
 11    function Sys$Preview$UI$Button$get_argument() {
 12        /// <value type="String" mayBeNull="true"></value>
 13        if (arguments.length !== 0throw Error.parameterCount();
 14        return this._arg;
 15    }

 16
 17    function Sys$Preview$UI$Button$set_argument(value) {
 18        var e = Function._validateParams(arguments, [{name: "value", type: String, mayBeNull: true}]);
 19        if (e) throw e;
 20
 21        if (this._arg !== value) {
 22            this._arg = value;
 23            this.raisePropertyChanged('argument');
 24        }

 25    }

 26
 27    function Sys$Preview$UI$Button$get_command() {
 28        /// <value type="String" mayBeNull="true"></value>
 29        if (arguments.length !== 0throw Error.parameterCount();
 30        return this._command;
 31    }

 32
 33    function Sys$Preview$UI$Button$set_command(value) {
 34        var e = Function._validateParams(arguments, [{name: "value", type: String, mayBeNull: true}]);
 35        if (e) throw e;
 36
 37        if (this._command !== value) {
 38            this._command = value;
 39            this.raisePropertyChanged('command');
 40        }

 41    }

 42
 43    function Sys$Preview$UI$Button$initialize() {
 44        Sys.Preview.UI.Button.callBaseMethod(this'initialize');
 45        this._clickHandler = Function.createDelegate(thisthis._onClick);
 46        $addHandler(this.get_element(), "click"this._clickHandler);
 47    }

 48
 49    function Sys$Preview$UI$Button$dispose() {
 50        if (this._clickHandler) {
 51            $removeHandler(this.get_element(), "click"this._clickHandler);
 52        }

 53        Sys.Preview.UI.Button.callBaseMethod(this'dispose');
 54    }

 55
 56    function Sys$Preview$UI$Button$add_click(handler) {
 57        var e = Function._validateParams(arguments, [{name: "handler", type: Function}]);
 58        if (e) throw e;
 59
 60        this.get_events().addHandler("click", handler);
 61    }

 62
 63    function Sys$Preview$UI$Button$remove_click(handler) {
 64        var e = Function._validateParams(arguments, [{name: "handler", type: Function}]);
 65        if (e) throw e;
 66
 67        this.get_events().removeHandler("click", handler);
 68    }

 69
 70    function Sys$Preview$UI$Button$_onClick() {
 71        var handler = this.get_events().getHandler("click");
 72        if (handler) {
 73            handler(this, Sys.EventArgs.Empty);
 74        }

 75
 76        if (this._command) {
 77            this.raiseBubbleEvent(thisnew Sys.Preview.UI.CommandEventArgs(this._command, this._arg));
 78        }

 79    }

 80Sys.Preview.UI.Button.prototype = {
 81
 82    _command: null,
 83    _arg: null,
 84    _clickHandler: null,
 85    
 86    get_argument: Sys$Preview$UI$Button$get_argument,
 87    
 88    set_argument: Sys$Preview$UI$Button$set_argument,
 89    
 90    get_command: Sys$Preview$UI$Button$get_command,
 91    
 92    set_command: Sys$Preview$UI$Button$set_command,
 93    
 94    initialize: Sys$Preview$UI$Button$initialize,
 95
 96    dispose: Sys$Preview$UI$Button$dispose,
 97    
 98    add_click: Sys$Preview$UI$Button$add_click,
 99
100    remove_click: Sys$Preview$UI$Button$remove_click,
101    
102    _onClick: Sys$Preview$UI$Button$_onClick
103}

104    
105Sys.Preview.UI.Button.descriptor = {
106    properties: [ { name: 'command', type: String },
107                  { name: 'argument', type: String } ],
108    events: [ { name: 'click' } ]
109}

110
111Sys.Preview.UI.Button.registerClass('Sys.Preview.UI.Button', Sys.UI.Control);

 

      乍看之下,代码很复杂,但是如果熟悉JavaScript语言的话,读懂这段代码应该是不成问题的。这里只拣关键的地方解析。

      最开始是定义构造函数,其具体内容不必细究,只需看到,它需要一个参数associatedElement,即与这个控件想关联的DOM元素。接着定义了两个属性:argument和command,可以看到,它们是符合“get_ set_”规范的。再接下来,是初始化函数和析构函数,再后面,则定义了onclick事件,这个事件也是遵循“add_ remove”语法规范的。接着通过prototype将各个函数与Button组件关联起来,而最后的“Sys.Preview.UI.Button.registerClass('Sys.Preview.UI.Button', Sys.UI.Control);”则是将Button注册成为一个组件,其完全先定名叫做“'Sys.Preview.UI.Button”,而其继承自“Sys.UI.Control”类。

      通过以上简单分析,我们可以发现,所谓的客户端组件,实际上就是封装了DOM元素,用一种我们熟悉的语法封装了晦涩的DOM操作。例如,TextBox的text属性,其实是封装了type属性为“text”的input元素的value属性。而Button的click事件,其实也就是封装了type属性为button的input元素的onclick事件。所以,从某种层面上来说,客户端组件其实就是一种“语法糖”,它将晦涩怪异的DOM操作封装成我们熟悉的控件式语法。

 

探寻客户端组件的家谱

      不知各位朋友有没有发现,在上文中,我有时用“组件”这个词,而有时又用“控件”这个词。那么,“客户端组件”和“客户端控件”一样吗?严格来说,不一样,客户端控件应该算作客户端组件的一部分。其实,ASP.NET AJAX为我们提供的客户端组件非常丰富,它不仅包含了文本框、复选框、按钮等这些看得见的东西(一般这些看得见的东西可以叫做控件),还有一部分看不见的组件。如有一种组件叫“行为组件”,它不是一个具体的控件,而是为控件添加一种行为。例如,一般div是没有onclick事件的,但是如果我们给一个Label控件添加一个“ClickBehavior”组件,则它就可以拥有单击事件,另外常用的行为组件还有使元素具有透明度的“OpacityBehavior”等。

      还有一种组件,叫做“绑定组件”,它更不是一种具体的控件,而是用来描述多个控件间属性相互绑定的一种关系。另外还有“动画组件”,它可以给元素添加动画效果。

 

 

 

      上图展示了一个不完整的ASP.NET AJAX客户端组件家谱。由于这个家族非常庞大,所以这幅图并没有完全把它们表示出来,而只是表示出一个骨架。从图上可以看出,所以组件均继承自Componet类。

      总体来说,ASP.NET AJAX客户端组件分为一下几类:

      *Control:基本UI组件,包括TextBox、Button等。

      *Behavior:行为组件,用于给组件添加行为。

      *Action:动作响应组件,响应特定的动作。

      *Animation:动画组件,用来给组件添加动画效果。

      *Data:数据组件,包括DataView等与数据显示有关的组件。

      *Validator:验证组件,用于用户输入数据的验证。

      *Drag:拖放组件,用于创建可拖放式的Ajax应用。

      *Bind:绑定组件,用于实现绑定技术。

      *Counter:客户端计数器。

      *Timer:客户端定时器,用于定时触发事件。

      这里就不对各种组件做一一介绍,具体请参考微软官方的Roadmap文档:http://www.asp.net/ajax/documentation/live

 

结束语

      通过本文,朋友们应该对组件思想进行JavaScript编程有了一个了解。并且通过分析,也明白了ASP.NET AJAX客户端组件的本质。当然,严格说,这里仅仅介绍了“控件”的本质,而其他组件则没有介绍,有兴趣的朋友可以读读它的源代码。最后我们从全景的方式看了看客户端组件的家谱。限于篇幅,不可能对每个组件进行详尽介绍,具体可以参考相关文档。在下一篇中,将通过实例介绍行为组件和绑定技术。并介绍一种新的JavaScript编程方式:xml-script。

      本文用到的例子可以在这里下载:ASPNETAJAXTest1.rar

 

主要参考文献

      [1] 陈黎夫,ASP.NET AJAX程序设计-第II卷:客户端,人民邮电出版社,2007年10月

      [2] ASP. NET AJAX Roadmap Documentation,http://www.asp.net/ajax/documentation/live