探讨微软ASP.NET AJAX控件开发技术

本系列文章将通过具体的实例从客户端和服务端两个角度全面探讨ASP.NET AJAX框架中的控件(Control)开发所涉及的技术。

客户端

 一、 简介

  如今,ASP.NET AJAX框架以其与ASP.NET 2.0系统的有机整合与完全面向对象的客户端JavaScript组件模型正在吸引着越来越多的Web开发人员。此外,这个框架还为基于ASP.NET 2.0平台的AJAX Web开发提供了一揽子方案(尽管尚嫌稚气)。

  首先,我们来回忆ASP.NET AJAX框架设计的主要目标:其一,扩展现有的ASP.NET服务器端模型,让其能够生成支持富客户端的JavaScript代码;其二,为ASP.NET应用增加客户端编程模型,让纯粹的客户端编程变得更为容易。

  基于此,我们至少可以从如下两个方面来扩展ASP.NET AJAX框架。第一,创建更为丰富的ASP.NET AJAX服务器端扩展器控件,以便为这些控件提供丰富的客户端Ajax行为。此部分的极好例证就是AJAX Control Toolkit中的大部分控件。第二,创建纯粹的客户端组件/控件。这部分的例证就是框架已经实现的对于常规HTML元素的封装、高级客户端控件ListView及客户端离线数据源DataSource等。为此,控件开发者可以创建新的客户端组件/控件来封装典型的客户端功能,或者基于现有客户端组件/控件加以扩展。

  【说明】第一,事实上,我们可以进一步沿着ASP.NET AJAX客户端与服务器端架构层次关系图进一步扩展其底层。第二,要开发ASP.NET AJAX控件要求开发人员首先具备关于ASP.NET AJAX框架的客户端和服务器器知识(相比而言,在具体编程的过程中要求掌握更多的客户端相关知识)。

  在本文中,我们将从客户端和服务端两个角度全面探讨ASP.NET AJAX框架中的控件开发相关技术。首先,让我们从客户端开始。

  二、 控件开发客户端相关技术

微软Ajax库的最伟大之处在于,它扩展了JavaScript的面向对象编程模型并且提供一个增强的类型系统(其中包含命名空间,类,接口,枚举,异常处理,反射及其它为.NET开发者所熟悉的结构)。

  尽管Ajax控件都是继承自Sys.UI.Control,但是,整个Ajax控件的核心却是Sys.Comoponent类。这个类模拟了.NET框架的System.ComponentModel.Comoponent类。图1展示了在Ajax控件开发中继承类之间关系的示意图。

  

  图1.Ajax控件开发中所涉及的类关系图。

  (一) Sys.IDisposable

  这个接口类似于.NET框架的IDisposable接口。组件利用的所有资源都应该在dispose方法中释放。具体地说,这些资源包括事件处理器,大型数组,等等。

  (二) Sys.INotifyDisposing

  这个接口允许组件用户通过disposing事件检测该组件的释放情况。

  (三) Sys.INotifyPropertyChange

  这个接口允许组件用户通过PropertyChanged事件检测有关属性的改变情况。在本文后面,我们还要对此展开详细的讨论。

  (四) Sys.Component

  该Sys.Component类要实现上面所有的接口。封装了复杂的逻辑或包含子DOM元素的组件一般都要求使用一个中央位置用于初始化和清除类的实例。这通常是通过重载Sys.Component类的initialize和dispose方法实现的。下列图2展示了Sys.Component类的主要功能。

  

图2—Sys.Component类主要功能展示。

  进一步归纳来看,共存在两种类型的组件:

   Nonvisual(非可视化的)

   Visual(可视化的)

  一个非可视化的组件没有任何用户接口。一个典型的例子是ASP.NETAJAX框架内置的定时器控件或一个定制的对web服务进行队列调用的组件。非可视化组件类似于ASP.NET中的ObjectDataSource和TableAdapter控件,但是没有任何用户接口。

  另一方面,可视化的组件则提供了一个用户接口。一个典型的例子是Ajax Control Toolkit中的控件,UpdateProgress,以及AjaxGrid控件等等。其实,我们还可以把可视化组件进一步划分为:

   Action(Sys.UI.Action)

   Control(Sys.UI.Control)

  (五) Sys.UI.Action

  一个行为(Action)的目的是用来扩展一个DOM元素而不改变它的核心功能。Ajax Control Toolkit中的大多数控件就属于行为,例如该AutoCompleteTextBox,MaskEdit,DragPanel等。一个行为必须拥有一个相关联的DOM元素。注意,单个DOM元素可以拥有多个与之相关联的行为。既然本文的重点在讨论控件的开发技术,所以,我们不再进一步讨论行为的概念。如果你对此感兴趣的话,请访问Ajax Control Toolkit站点。

  (六) Sys.UI.Control

  相对于上面的Action而言,一个Control(控件)本身就是一个DOM元素。创建一个控件的主要目的是通过对一个现有控件加以包装进而提供新的功能。其典型的示例就是UpdatePanel,UpdateProgress或Ajax Control Toolkit中新加入的Tab控件。下列的代码向你展示了创建一个控件所要求的最少代码。

  列表1.构建一个客户端控件至少需要实现的JavaScript框架代码。

