ASP.NET AJAX 自定义控件的开发是一个复杂、详细的议题。如果你是一名业务开发人员,你会更希望使用现有的 ASP.NET AJAX 控件而不是自己编写代码。如果你是一名组件开发人员,那么你可以通过一些专攻的资料学习自定义控件的开发。这里介绍两本书:
- ASP.NET AJAX in Action(Manning,2010):详细展示了 ASP.NET AJAX 工具包
- Advanced ASP.NET AJAX Server Controls For .NET Framework 3.5(Addison,2008):如何开发自定义控件
理解客户端模型
ASP.NET AJAX 的基础框架是客户端的 JavaScript 库。它们是其他所有特性的粘合剂。客户端库为 JavaScript 世界添加了 .NET 特色,它们由 3 个主要部分组成:
- JavaScript 扩展:能在普通的 JavaScript 代码里使用面向对象的技术。
- 核心 JavaScript 类:建立了一个简单的框架,提供 Ajax 应用程序需要的基本客户端功能。核心类包含用于字符串操作、组件、网络以及 Web 服务的类。
- UI 框架:建立在由核心类构建的框架之上。UI 框架增加了客户端控件和客户端页面的概念。
客户端库非常紧凑,只有不到 200KB 的脚本代码。这些脚本只被下载一次,然后由浏览器缓存。如果浏览器支持(IE7 或以后版本),ASP.NET 还会使用压缩。
JavaScript 面向对象编程
JavaScript 并不是真正面向对象的语言,因为它缺乏对面向对象核心特性的支持,如继承、接口等。但 JavaScript 经常被描述为基于对象的语言,因为 JavaScript 提供内建对象(代表浏览器窗口、当前 HTML 文档等)。
JavaScript 没有提供自定义类的能力。不过,有一个流行的方案,开发人员一般会使用它创建与类近似的代码构造。
首先,创建具有任意属性的一次性对象非常容易,如下:
var emp = new Object;
emp.FirstName = "Joe";
emp.LastName = "Higgens";
这段代码段问题是它没有使用类,因此无法验证你正在使用的对象是不是真的员工,也没有办法确保两个员工对象是否真正公开相同的属性。
JavaScript 是一种非常灵活和松散的语言。上例中,2个属性在代码给它们赋值时被自动创建,并没有必要预先显式声明它们。这使得创建对象很容易,但也带来很多潜在问题。很可能在引用某个现有属性但不小心使用了错误的名称偶然创建了一个新属性。
要创建更标准化的对象定义,JavaScript 开发人员一般会使用这两个技术:闭包或者原型。
1. 闭包
闭包是一个封装了类的函数。你并不会真正运行闭包函数,运行的是它的嵌入函数。这些函数正是类的方法、属性。它们能访问定义在闭包函数里的所有变量。
理解闭包模型最简单的是研究一个示例:
function Employee(first, last) {
// The private section
var _firstName = first;
var _lastName = last;
// The public section
this.set_FirstName = function (first) {
_firstName = first;
}
this.get_FirstName = function () {
return _firstName;
}
this.set_LastName = function (last) {
_lastName = last;
}
this.get_LastName = function () {
return _lastName;
}
}
在闭包里定义的变量(_firstName、_lastName)在 Employee() 函数内有效,但函数外不能访问。但另一方面,Employee() 函数中的方法(set_FirstName()、get_FirstName()等)则可以随时被调用。
要创建员工对象,需要在同一个脚本块或者页面之后不同的脚本块里使用类似下面的代码:
var emp = new Employee("Joe", "Higgens");
var name = emp.get_FirstName() + " " + emp.get_LastName();
alert(name);
早期版本的 ASP.NET AJAX 为面向对象编程使用了闭包,不过,后来的版本转而使用了原型系统。
从技术上而言,Employee() 函数为自己增加了 4 个属性(那些 get、set 函数),调用对象的方法时实质上访问的是这些属性,只不过这些属性是一些函数指针。这个系统引入了一些潜在错误。比如,你调用函数时只是简单指向了这个函数(省略括号)或无意中删除了一个对象的方法(通过指定属性)。
2. 原型
由于各种技术原因,原型是 ASP.NET AJAX 中的首选。在某些浏览器(比如 Firefox),原型可以提供更好的性能,并且能够为反射、只能提示和调试提供更好的支持。因为原型使自己的成员“固化”,而闭包在每次对象被实例化的时候创建自己的成员。
要使用原型,需要依赖于所有 JavaScript 对象都拥有的公共属性 prototype 。这个属性公开对象的公共接口,使用方式见下面重写刚才闭包的定义:
Employee = function (first, last) {
// The private section
this._firstName = first;
this._lastName = last;
}
// The public section
Employee.prototype.set_FirstName = function (first) {
this._firstName = first;
}
Employee.prototype.get_FirstName = function () {
return this._firstName;
}
Employee.prototype.set_LastName = function (last) {
this._lastName = last;
}
Employee.prototype.get_LastName = function () {
return this._lastName;
}
Employee 对象自身其实是对某个用作构造函数的函数的引用。这个函数初始化所有的私有成员。而公共成员通过添加到 prototype 上单独定义。这些公共方法中访问原型中定义的属性必须加上 this 关键字。使用原型的 Employee 类的代码保持不变。
闭包和原型的工作方式有一点很小的不同。本质上,每次基于闭包创建一个新对象时,那个闭包都会为对象创建专门的成员(set_FirstName、get_FirstName等)。但是原型对象只创建和配置一次,然后复制到每个新对象。这就是某些浏览器上性能会提高的原因。
另外,原型可简化 ASP.NET AJAX 里的某些特定任务,如反射。因此,最终选择了原型。
3. 注册 ASP.NET AJAX 类
闭包和原型本来就已经用于 JavaScript 语言了。后面将会介绍 ASP.NET AJAX 添加到面向对象开发的 3 要素:命名空间、继承和接口。但在使用这些特性之前,必须在 ASP.NET AJAX 框架里注册自己的 JavaScript 类。
首先确保 JavaScript 代码位于含有 ASP.NET AJAX 客户端库的某个 Web 页面上。最简单的办法是在页面上添加 ScriptManager 控件,然后把自己的脚本块放在 ScriptManager 控件之后,接着在定义了原型后在构造函数上调用 registerClass() 函数即可:
Employee = function (first, last)
{ ... }
Employee.prototype.set_FirstName = ...
Employee.prototype.get_FirstName = ...
...
Employee.registerClass("Employee");
从技术角度讲,Employee 变量是你用于创建员工对象的那个构造函数的引用。之所以能够调用 registerClass() 方法,是因为 ASP.NET AJAX 客户端库为注册类、命名空间、接口和枚举添加了方法。
注册了 Employee 类后,仍然使用相同的代码创建员工对象,但现在 ASP.NET AJAX 知道你的类并且为它提供了更多的内建功能。反射就是其中的一个例子,用类似下面的语法能获取类的类型信息:
var emp = new Employee("Joe", "Higgens");
alert(Object.getTypeName(emp));
如果和这段代码一起运行的是一个未注册的类,你会看到类的名字是 Object 。而现在你可以看到下图所示:
Object 类提供了另外几个成员,可以使用它们取得注册过的自定义类的类型信息。这些成员包括:
- implementsInterface:检查类是否实现了某个特定的接口
- getInterfaces:得到类所实现的全部接口
- inheritsFrom:检查类是否直接或间接从某个类派生
- isInstanceOfType:检查对象是否是某个特定类或从该类派生类的实例
4. 基类
ASP.NET AJAX 客户端库扩展了几个 JavaScript 核心类型,为它们增加了辅助方法。很多情况下,这些扩展使得它们能够像 .NET 中对应的类那样工作。
ASP.NET AJAX 中扩展的 JavaScript 类型
Array | 增加了能添加、删除、清空和搜索数组元素的静态方法 |
Boolean | 增加了 parse() 方法,能把字符串格式的 Boolean 值转换为 Boolean 类型 |
Date | 增加了格式化和解析方法,可以把日期字符串相互转换,可使用不变格式或 Locale 格式 |
Number | 增加了格式化和解析方法,可以把数值和字符串相互转换,可使用不变格式或 Locale 格式 |
String | 为截取和比较字符串增加了很小一组字符串操作的方法(Sys.StringBuilder类增加了构建字符串的其他方法) |
Error | 为常见错误类型增加了一些属性,它们返回相应的异常对象。例如 Error.argument 返回 Sys.ArgumentException 对象 |
Object | 增加了 getType() 和 getTypeName() 方法,它们是反射类型信息的入口 |
Function | 为管理类增加了方法,包括定义命名空间、类和接口的方法 |
5. 命名空间
按照惯例,所有 JavaScript 函数都同属一个全局的命名空间。不过 ASP.NET AJAX 为之添加了把代表类的函数分离到各个独立的逻辑命名空间里的能力。这对于预防 ASP.NET AJAX 内建类和自定义类的冲突特别有用。
如果要注册一个命名空间,那么在创建类之前就要使用 Type.registerNamespace() 方法,然后通过完全限定名把类型放到命名空间里:
Type.registerNamespace("Business");
Business.Employee = function (first, last)
{ ... }
Business.Employee.prototype.set_FirstName = ...
Business.Employee.prototype.get_FirstName = ...
...
Business.Employee.registerClass("Business.Employee");
如果现在执行下面的代码就会得到类型的完全限定名“Business.Employee”:
var emp = new Business.Employee("Joe", "Higgens");
alert(Object.getTypeName(emp));
必须把命名空间名称放在每个成员之前的要求使得代码比以前冗长。为了节省空间,一般约定单独定义每个方法,然后在某一步内把所有方法都赋给原型。下面是这项技术的示例:
Type.registerNamespace("Business");
Business.Employee = function (first, last)
{ ... }
function Business$Employee$set_FirstName(first) {
this._firstName = first;
}
function Business$Employee$get_FirstName() {
return this._firstName;
}
function Business$Employee$set_LastName(last) {
this._lastName = last;
}
function Business$Employee$get_LastName() {
return this._lastName;
}
Business.Employee.prototype = {
set_FirstName: Business$Employee$set_FirstName,
get_FirstName: Business$Employee$get_FirstName,
set_LastName: Business$Employee$set_LastName,
get_LastName: Business$Employee$get_LastName
};
Business.Employee.registerClass("Business.Employee");
约定通过完全限定的命名空间和类命名每个成员,并用美元符号“$”替换点号“.”!
这两种方式都是可接受的,但前面一个示例采用的方法更常见,也是 ASP.NET AJAX 采用的方式。
6. 继承
ASP.NET AJAX 支持继承。注册派生类时,应把基类的名称作为第二个参数。看下面示例,不过要知道作为基类必须在脚本块的前面或者前面的脚本块中:
Business.SalesEmployee = function (first, last, salesDepartment) {
// Call the base constructor to initialize the parent class data.
// The base class constructot accepts two parameters,
// which represent the first and last name.
Business.SalesEmployee.initializeBase(this, [first, last]);
// Initialize the derived class data.
this._salesDepartment = salesDepartment;
}
Business.SalesEmployee.prototype.get_SalesDepartment = function () {
return this._salesDepartment;
}
Business.SalesEmployee.registerClass("Business.SalesEmployee",
Business.Employee);
registerClass() 调用传入新类的名称(字符串形式)和父类的名字(父类函数引用的形式),这样注册了一个类后,除了自己的成员,它还拥有了父类的全部成员。你可以用下面的代码获取信息:
var salesEmp = new Business.SalesEmployee("Joe", "Higgens", "Western");
var desc = salesEmp.get_FirstName() + " " + salesEmp.get_LastName()
+ " " + salesEmp.get_SalesDepartment();
alert(desc);
如果派生类提供的某个成员和父类的成员名称相同,那么派生类的版本自动覆盖父类对应成员。和 C# 语言不同,没有办法创建必须覆盖或不能覆盖的成员。此外,派生类能够访问父类定义的所有变量,但应该避免直接访问它们,应该通过属性访问器方法访问它们。
SalesEmployee 代码唯一的巧妙在于 initializeBase() 调用,它允许构造函数去调用基类的构造函数,并因此初始化姓和名。initializeBase() 方法是 ASP.NET AJAX 增加到基本函数类型的方法之一。
还可以通过 callBaseMethod() 方法调用基类中被派生类覆盖的方法。
7. 接口
还可以采用创建类所用的原型模式在 JavaScript 里定义接口。Prototype 属性公开接口的成员。不过,需要其他的限定来保证接口不会像对象那样被使用。
首先,接口的构造函数不能包括任何代码,也不能给任何数据赋值。相反,它应该抛出一个 NotImplementException 异常以防止被实例化。通用,原型里定义的方法也不应该包含任何代码,被调用时要抛出 NotImplementException 异常。这种需要使得 JavaScript 的接口定义远比 C# 接口定义要长。
理解这些原理,可以查看一个 ASP.NET AJAX 中定义的某个接口。
Sys.IDisposable 接口是 .NET System.IDisposable 接口在 ASP.NET AJAX 中的对应物。它向对象提供了一种方式来释放刚刚使用的资源。Sys.IDisposable 接口只定义了一个方法,名为 dispose() ,下面是 IDisposable 接口的全部代码:
Type.registerNamespace("Sys");
Sys.IDisposable = function Sys$IDisposable() {
throw Error.notImplemented();
}
function Sys$IDisposable$dispose() {
throw Error.notImplemented();
}
Sys.IDisposable.prototype = {
dispose: Sys$IDisposable$dispose
}
Sys.IDisposable.registerInterface("Sys.IDisposable");
要使用接口,首先要确保类包含了具有所需名称的成员,例如下面的模拟实现接口:
Business.SalesEmployee.prototype.dispose = function () {
alert("Disposed");
}
如果确实包含,那么修改注册类的代码来实现接口,把要实现的接口作为第三个参数传入:
Business.SalesEmployee.registerClass("Business.SalesEmployee",
Business.SalesEmployee, Sys.IDisposable);
如果要实现多个接口,可在第三个参数后面加入任意多个参数,每一个参数代表一个接口。
可以用下面的代码验证 dispose 行为,当 SalesEmployee 对象被释放时,你将会看到释放消息出现:
var salesEmp = new Business.SalesEmployee("Joe", "Higgens", "Western");
salesEmp.dispose();