Type.registerNamespace('DummyNamespace');
DummyNamespace.DummyControl = function(element)
{
DummyNamespace.DummyControl.initializeBase(this, [element]);
}
DummyNamespace.DummyControl.prototype =
{
initialize : function()
{
DummyNamespace.DummyControl.callBaseMethod(this, 'initialize');
},
dispose : function()
{
DummyNamespace.DummyControl.callBaseMethod(this, 'dispose');
}
}
DummyNamespace.DummyControl.registerClass('DummyNamespace.DummyControl',
Sys.UI.Control);
if (typeof (Sys) != 'undefined')
{
Sys.Application.notifyScriptLoaded();
}

  上面所列出的仅是一个控件的框架代码。如你所见,与之相关联的DOM元素作为参数传入该控件的构造器函数中。我们还要重载initialize和dispose方法以便执行初始化和最后释放控件时的清理工作。因为在此列出的仅是一个虚构的控件,所以,除了调用基类的一些方法外我们什么也没有做。下列图3展示了Sys.UI.Control类提供的主要功能。

  

   图3—Sys.UI.Control类提供的方法。

  一个控件和一个行为之间的另一个区别在于,一个行为允许你设置一个id而一个控件却不允许。事实上,一个控件的id与其相关联的DOM元素是一致的。下面是这个类中常用方法(有些是父类提供)的详细解释。

get_element():返回该控件描述的DOM元素。

   get_id():返回该控件的id,为$find语句所用以引用此控件。

   set_id():如果你试图设置id,那么,你将得到一个Error.invalidOperation异常,因为控件是不允许设置id的。

   get_parent():返回父级控件。

   set_parent():设置父控件。

   get_visibilityMode():visibilityMode是一个枚举,其取值是hide或collapse。

   set_visibilityMode():设置visibilityMode的值。

   get_visibile():返回相应DOM元素的可见性。

   set_visibile():设置DOM元素风格可见性,取值为hidden或visible。

   addCssClass():把指定的CSS类添加到DOM元素的className中。

   dispose():继承自Sys.Component。

   initialize():继承自Sys.Component。

   onBubbleEvent():处理由raiseBubbleEvent激发的事件。如果这个方法返回true,则该事件将不会被上传到其父元素中进行处理。注意,如果你不处理该事件的话,你应该把此事件交由父级来作默认处理。

   raiseBubbleEvent():引发一个事件并交由父控件处理。总的来看,当你创建复杂的控件(经常是包含一个或多个子控件并且想把一个事件从子控件上交由其父控件来处理时)时往往要使用onBubbleEvent与这个方法。

   removeCssClass():从DOM元素的className中删除指定的CSS类。

   toggleCssClass():把指定的CSS类添加到DOM元素的className中(如果以前没有设置的话)。注意,如果已经指定了这个CSS类,那么,将从className中移除该类。

 三、 实例分析

  在本示例中,我们将创建一个增强的客户端ImageButton控件。这个控件在鼠标移过时将显示一幅不同的图像。

  (一)创建示例AJAX网站

  启动Visual Studio 2005,选择“文件→新建网站…”,然后选择“ASP.NET AJAX-Enabled Web Site”模板,命名工程为“AjaxClientCtrlTest”,并选择C#作为内置支持语言,最后点击OK。

  注意,此后系统将自动加入对于程序集System.Web.Extensions.dll的引用(因其被自动加入到GAC中,所以默认情况下无法直接看到)。当然,你还会注意到,作为ASP.NET AJAX控制中心的服务器控件ScriptManager被自动地添加到默认网页Default.aspx中。然后,把一个HTML <image>元素拖动到此页面中,并稍加修改,得到如下图4所示布局。

  

  图4.示例网站屏幕快照。

  (二)使用面向对象JavaScript设计控件类

  以鼠标右击工程添加一个JavaScript脚本文件ImageButton.js,内容如下列表2所示。

  列表2—脚本控件MyCliImageButton完整源码。  

Type.registerNamespace('AjaxImageButtonNamespace');
AjaxImageButtonNamespace.MyCliImageButton = function(element)
{
this._hoverImageUrl = '';
this._originalImageUrl = '';
this._mouseOverHandler = null;
this._mouseOutHandler = null;
this._clickHandler = null;
AjaxImageButtonNamespace.MyCliImageButton.initializeBase(this, [element]);
}
AjaxImageButtonNamespace.MyCliImageButton.prototype =
{
get_hoverImageUrl : function(){
return this._hoverImageUrl;
},
set_hoverImageUrl : function(value) {
var e = Function._validateParams(arguments, [{name: 'value', type: String}]);
if (e) throw e;
if (this._hoverImageUrl != value)
{
this._hoverImageUrl = value;
this.raisePropertyChanged('hoverImageUrl');
}
},
initialize : function(){
AjaxImageButtonNamespace.MyCliImageButton.callBaseMethod(this, 'initialize');
var target = this.get_element();
this._originalImageUrl = target.src;
this._mouseOverHandler = Function.createDelegate(this, this._onMouseOver);
this._mouseOutHandler = Function.createDelegate(this, this._onMouseOut);
this._clickHandler = Function.createDelegate(this, this._onClick)
$addHandlers(target, {'mouseover':this._mouseOverHandler, 'mouseout':this._mouseOutHandler, 'click': this._clickHandler}, this);
},
dispose : function(){
$clearHandlers(this.get_element());
delete this._mouseOverHandler;
delete this._mouseOutHandler;
delete this._clickHandler;
AjaxImageButtonNamespace.MyCliImageButton.callBaseMethod(this, 'dispose');
},
add_click : function(handler) {
this.get_events().addHandler('click', handler);
},
remove_click : function(handler) {
this.get_events().removeHandler('click', handler);
},
_onMouseOver : function(e) {
e.target.src = this._hoverImageUrl;
},
_onMouseOut : function(e) {
e.target.src = this._originalImageUrl;
},
_onClick : function(e) {
e.preventDefault();
var handler = this.get_events().getHandler('click');
if (handler != null) {
handler(this, Sys.EventArgs.Empty);
}
}
}
AjaxImageButtonNamespace.MyCliImageButton.registerClass('AjaxImageButtonNamespace.MyCliImageButton', Sys.UI.Control);
if (typeof(Sys) != 'undefined')
Sys.Application.notifyScriptLoaded();
}

  如你所见,我们重载了initialize方法来“钩住”对应DOM元素的mouseover,mouseout和click事件,以便我们可以在这些事件中设置合适的图像并激发click事件。我们还重载了dispose方法以分离我们在initialize方法中设置的事件处理器。后面,我们还要细致讨论。

(三)使用控件

  有了上面的控件类,至于使用就很简单了。请切换到页面default.aspx的源码视图,然后在<head>区创建如下列表3所示内容。

  列表3 

<script type="text/javascript">
function pageLoad(){
$create(AjaxImageButtonNamespace.MyCliImageButton, {'hoverImageUrl':'Images/updateh.gif'}, {'click':buttonClicked}, null, $get('cliBtn'));
}
function buttonClicked(sender, e) {
alert('I am clicked');
return false;
}
</script>

  在此,我们使用ASP.NET AJAX客户端全局方法$create创建控件AjaxImageButtonNamespace.MyCliImageButton的一个实例,指定其属性hoverImageUrl取值、click事件的处理器函数,并把它与当前页面中的HTML <IMAGE>元素关联起来。当然,为了使得框架在运行时能够找到我们刚才建立的脚本文件,还要对ScriptManager的属性稍微设置一下,如下列表4所示。

  列表4—在ScriptManager控件内配置脚本文件。

  <asp:ScriptManager ID="ScriptManager1" runat="server" >

  <Scripts>

  <asp:ScriptReference Path="ImageButton.js" />

  </Scripts>

  </asp:ScriptManager>

  现在,请按F5键运行此页面观察结果即可。下图5相应于当鼠标移动到图像按钮上时发生的变化(你还可以点击之自行观察效果)。

  

 图5.当鼠标移动到图像按钮上时图像切换成另一幅。

  下面,我们针对使用ASP.NET AJAX框架提供的面向对象JavaScript进行客户端控件编程时有关概念作全面分析。

  四、 使用JavaScript进行客户端控件编程相关概念

  (七) 属性

  把属性添加到一个控件中是很简单的。在此,推荐使用的方法是在控件构造器中声明一个私有变量,并在prototype部分添加相应的getter及setter属性方法。当访问该属性时,ASP.NET AJAX框架总是添加get_和set_前缀。因此,当添加getter/setter时,我们应该遵循get_propertyName和set_propertyName命名惯例。正如在上面的示例中相应于hoverImageUrl属性所实现的,我们使用get_hoverImage()作为getter,set_hoverImage()作为setter。但是在上面的例子中当我们使用一个$create()语句创建该控件时,我们仅传递了hoverImageUrl,因为ASP.NET AJAX框架将会自动添加set_前缀。与属性有关的另外一个重要问题是,当它的属性改变时如何通知该控件的客户端。下列的代码片断展示了如何激发propertyChanged事件。

set_hoverImageUrl : function(value){
if (this._hoverImageUrl != value)
{
//如果不同于当前值则仅设置新值
this._hoverImageUrl = value;
//激发propertyChanged事件
//注意这是一个位于基类Sys.Component中的方法
this.raisePropertyChanged('hoverImageUrl');
}
}

  (八) 方法

  使用JavaScript编写一个类的方法时,一般不作特殊考虑;但是,我们还是推荐在控件的原型(prototype)对象中添加它们。

  【注】就JavaScript本身编程而言,其表达形式多样,但建议你遵循这里推荐的格式(也正是框架相应js源码中所使用的格式)。

 (九) 事件

  在控件类中添加事件类似添加属性。首先,在控件构造器中把事件处理器声明为一个私有变量并在原型对象中创建相应于该事件的一个add/delete方法对。尽管并不要求在模块级声明事件处理器变量,但是,这样做的确存在一些优点,我将在Ajax框架的Function.createDelegate()节讨论这个问题。当添加/删除事件时,Ajax框架总是自动地添加上add_和remove_前缀。因此,当添加事件订阅代码时,该方法应该以add_eventName方式命名;反之,以remove_eventName方式命名。例如,在上面的click事件示例中,我们以add_click和remove_click形式添加事件订阅方法,但是在$create语句中,我们以click方式对它命名,由Ajax框架负责添加add_前缀。如你在上面的示例所见,add_click和remove_click都引用了this.get_event()(继承自Sys.Component)。这个Sys.Component类维持着一个订阅事件的内部列表,存储于一个Sys.EventHandlerList类的实例(以及各自它们的处理器)中。

  除了这个get_event()方法对外,还推荐使用raiseEventName方法来激发事件。由于上面所举的仅是一个很基本的例子,所以,我没有在其中使用它。但是,当你想从多处激发同一个事件时你必须添加它。当激发一个事件时,我们可以传递一些称为事件参数的上下文数据。

  在事件中传递参数的推荐的方法是,创建一个继承自Sys.EventArgs的新类并构造器中传递要求的参数。针对每一个参数,它将在原型对象部分暴露getter和setter。因为本例非常简单,所以,没有使用这种更一般的方式。

  当前在该微软Ajax库中相应于事件参数存在两个内置类—Sys.EventArgs和Sys.CancelEventArgs。即使你有一个不需要任何事件参数的事件,那么,你也应该传递Sys.EventArgs.Empty(例如在上面的ImageButton控件示例中),以便事件订阅者可以遵循相同的模式来处理所有的事件。

 如果你细致研究框架源码,你会注意到,其中绝大部分的控件都遵循了相同的模式—使用函数HandleEvent(sender, e)来处理所有事件。注意,这与.NET框架处理事件所实现的模式是相同的。而且,遵循这种方式给人的初始感觉过于复杂,但是却更遵循了一般性规律,便于以后的更复杂设计。

  五、 其它几个重要的全局客户端方法   下列,我们列出在开发客户端控件时,一些常用(有些是必用的)的重要方法。

  $get()

  这个方法是我们以前常用的Document.getElementById方法的一个快捷方式。此方法使用一个可选的父级DOM元素;如果没有指定这个父元素,那么,搜索操作将从Document对象开始执行。

  $create()

  这个方法是Sys.Component.Create方法的一个快捷方式,用于实例化一个客户端组件,行为或控件。这个方法的完整定义如下所示:

  $create(type, properties, events, references, element);

  举例:

  $create(AjaxImageButtonNamespace.MyCliImageButton, {'hoverImageUrl':'Images/updateh.gif'}, {'click':buttonClicked}, null, $get('cliBtn'));

  在此,第一个参数使用了对象的完整类型名。例如,当创建一个MyCliImageButton控件时,我们需要传递AjaxImageButtonNamespace.MyCliImageButton。这个参数必须存在。

  第二个参数对应一个键/值对(每一个一个键/值对对应一个特定属性及其该属性的取值)数组。例如,当创建MyCliImageButton控件时,我们需要要传递hoverImageUrl并给它赋值为{'hoverImageUrl':' Images/update-h.gif'}。如果我们想设置更多的属性,我们需要使用一个逗号来隔开这些键/值对。例如:

{'aProperty':value1, 'bProperty':value2}.

  第三个参数与第二个参数几乎相同。这个参数用于定义事件,也使用一个键/值对数组的表示方式。在上面的例子中,我们使用onButtonClick处理器订阅了click事件。

  第四个参数使用一个键/值对数组,用于引用那些在创建此组件时所需要使用的组件。既然本例中的图像按钮并没有请求任何对外部组件的引用;所以,我们简单地传递null为这个参数值。

  第五个也即是最后一个参数是一个DOM元素。在创建行为和控件时,这是一个必需的参数;但是,对于一个非可视化组件而言,这个参数是可选的。

  $find()

  这个方法是Sys.Application.findComponent静态方法的快捷方式。其使用语法为:

  var o = $find(id, [parent]);

  因此,它类似于$get方法;但是,它返回的是组件或其子类而不是DOM元素。可以给它指定一个可选的IContainer类型的父类,以便从它开始搜索。如果没有指定父类,那么,它从Sys._Application开始进行搜索。

  【注】这里的parent既可以是Component也可以是Sys.UI.DomElement。当其类型为Component时,搜索对象被限定为它的子组件;当其类型为DOM元素时,搜索对象被限定为其下的子元素。

  Function._validateParams

  我们知道,JavaScript并不是一个强类型化的语言;所以,为了充分保证方法中的参数符合既定的类型,必须进行大量编码,例如使用typeOf,parseInt,parseFloat,(parameters[0]!=null)等语句对参数进行测试。很可能是基于这样的原因,Ajax开发小组包括了这样一个功能极为强大的函数。现在,仅凭一个语句,这个函数就可以实现校验所有的方法参数。

  【注】虽然是一个函数,但它实际上是Function._validateParameterCount、Function._validateParameter及Function._validateParameterType等多个子函数的功能组合。读者可以参考下载源码中的MicrosoftAjax.js文件自行分析。

让我们来看一个简短的示例:

set_hoverImageUrl : function(value)
{
var e = Function._validateParams(arguments, [{name: 'value',
type: String, mayBeNull: false}]);
if (e) throw e;
//……
}

  在上面的示例中,我们要保证参数值是一个字符串数据类型并且非空。所以,这个函数的正确形式应该是:

  Function._validateParams(params, expectedParams)

  第一个参数是一个参数数组,通常是参数变量;第二个参数是一个定义了每一个参数信息的数组。每一个定义可以包含:

   Name:参数名;

   Type:参数的数据类型,例如String,Number,Boolean,Array或任何其它你自己创建的定制类型;

   mayBeNull:取值为true或false;缺省为false;

   optional:取值为true或false;缺省为false;

   parameterArray:取值为true或false;缺省为false;

   integer:取值为true或false;缺省为false;

   elementType:如果参数是一个数组,那么该项用于指定此数组包含的数据类型;

   elementInteger:如果参数是一个数组,那么该项用于确定此数组包含的是integer;

   elementDomElement:如果参数是一个数组,那么该项用于确定该数组包含的是DOM元素;

   elementMayBeNull:取值为true或false。如果参数是一个数组,那么该项用于确定是否该数组可以拥有任何的空项。

  如果校验失败,Function._validateParams将返回一个应该由此函数的调用者抛出的异常。

  $addHandler

  这是Sys.UI.DomEvent.addHandler的一个快捷方式。它提供了一种一致的方式用于“钩住”DOM元素的事件;同时,隐藏了不同浏览器下事件操作的复杂性。该方法的完整形式如下所示:

$addHandler(element, eventName, handler);

  第一个参数是一个DOM元素的引用。第二个参数相应于事件名(这里不必提供“on”前缀,ASP.NET Ajax系统为自动地添加这个前缀)。最后一个参数用于指定该事件的处理器函数。例如,在下列代码中,我为按钮“btnSelect”绑定了click事件(也即是为此按钮添加了一个事件代理,这与C#中的事件机制是基本一致的)。

$addHandler($get('btnSelect'), 'click', selectRowHandler);
//更多代码……
function selectRowHandler()
{
//处理click事件的代码
}
//更多代码……
$removeHandler

  删除一个之前使用$addHandler创建的事件处理器。这个方法的语法形式与$addHandler完全一致。实际使用中,一般建议在pageUnload事件中删除所有的事件处理器。

  $addHandlers

  这个方法类似于$addHandler,其语法如下所示。

  $addHandlers(element, events, [handlerOwner]);

  因此,它允许使用单个语句指定相同DOM元素的多个事件代理。例如:

  $addHandlers(this.get_element(), {'mouseover':this._mouseOverHandler,

  'mouseout':this._mouseOutHandler, 'click': this._clickHandler}, this);

  在上面的代码中,我们使用一个语句添加了mouseover,mouseout和click事件。第一个参数是对DOM元素的引用;第二个参数是一个“事件名/处理器”对数组。最后一个参数是一个可选的参数(通常是this)。

  $clearHandlers

  删除所有之前使用$addHandler或$addHandlers创建的事件处理器。它仅有一个参数(引用一个DOM元素)。例如,下列语句会清除所有添加到一个DOM元素上的事件处理器函数:

 $clearHandlers(this.get_element());

  Function.createDelegate

  这是一个既能够“包装”一个现有函数,又能够返回一个新函数的函数。创建此函数的主要目的是解决this关键字的问题。在一个由一个DOM元素引发的事件处理器中,this关键字总是引用此DOM元素而不是类本身。这一点很重要,我们不妨来考虑下列的示例。如果我们如下面这样对图像按钮控件编程,那么,它将总是引发一个异常:

//…………
initialize : function()
{
//调用基类方法
AjaxImageButtonNamespace.MyCliImageButton.
callBaseMethod(this, 'initialize');
var target = this.get_element();
//添加需要使用的事件处理器
$addHandler(target, 'mouseover', this._onMouseOver);
},
//…………
_onMouseOver : function()
{
//请注意下面的this关键字,它不是MyCliImageButton控件本身,而是指相应的DOM元素,所以它肯定会引发一个异常
this.get_element().src = this._hoverImageUrl;
},
//……

  接下来,让我们看一下真正有趣的Function.createDelegate函数。现在,我们按如下方式来重写前面的代码:

……
initialize : function()
{
//调用基类方法
AjaxImageButtonNamespace.MyCliImageButton.
callBaseMethod(this, 'initialize');
var target = this.get_element();
//请注意下面两句的联合使用
this._mouseOverHandler = Function.createDelegate(this, this._onMouseOver);
//添加需要的事件处理器
$addHandler(target, 'mouseover', this._mouseOverHandler);
},
……………
_onMouseOver : function(e) {
//现在的this关键字就变成了MyCliImageButton控件本身而不再是前面的DOM元素了。
//注意:现在e.target对应于原来的DOM元素
e.target.src = this._hoverImageUrl;
},
…………

 

  总之,Function.createDelegate方法要求使用指定两个参数。第一个参数是在该该事件处理器中的this所指向的对象。第二个参数是将作为事件处理器执行的函数。

  Function.createCallback

  这个函数也有些特别;因为它也能够“包装”一个现有函数,同时返回一个新的函数。但是,创建这个函数的真正目的是为创建的回调函数提供相应的上下文数据(与Win32中的回调函数意义完全可比)。

  Function.createCallback方法也要求使用两个参数。第一个参数是将被执行的函数,第二个参数指定相应的上下文数据。不同于Function.createDelegete,这个函数并没有解析this关键字。

  最后,除了上面的几个方法外,我还强力推荐你认真研究一下ASP.NETAJAX文档中提供的Sys.UI.DomEvent,Sys.UI.MouseButton和Sys.UI.Key等内容。

  六、 总结

  在本篇中,我们讨论了开发Ajax控件所涉及的客户端相关技术并给出相应的示例。在下篇中,我们将着重讨论开发基于ASP.NET Ajax框架的控件所涉及的服务器端主要技术,也将一并开发出相应的示例程序。

服务器端

一、简介

  到目前为止,我们已经讨论了开发Ajax控件所涉及的客户端相关技术。现在,让我们来讨论此过程中与服务器端相关的一些技术。

  需要说明的是,在【客户端】篇中我们的举例本质上仅是使用ASP.NET AJAX框架提供的面向对象JavaScript技术来增强了一个客户端图像组件,而没有明显涉及到AJAX技术(除了ScriptManager在后台以AJAX方式下载并管理客户端脚本代码外)。所以,这个例子是简单的,仅凭客户端相关知识就可以使用这个增强控件。

  但是,在实际开发中,当要增强的客户端控件涉及到AJAX技术时,或者干脆是想增强服务器端组件(如UpdatePanel控件)时,我们必须进行相关的服务器端编程,而这要求我们必须对Ajax控件开发中所涉及的服务器端相关联的类有所了解。而且,还要以ASP.NET 2.0服务器控件开发相关知识为基本前提,特别是在开发复杂的Ajax控件时。

  在本篇中,我们要重新构造一个增强的图像按钮控件MySrvImageButton,此控件将以ASP.NET 2.0服务器控件ImageButton为基础。

  二、AJAX控件开发服务器端相关技术

  首先,让我们来看一下AJAX控件开发服务器端相关组件及其关系,这些类之间的继承关系图如下图1所示。

  

  图1:控件开发涉及的主要服务器端类之间层次结构图

  上图展示了组件、控件和扩展器之间的继承关系。如你所见,为了开发一个控件(注意,Component和Extender不在本文讨论范围之内),我们有两个选择:其一,创建一个派生自ScriptControl的类;其二,创建一个实现IScriptControl接口的类。但是,如果你想使你的控件从WebControl派生,那么,ScriptControl应该是一个更好的选择—因为它正是派生自WebControl控件本身。但是,如果你想从头开发创建你的控件,并且不要求实现WebControl所具备的任何内在特征,那么,实现IScriptControl则更为恰当。此外,当你想在一个现有控件(例如本文中的MySrvImageButton)中添加Ajax特征时选择使用接口IScriptControl也会是你的选择。但是这两种方法都要求重载下列两个方法:①、GetScriptDescriptors;②、GetScriptReferences。

 三、GetScriptDescriptors

  这个方法负责在客户端以自动方式生成$create语句(而在上篇中是使用手工方式)。它使用了一个特殊类ScriptDescriptor来生成它。在继续往下讨论前,首先让我们来浏览一下ScriptDescriptor类中的继承层次图(如图2所示):

  

  图2:ScriptDescriptor类中的继承层次图

  显然,每一种具体类型的组件都存在其单独的描述符。如果你在开发一个常规组件,那么,这个方法会返回一个ScriptComponentDescriptor的实例。对于扩展器而言,该方法返回的是一个ScriptBehaviorDescriptor实例;而对于一个控件,则返回ScriptControlDescriptor类的实例。该描述符中提供了一些特定的方法用于创建服务器端与客户端的“连接”。下面,还是让我们来分析一个有关$Create语句是如何在服务器端“注入”的简短示例:

//上篇中从客户端以手工方式使用$create
$create(AjaxImageButtonNamespace.MyCliImageButton,
{'hoverImageUrl':'Images/updateh.gif'},
{'click':buttonClicked}, null, $get('cliBtn'));
在本文中,我们在服务器端以下列几个语句共同自动生成$Create语句:
ScriptControlDescriptor desc =
new ScriptControlDescriptor("AjaxImageButtonLib.MySrvImageButton",
ClientID);
if (!string.IsNullOrEmpty(HoverImageUrl))
{
desc.AddProperty("hoverImageUrl", HoverImageUrl);
}
if (!string.IsNullOrEmpty(ClientClickFunction))
{
desc.AddEvent("click", ClientClickFunction);
}
return desc;

  在上面的代码中,我们在构造器中传递客户端类型(尽管客户端和服务器端具有相同的类型名)和DOM元素ID。这一步将填充$Create语句的第一个和最后一个参数。此后,我们设置hoverImageUrl属性,它对应于$Create语句的属性部分。最后,通过设置Click事件处理器,我们填充$Create语句的事件部分。下面是脚本描述符中暴露的几个重要的方法:

1)AddProperty()

  这个方法能够在客户端添加一个属性。第一个参数相应于属性名,第二个参数对应该参数的取值。

  2)AddEvent()

  这个方法在客户端添加一个事件。第一个参数相应于事件名,第二个参数对应你想绑定的函数的名称。

  3)AddScriptProperty()

  这个方法能够把JavaScript代码指定为属性的一个值。对于复杂的属性赋值通常都要求这样的操作。

  4)AddElementProperty()

  这个方法在客户端添加一个属性,但是其与AddProperty方法间的区别在于,该值作为一个参数传递给$get方法。

  5)AddComponentProperty()

  这个方法负责在$Create语句的Component部分添加一个组件引用。该值将被用于$find语句中实现属性的赋值。这个方法的第一个参数相应于属性名,第二个参数对应该组件的id。

  四、GetScriptReferences

  这个方法负责在ScriptManager中注册JavaScript文件。在这个方法中,针对每一个要求的JavaScript文件,我们都需要创建一个相应的ScriptReference实例。例如,在图像按钮例子中,我们按如下方式注册JavaScript文件:

IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
{
return new ScriptReference(Page.ResolveUrl("~/SrvImageButton.js"));
}

  此外,我们还可以以嵌入式资源方式注册JavaScript文件。为此,我们必须首先在Visual Studio的解决方案资源管理器中把一个JavaScript文件标记为一种嵌入式资源。然后,我们必须添加上WebResource属性以便利用Asp.Net 2.0 Web资源处理器。下列代码展示了如何把一个JavaScript文件注册为一个嵌入式资源:

[assembly: WebResource("MyControls.SubControl.Script1.js", "text/javascript")]
namespace MyControls{
public class SubControl: Control, IScriptControl
{
//………
IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
{
return new
ScriptReference(this.Page.ClientScript.GetWebResourceUrl(
this.GetType(), "MyControls.SubControl.Script1.js"));
}
}
}

  此外,如果我们想实现IScriptControl接口而不是从ScriptControl中继承的话,我们还必须重载OnPreRender与Render方法。这样将能确保ScriptManger识别出该服务器控件是一个支持Ajax功能的控件。

下列代码展示了开发基于ASP.NET 2.0服务器端控件的AJAX控件时必须在服务器端实现的最少代码:

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace DummyNamespace
{
public class DummyControl : Control, IScriptControl
{
public DummyControl(): base()
{}
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
ScriptManager scriptManager = ScriptManager.GetCurrent(Page);
if (scriptManager == null)
{
throw new InvalidOperationException(
"此页面中必须存在一个ScriptManager控件!");
}
scriptManager.RegisterScriptControl(this);
}
protected override void Render(HtmlTextWriter writer)
{
base.Render(writer);
writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);
writer.RenderBeginTag(HtmlTextWriterTag.Div);
writer.Write("这是一个哑元控件");
writer.RenderEndTag();
if (!DesignMode) {
ScriptManager.GetCurrent(this.Page).RegisterScriptDescriptors(this);
}
}
IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors()
{
//因为这仅是一个哑元控件,所以我们只是创建一个新的实例
return new ScriptControlDescriptor("DummyNamespace.DummyControl", ClientID);
}
IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
{
return new ScriptReference(Page.ResolveUrl("~/DummyControl.js"));
}
}
}

  上面代码中,我们在OnPreRender方法中实现注册控件,而在Render方法中注册脚本描述符部分。当然,如果页面上不存在ScriptManager控件的话,我们必须抛出一个错误提示。但是,如果你正在开发非基于ASP.NETAJAX框架的控件的话,你完全可以从页面中删除ScriptManager控件。

 五、创建基于ASP.NET服务器端控件的增强AJAX图像控件

  (一)创建示例AJAX网站

  启动Visual Studio 2005,选择“文件→新建网站…”,然后选择“ASP.NET AJAX-Enabled Web Site”模板,命名工程为“AjaxServCtrlTest”,并选择C#作为内置支持语言,最后点击“确定”。

  (二)创建AJAX技术支持的增强服务器控件

  点击菜单“文件→添加→新建项目…”,在“添加新项目”对话框中,从左边选择“项目类型”为“Visual C#→Windows”,从右边选择“模板类型”为“Web控件库”,输入控件库的名字为AjaxImageButtonLib,选择目标目录为前面创建的网站根目录,最后点击“确定”。

  接下来,根据我们前面的分析,把类库源WebCustomControl1.cs文件的内容更改为以下形式:

//…………(省略命名空间引用部分)
namespace AjaxImageButtonLib
{
public class MySrvImageButton :
System.Web.UI.WebControls.ImageButton, IScriptControl
{
public string HoverImageUrl
{
get
{
object value = ViewState["hoverImageUrl"];
return (value == null) ? string.Empty : (string)value;
}
set
{
ViewState["hoverImageUrl"] = value;
}
}
public string ClientClickFunction
{
get
{
object value = ViewState["clientClickFunction"];
return (value == null) ? string.Empty : (string)value;
}
set
{
ViewState["clientClickFunction"] = value;
}
}
public MySrvImageButton()
: base()
{
}
protected override void OnPreRender(EventArgs e)
{
base.OnPreRender(e);
ScriptManager scriptManager = ScriptManager.GetCurrent(Page);
if (scriptManager == null)
{
throw new InvalidOperationException
("ScriptManager required on the page.");
}
scriptManager.RegisterScriptControl(this);
}
protected override void Render(HtmlTextWriter writer)
{
base.Render(writer);
if (!DesignMode)
{
ScriptManager.GetCurrent(this.Page).RegisterScriptDescriptors(this);
}
}
IEnumerable<ScriptDescriptor> IScriptControl.GetScriptDescriptors()
{
ScriptControlDescriptor desc =
new ScriptControlDescriptor("AjaxImageButtonLib.MySrvImageButton", ClientID);
if (!string.IsNullOrEmpty(HoverImageUrl))
{
desc.AddProperty("hoverImageUrl", HoverImageUrl);
}
if (!string.IsNullOrEmpty(ClientClickFunction))
{
desc.AddEvent("click", ClientClickFunction);
}
yield return desc;
}
IEnumerable<ScriptReference> IScriptControl.GetScriptReferences()
{
yield return new ScriptReference(Page.ResolveUrl("~/SrvImageButton.js"));
}
}
}

  首先,我们注意到我们要创建的新控件MySrvImageButton继承自ASP.NET服务器控件ImageButton,并继承了接口IScriptControl。

 有了前面的理论描述,在此,我们省略对于其中几个常规方法的描述。

  接下来,我们要构建这个控件库(即程序集)。右击此库工程,并点击“生成”命令以生成程序集AjaxImageButtonLib.dll,此库文件中即包含了我们的服务器控件。

  (三)创建客户端JavaScript代码

  这里创建的客户端控件类相应的JavaScript文件ImageButton.js在内容上完全相同,只不过为了区别起见,我们进行了某些地方的重新命名罢了。在此不再赘述。

  (四)在示例网页中应用构建的新控件

  以鼠标右击前面网站AjaxServCtrlTest,把它设置为“启动项目”。事实上,因为这个网站工程与前面的类库工程创建于同一个方案下,所以,在前面生成程序集AjaxImageButtonLib.dll的一结束,新建的服务器控件就被自动添加到Visual Studio 2005工具栏中,如下图3所示。于是,我们可以直接把这个控件拖动到示例网页Default.aspx中。

  

  图3:拖动新建的服务器控件到示例网页中

  根据前面的控件代码实现,不出所料,点击上图3中的图形按钮控件,即可在其相应的“属性”对话框中设置这个控件的hoverImageUrl属性,而且指定其ClientClickFunction方法(其实正是此控件的click事件处理器函数指针)。

  注意,因为控件代码中的方法GetScriptReferences已经为我们自动生成了前面提到的$create方法,所以我们不需要再在ScriptManager中注册在本篇中创建的JavaScript文件—SrvImageButton.js了。

  (五)运行及性能简析

 现在,请按F5键运行此页面并移动鼠标到图像按钮上观察,你会注意到结果与上篇中的效果一致(即在鼠标移动切换新图像时,这些动作都发生于客户浏览器端而不再与服务器端相关)。下图4相应于此示例页面运行时刻快照。

  

  图4:示例网页运行时刻屏幕快照

  通过以鼠标右击网页并选择弹出菜单中的“查看源文件”观察上、下篇中示例页面相应的源码,我们会注意到其内容基本是一致的。另外,通过使用Fiddler观察这两个示例页面下载到客户端时各模块的大小,你也会注意到基本一致,如下面图5所示。

  

  图5:两个示例页面下载到客户端时各模块大小比较

  因为本文两个例子极为简单,所以其性能基本平衡。但随着服务端编程的复杂化,本篇中基于服务器端控件的扩展方案应该有较大的性能损耗。但应该仍具有令人满意的效果,这也正是AJAX Control Toolkit控件数量急剧增加的重要原因之一。而上篇中的方案基于“纯粹”(相对而言)的客户端,即使性能上与本篇中方案相差无几,但是却明显多出了跨越服务器端平台的优势,这也正是上篇中方案吸引人的主要原因。

  六、总结

  虽然以上、下两个篇幅形成此文,但是这也仅能通过简短的例子向你阐述了开发ASP.NETAJAX框架中的Ajax控件所涉及的主要技术。尽管目前的ASP.NET AJAX框架已经形成正规的1.0版本,而且这个框架为基于AJAX技术开发以ASP.NET 2.0为主的Web应用提供了全方位支持,但是这个框架仍然在许多方面有待改进。事实上,我们可以进一步沿着ASP.NET AJAX客户端与服务器端架构层次关系图进一步扩展其底层。当然,在此框架与Visual Studio整合方面也存在相当的挖掘潜力。

 

  如今,随着微软Silverlight技术的推出,ASP.NET AJAX框架的重要性日显突出。自然,与此框架相关的控件开发也必将在这一大环境中占居着重要的位置。

你可能感兴趣的:(JavaScript,Ajax,服务器,asp.net,微软,ajax框架)