C# 事件实现基础、最佳实践和约定






4.90/5 (280投票s)
本文介绍了事件实现的原理、最佳实践和约定。
引言
本文介绍了使用 C# 定义、实现和理解自定义事件所需的一切。为了实现这些目标,除了事件实现的最佳实践和约定之外,本文还介绍了必须或应该使用的基本构建块。本文介绍了 .NET 1.x 和 2.0+ 发布和订阅事件的替代方案。
虽然从 .NET Framework 1.0 版本开始就支持自定义事件的实现,但自那时以来又增加了额外的事件相关支持和功能。一些新功能(例如,泛型 System.EventHandler
、匿名方法、委托推断等)包括旨在使事件实现更容易的快捷方式。虽然这些技术确实有助于更快地实现事件,但在基本构建块之前或代替它们介绍它们会导致不那么明确的介绍。因此,本文避免在介绍基本构建块之前使用这些快捷方式。
目录
- 对读者的假设
- 术语和定义
- 委托(Delegates)
- 委托与事件的关系
- 事件参数 (EventArgs)
- 事件声明语法
- 事件触发代码
- 事件订阅者注册和取消注册
- 事件处理方法
- .NET 1.x 与 2.0+ 的考虑因素
- 约定
- 创建自定义事件的步骤
- 示例事件实现
- 处理 .NET Framework 组件触发的事件 - 演练和示例
- Windows 窗体事件
- 可取消事件
- ASP.NET Web 窗体事件
- 来源
- 历史
1. 对读者的假设
本文假设读者对使用 C# 进行 .NET 编程有实际的了解,并理解 .NET Framework 2.0 版本中引入的泛型。如果您不理解泛型,本文仍然会有帮助,因为有一些实现事件的方法不依赖于泛型。本文介绍了泛型和非泛型事件实现技术。
2. 术语和定义
介绍事件及相关概念的文献经常使用多个词或表达来描述任何给定的概念。以下列表列出了大部分这些术语,并简要解释了这些表达背后的概念。
事件、预事件、后事件、状态、状态更改和预期状态更改
术语“事件”通常意味着对象的`状态`发生了变化或即将发生变化。该术语也用于指对象或应用程序中发生的某些活动——例如处理来自最终用户的手势(例如,按钮点击),或在长时间运行的任务期间报告进度。
术语“状态”是指对象或应用程序中一个或多个变量的当前值集合。状态更改意味着对象中一个或多个变量的值已更改。在事件通知过程中,状态更改或预期状态更改是触发事件的主要动机。因此,我们有两种方式根据状态更改来定义事件:紧接在状态更改之前,或紧接在状态更改之后。前者被称为预事件,后者被称为后事件。
后事件宣布状态更改已经发生,而预事件宣布状态更改即将发生。预事件可以实现为可取消的——这意味着订阅者可以在状态更改发生之前取消事件,从而阻止状态更改发生,或阻止长时间运行任务的进一步处理。
事件发布者、事件源、主题
这些是其他类或对象对其状态感兴趣的类或对象。事件发布者维护其内部状态,并通过触发事件或类似的通知机制通知其他类(订阅者)。
事件订阅者、接收器、监听器、观察者
这些是对事件发布者的状态变化(或预期的状态变化)感兴趣的类或对象。这些术语指的是那些通常响应事件发生而执行某些操作的类或对象。
触发、激发或启动事件;通知或事件通知
事件通知(通常表示为“激发事件”、“触发事件”或“启动事件”)通常采用事件发布者调用一个或多个订阅者中的方法的形式。因此,触发事件最终意味着事件发布者中的代码导致一个或多个订阅者中的代码运行。
如果(事件的)没有订阅者向发布者注册,则事件将不会被触发。
请注意,在本文中,事件被描述为“触发”(而不是“激发”或“启动”)。这种约定来自于编写大部分 .NET Framework 的开发团队(Cwalina 和 Abrams,2006)。他们更喜欢“触发”这个词,因为它没有“激发”或“启动”等表达的负面含义。
事件数据、事件相关数据和事件参数(“event args”)
当事件被触发时,发布者通常会包含通过事件通知过程发送给订阅者的数据。这些数据可能与被触发的特定事件相关,并且会引起事件订阅者的兴趣。
例如,当文件被重命名时,可能会触发一个事件。与该特定“文件重命名”事件相关的数据可能包括 (1) 文件更名前的名称,以及 (2) 文件更名后的名称。这些文件名可以构成在“文件重命名”事件触发期间发送给订阅者的事件数据。
委托类型,委托
对 .NET 委托类型的清晰理解对于理解 .NET Framework 中事件的实现至关重要。因此,本文的很大一部分专门用于解释委托和事件之间的关系。
“事件处理程序”的两种含义
本文之外的文献经常使用术语“事件处理程序”来指代 (1) 事件所基于的委托(在发布者中),或 (2) 任何注册到事件的方法(在订阅者中)。此外,Visual Studio 中的 Intellisense 将事件处理方法(在订阅者中)简称为“handler”。为了清晰起见,本文使用“事件处理程序”来指代委托,而使用“事件处理方法”来指代任何注册到事件的方法。
总结一下;“事件处理程序”是事件所基于的委托,而“事件处理方法”是当事件被触发时在订阅者中调用的方法。
事件处理程序是委托,尽管委托不一定是事件处理程序(委托的用途远不止支持事件)。委托将在本文后面更详细地介绍,但仅限于与事件相关的范围。
.NET 事件和 GoF 观察者模式
.NET Framework 中实现的事件(如本文所述)构成了“四人帮”或“GoF”(Gamma 等人,1995)文档中观察者模式的 .NET 优化实现。用于实现事件的 .NET 机制(特别是委托)大大减少了在 .NET 应用程序中实现观察者模式所需的工作量。
3. 委托
为了理解 .NET 应用程序中事件的实现方式,必须清楚地理解 .NET delegate
类型及其在事件实现中的作用。
3.1 委托的定义和用法
委托可以理解为智能容器,它持有对方法的引用,而不是持有对对象的引用。委托可以包含零个、一个或多个方法的引用。要使方法由特定的委托实例调用,该方法必须注册到该委托实例。注册后,该方法会添加到委托的内部方法引用集合(委托的“调用列表”)中。委托可以持有对任何对委托实例可见的类中的静态方法或实例方法的引用。委托实例可以同步或异步调用其引用的方法。当异步调用时,方法在单独的线程池线程上执行。当委托实例被调用(“触发”)时,委托引用的所有方法都会自动由委托调用。
委托不能包含对任何方法的引用。委托**只能**包含对方法签名**精确**匹配委托签名的方法的引用。
考虑以下委托声明
public delegate void MyDelegate(string myString);
请注意,委托声明看起来像方法声明,但没有方法体。
委托的签名决定了委托可以引用的方法的签名。因此,上面的示例委托(MyDelegate
)只能持有返回 void
并接受单个字符串参数的方法的引用。因此,以下方法可以注册到 MyDelegate
的实例:
private void MyMethod(string someString)
{
// method body here.
}
然而,以下方法不能被 MyDelegate
实例引用,因为它们的签名与 MyDelegate
不匹配。
private string MyOtherMethod(string someString)
{
// method body here.
}
private void YetAnotherMethod(string someString, int someInt)
{
// method body here.
}
声明新的委托类型后,必须创建该委托的实例,以便方法可以注册到该委托实例并最终由其调用。
// instantiate the delegate and register a method with the new instance.
MyDelegate del = new MyDelegate(MyMethod);
委托实例化后,可以将其他方法注册到委托实例,如下所示:
del += new MyDelegate(MyOtherMethod);
此时,委托可以像这样被调用:
del("my string value");
而且,由于 MyMethod
和 MyOtherMethod
都已注册到 MyDelegate
实例(名为 del
),因此当上面这行代码执行时,该实例将调用 MyMethod
和 MyOtherMethod
,并向每个方法传递字符串值“my string value”。
委托和重载方法
在重载方法的情况下,只有签名与委托签名完全匹配的特定重载才能被委托引用(或注册到)。当您编写将重载方法注册到委托实例的代码时,C# 编译器将自动选择并注册具有匹配签名的特定重载。
因此,例如,如果您的应用程序声明了以下委托类型...
public delegate int MyOtherDelegate(); // returns int, no parameters
...并且您将一个名为 MyOverloadedMethod
的重载方法注册到 MyOtherDelegate
的实例中,像这样...
anotherDel += new MyOtherDelegate(MyOverloadedMethod);
... C# 编译器将只注册具有匹配签名的特定重载。在以下两个重载中,只有第一个会注册到 MyOtherDelegate
类型的 anotherDel
实例中。
// requires no parameters - so can be registered with a MyOtherDelegate
// instance.
private int MyOverloadedMethod()
{
// method body here.
}
// requires a string parameter - so cannot be registered with a MyOtherDelegate instance.
private int MyOverloadedMethod(string someString)
{
// method body here.
}
单个委托无法选择性地注册或调用两个(多个)重载。如果您需要调用两个(多个)重载,那么您将需要额外的委托类型——每种签名对应一种委托类型。您的应用程序特定逻辑将决定调用哪个委托,从而决定调用哪个重载(由具有相应签名的委托调用)。
3.2 为什么要使用委托?
如果这是您第一次接触委托,您可能会想:“为什么要费劲呢?直接调用方法更简单——那么使用委托有什么好处呢?”
必要的间接性
(针对上面的“为什么要费劲?”问题)一个简短的回答是,我们编写的代码或我们使用的组件并非总能“知道”在特定时间点要调用哪个具体方法。因此,关于委托的一个重要观点是,它们提供了一种 .NET 组件调用您代码的方式——而 .NET 组件无需知道您的代码的任何信息,只需知道方法签名(由委托类型强制要求)。例如,像 Timer 组件这样的 .NET Framework 组件经常需要执行您编写的代码。由于 Timer 组件不可能知道要调用哪个具体方法,它会指定一个委托类型(以及因此是方法的签名)来调用。然后,您通过将您的方法注册到 Timer 组件所期望的委托类型的委托实例来将您的方法(具有必需的签名)连接到 Timer 组件。Timer 组件然后可以通过调用委托来运行您的代码,而委托又会调用您的方法。请注意,Timer 组件仍然对您的具体方法一无所知。Timer 组件所知道的只是委托。反过来,委托知道您的方法,因为您将您的方法注册到了该委托。最终结果是 Timer 组件导致您的方法运行,但却不知道您的具体方法。
就像上面的 Timer 组件示例一样,我们可以以一种方式利用委托,使我们能够编写代码,而无需我们的代码“知道”在特定点最终将调用的特定方法。我们的代码可以在该点调用委托实例,而不是直接调用方法,而委托实例又会调用注册到该委托实例的任何方法。最终结果是调用了一个兼容的方法,即使要调用的特定方法没有直接写入我们的代码中。
同步和异步方法调用
所有委托本质上都提供同步和异步方法调用。因此,通过委托实例调用方法的另一个常见原因是以异步方式调用方法——在这种情况下,被调用的方法在单独的线程池线程上运行。
事件基础
正如您将在本文后面看到的那样,委托在 .NET Framework 的事件实现中扮演着不可或缺的角色。简而言之,委托在事件发布者及其订阅者之间提供了一个必要的间接层。这种间接是必要的,以保持发布者和订阅者之间的清晰分离——这意味着可以添加和删除订阅者,而无需以任何方式修改发布者。在事件发布的情况下,使用委托使得事件发布者可以对其任何订阅者一无所知,同时仍将事件和相关的事件数据广播给所有/所有订阅者。
其他用途
委托在 .NET 应用程序中扮演着重要的角色,超出已列出的范围。这些其他角色将不再在此处介绍,因为本文的目的是只关注委托在 .NET 应用程序事件实现中的基础作用。
3.3 委托内部机制
声明委托会导致创建新类
您编写的委托声明足以定义一个完整的全新委托类。C# 编译器会接收您的委托声明,并在输出程序集中插入一个新的委托类。该新类的名称就是您在委托声明中提供的委托类型名称。您在委托声明中指定的签名成为新类中用于调用委托的所有引用方法(特别是 Invoke
和 BeginInvoke
方法)的方法的签名。这个新类扩展(继承)System.MulticastDelegate
。因此,您新委托类中可用的大多数方法和属性都来自 System.MulticastDelegate
。Invoke
、BeginInvoke
和 EndInvoke
方法由 C# 编译器在输出程序集中创建新类时插入(这些是您可以调用的方法,以使委托调用所有引用方法——Invoke
用于同步调用,BeginInvoke
和 EndInvoke
用于异步调用)。
从您的委托声明创建的新类可以理解为是一个完整且功能齐全的 MulticastDelegate
实现,它具有您在委托声明中提供的类型名称,并且能够调用具有您在委托声明中提供的特定签名的方法。
例如,当 C# 编译器遇到以下委托声明时...
public delegate string MyFabulousDelegate(int myIntParm);
... 编译器会将一个名为 MyFabulousDelegate
的新类插入到输出程序集中。MyFabulousDelegate
类的 Invoke
、BeginInvoke
和 EndInvoke
方法在其各自的方法签名中包含 int
参数和返回的 string
值。
应该注意的是,MulticastDelegate
是一个特殊的类,编译器可以从它派生,但您不能显式地从它派生。您使用 C# 的 delegate
关键字和相关语法的方式就是您指示 C# 编译器为您的目的扩展 MulticastDelegate
的方式。
多播的含义
System.MulticastDelegate
中“多播”的含义是,委托能够持有对多个方法的引用——而不仅仅是一个方法。在持有对多个方法引用的委托实例的情况下,当委托实例被调用时,所有引用的方法都会被调用。
委托是不可变的
委托实例是不可变的——这意味着一旦创建了委托实例,就无法修改它。因此,当您向委托注册方法时,实际发生的是创建了一个新的委托实例,其调用列表中包含了该附加方法。如果您从委托实例中取消注册方法,则会返回一个新的委托实例,其调用列表中省略了已取消注册的方法。如果您要创建一个特定委托类型的新的对象变量,然后将其设置为等于一个现有委托实例(该特定类型),您将获得该委托的一个完整且独立的副本。对该副本的修改(例如,注册一个附加方法)只会影响该副本。原始实例的调用列表将保持不变。
委托不是函数指针
最后,C 和 C++ 程序员会认识到委托与 C 风格的函数指针相似。然而,一个重要的区别是,委托不仅仅是指向原始内存地址的指针。相反,委托实例是类型安全的,由 .NET CLR 管理的对象,它们专门引用一个或多个“方法”(而不是内存地址)。
3.4 委托都是一样的(没有根本上不同的委托类型)
这些陈述都是正确的:
“如果你见过一个委托,你就见过所有的委托。”
或
“所有的委托都是平等的。”
或
“委托就是委托就是委托。”
当您阅读有关不同“类型”委托的内容时,您应该明白,在内部,所有委托都是相同的。对于 .NET Framework 提供的委托和您为自己目的创建的委托,这都是如此。说“它们都是相同的”具体意味着所有委托 (1) 都继承自 System.MulticastDelegate
,而 System.MulticastDelegate
又继承自 System.Delegate
;并且 (2) 提供相同的一组成员,包括 Invoke
、BeginInvoke
和 EndInvoke()
方法等。
委托类型之间的区别仅在于:
- 委托的**类型名称**。
- 委托的**签名**——包括返回类型以及参数的数量和类型。
- 委托的**预期用途或作用**。
例如,以泛型谓词委托(System.Predicate<T>
)为例。以下是使其成为“谓词委托”的原因:
- 类型名称:
Predicate
- **签名**:返回
bool
,接受一个类型为object
的参数,其类型(作为泛型)可以在设计时指定。 - **预期用途或作用**:此委托将引用一个方法,该方法定义了一组标准并确定指定的对象是否满足这些标准。
除了类型名称、签名和预期用途之外,Predicate<T>
委托还拥有任何其他委托所拥有的一组相同的成员,包括 Invoke
、BeginInvoke
等。因此,这就是“委托都是一样的”这句话的含义。
明确地说,Predicate<T>
委托**没有**任何额外的辅助方法或属性来帮助它实现其预期作用。如果某些委托具有其他委托没有的属性或方法,那么这些委托将具有不同或独特的功能,因此我们就无法说它们都是相同的。
关于预期用途的观点;您可以自由地将任何委托用于委托创建者**不打算**用于的目的——因为委托不与任何特定用途绑定。例如,您可以使用 Predicate<T>
委托调用任何返回 bool
并接受单个 object
类型参数的方法——即使这些方法不确定指定对象是否满足任何标准(这是 Predicate<T>
委托的预期用途)。当然,**您不应将委托用于其预期用途之外的目的**,因为 .NET Framework 提供预构建委托(如 Predicate<T>
)的许多价值在于我们无需深入研究大量代码即可了解它们的作用。
委托类型的名称传达了它在代码中的预期作用。因此,请务必使用适当的委托类型,或者创建您自己的、具有信息性类型名称的委托,即使存在具有必需签名但考虑到您的特定用法可能具有误导性名称的另一个委托。
4. 委托与事件的关系
.NET 编程中的事件基于委托。具体来说,事件可以理解为围绕特定委托提供了一个概念上的封装。事件然后控制对底层委托的访问。当客户端订阅事件时,事件最终会将订阅方法注册到底层委托。然后,当事件被触发时,底层委托会调用注册到它的每个方法(委托)。因此,在事件的上下文中,委托充当触发事件的代码和响应事件执行的代码之间的中介——从而解耦事件发布者和它们的订阅者。
事件本身不维护订阅者列表。相反,事件控制对某个底层订阅者列表的访问——该列表通常实现为委托(尽管其他列表类型对象或集合可以代替委托)。
4.1 事件处理程序(通常)
支持事件而存在的委托被称为“事件处理程序”。明确地说,一个“事件处理程序”是一个委托,尽管委托通常不是事件处理程序。
不幸的是,许多撰写事件相关文章的作者都用“事件处理程序”这个术语来指代 (1) 事件所基于的委托,以及 (2) 当事件被触发时由委托调用的方法。为了避免由此产生的混淆,本文仅将“事件处理程序”用于指代委托,而将“事件处理方法”用于指代任何注册到委托的方法。
自定义事件处理程序
您可以定义自己的事件处理程序(委托),也可以使用 .NET Framework 提供的事件处理程序之一(即 System.EventHandler
,或泛型 System.EventHandler<TEventArgs>
)。以下示例事件声明使用自定义事件处理程序,而不是使用 Framework 提供的事件处理程序。
考虑以下内容
Line 1: public delegate void MyDelegate(string whatHappened);
Line 2: public event MyDelegate MyEvent;
第 1 行声明了一个委托类型,任何方法都可以赋值给它——只要该方法返回 void
并接受单个 string
参数
public
— 作用域,指定我们类之外的对象可以引用该委托。如果委托类型声明在事件发布类中,那么它需要是公共作用域,以便事件订阅者可以看到它并声明它的实例来注册它们的事件处理方法(稍后会详细介绍)。delegate
— 用于在 .NET Framework 中声明自定义委托的关键字。void
— 返回类型。这是委托签名的一部分,因此是注册方法必须指定的返回类型。MyDelegate
— 委托的类型名称。(string whatHappened)
— 签名的其余部分。任何注册到事件的方法都必须接受一个单独的string
参数(除了返回void
)。
第 2 行以委托类型声明事件。请注意,事件(名为 MyEvent
)的声明方式与方法声明非常相似——但其数据类型指定为委托类型
public
— 作用域,指定我们类之外的对象可以订阅该事件。event
— 用于定义事件的关键字。MyDelegate
— 事件的数据类型(这是在第 1 行定义的自定义委托类型)。MyEvent
— 事件的名称。
第 1 行中声明的委托只是一个普通委托(所有委托都是如此),可以用于委托可以履行的任何目的。第 2 行(即委托类型的用法)将该委托变成了事件处理程序。为了表明特定委托类型被用作事件处理程序,出现了一种命名约定,即委托类型名称以“Handler”结尾(稍后会详细介绍)。
标准化事件处理程序
虽然您可以创建自己的事件处理程序(有时您可能需要这样做),但在 .NET Framework 提供的 EventHandler
委托之一适用于您的特定事件实现的情况下,您应该使用它。许多事件都使用具有常见或相同签名的事件处理程序。因此,与其用许多仅类型名称不同的委托来混淆您的源代码,您可以/应该使用内置的事件处理程序,因为这样做可以减少您需要编写和维护的代码量,并使您的代码更容易理解。例如,如果阅读您代码的人看到您基于 System.EventHandler
委托实现事件,那么他们无需进一步查看就可以自动了解有关您的事件实现的许多信息。
4.2 非泛型 System.EventHandler 委托
在 .NET Framework 1.x 版本中可用,非泛型 System.EventHandler
委托强制执行事件处理程序不返回值并接受两个参数的约定(下面将更详细地描述),第一个是 object
类型参数(用于保存对触发事件的类的引用),第二个是 System.EventArgs
类型或其子类(用于保存任何事件数据)。System.EventArgs
将在后面介绍。
.NET Framework 声明 System.EventHandler
委托的方式如下。
public delegate void EventHandler(object sender, EventArgs e);
4.3 泛型 System.EventHandler<TEventArgs> 委托
自 .NET Framework 2.0 版本以来可用,泛型 System.EventHandler
委托强制执行与非泛型版本相同的签名约定——但为第二个 System.EventArgs
参数接受泛型类型参数。
此内置委托的声明强制要求类型 TEventArgs
必须是 System.EventArgs
类型(当然也包括其子类)
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e)
where TEventArgs : EventArgs;
现在假设您想强类型化发件人,而不是将其类型化为 object
。您可以利用泛型创建自己的泛型事件处理程序:
public delegate void MyGenericEventHandler<T, U>(T sender,
U u) where U : EventArgs;
然后,您可以使用这个自定义泛型事件处理程序来额外指定一个类型安全的 sender
参数(即,从而限制可以作为触发事件的对象类型)
public event MyGenericEventHandler<MyPublisher, MyEventArgs> MyEvent;
这里的目的是,此事件将仅由 MyPublisher
类型的对象触发。因此,事件的订阅者将只能订阅由 MyPublisher
类发布的事件。
5. 事件参数 (EventArgs)
事件参数——有时被称为“事件 args”——构成了事件发布者在事件触发时发送给订阅者的数据。据推测,这些数据与事件的发生相关。例如,当触发“文件刚刚被删除”事件时,事件参数可能包括文件更名之前的名称,以及更名之后的文件名称。事件处理方法可以读取事件参数(被称为“事件数据”)以了解有关事件发生的更多信息。
5.1 System.EventArgs 的作用
您有两种基本方式将事件参数包含在您的事件中。
- 您可以将所有事件参数封装为派生自
System.EventArgs
的类的属性。在运行时,当事件触发时,该类的实例会被发送给事件订阅者。事件订阅者将事件参数作为该类的属性读取。 - 您可以避免使用
System.EventArgs
,而是声明单独的事件参数——就像在方法声明中包含参数一样。**不鼓励这种方法**,原因在 5.2 节中描述。
强烈建议使用上面列出的第一种替代方案,并且 .NET Framework 通过 System.EventArgs
类内置了对它的支持。按照约定,.NET Framework 组件中实现的事件将其事件参数作为 System.EventArgs
的实例,或作为 System.EventArgs
的事件特定子类提供。
有些事件不携带任何数据。在这种情况下,System.EventArgs
用作占位符,主要目的是在所有事件中保持一致的事件处理程序签名,无论事件是否携带数据。在没有数据的事件情况下,事件发布者在触发事件时发送值 System.EventArgs.Empty
。
5.2 扩展 System.EventArgs
System.EventArgs
的存在及其推荐用法是为了支持事件实现约定。事件的发布者当然可以在不使用 System.EventArgs
或其任何子类的情况下指定事件数据。在这种情况下,委托签名可以指定每个参数的类型和名称。然而,这种方法的问题在于,这样的签名将事件发布者与所有订阅者绑定在一起。如果将来您想修改参数,那么所有订阅者也必须进行修改。因此,建议将所有事件数据封装在 System.EventArgs
的子类中,因为这样做可以减少后续更改发送给事件订阅者的值的数量和类型所需的工作量。
为了说明发送 System.EventArgs
子类实例与发送单独事件参数之间的权衡,请考虑一个场景,您想向事件数据添加一个 string
值。如果您将事件数据指定为委托签名中的单独参数(而不是子类化 System.EventArgs
),那么您的事件的所有订阅者都必须修改以接受额外的 string
参数。即使不关心此额外 string
值的订阅者也必须修改以接受它,因为自定义事件处理程序签名将发生更改。如果您改为子类化 System.EventArgs
,那么您所要做的就是向您的类添加一个新的 string
属性。事件签名不会更改,因此任何现有订阅者中使用的事件处理程序签名也不会更改。不关心新 string
属性的订阅者将无需触及,因为事件处理程序签名不会更改——他们可以简单地忽略额外的 string
属性。关心新 string
值的订阅者将能够将其作为 EventArgs
子类的属性读取。
这是一个封装单个 string
值的 EventArgs
子类的示例:
public class FileDeletedEventArgs : System.EventArgs
{
// Field
string m_FileName = string.empty;
// Constructor
FileDeletedEventArgs(string fileName)
{
m_FileName = fileName;
}
// Property
public string FileName
{
get { return m_FileName; }
}
}
5.3 System.ComponentModel.CancelEventArgs 的作用
System.ComponentModel.CancelEventArgs
派生自 System.EventArgs
,旨在支持可取消事件。除了 EventArgs
提供成员之外,CancelEventArgs
还提供布尔类型 Cancel
属性,当事件订阅者将其设置为 true
时,事件发布者将使用它来取消事件。
本文第 16 节更详细地介绍了可取消事件(点击此处立即前往)。
6. 事件声明语法
6.1 事件声明语法备选方案
event
关键字用于正式声明事件。有两种有效的事件声明语法替代方案。无论您编写哪种语法,C# 编译器都会将两种属性声明都转换为输出程序集中的以下三个组件。
- 私有作用域事件处理程序(或功能等效的数据结构)。委托是私有作用域的,以防止外部代码调用事件,从而保持封装。
- 公共作用域
Add
方法;用于向私有事件处理程序添加订阅者。 - 公共作用域
Remove
方法,用于从私有事件处理程序中移除订阅者。
1. 字段式语法
public event TheEventHandler MyEvent;
字段式语法在代码中声明事件只需一两行(事件一行,关联的事件处理程序一行——如果/当不使用内置的 EventHandler
委托时)。
2. 属性式语法
public event TheEventHandler MyEvent
{
add
{
// code here adds the incoming delegate instance to underlying list of
// event handlers
}
remove
{
// code here removes the delegate instance from the underlying list of
// event handlers
}
}
属性式语法看起来与典型的属性声明非常相似,但用显式的 add
和 remove
块代替了“getter”和“setter”块。它们不是检索或设置私有成员变量的值,而是将传入的委托实例添加到/从底层事件处理程序或其他扮演类似角色的数据结构中。
线程考虑
字段式语法是自动线程安全的。
public event FileDeletedHandler FileDeleted;
属性式语法的线程安全性取决于您如何实现它。以下是一个线程安全版本:
private readonly object padLock = new object();
public event System.EventHandler<filedeletedeventargs />FileDeleted
{
add
{
lock (padLock)
{
FileDeleted += value;
}
}
remove
{
lock (padLock)
{
FileDeleted -= value;
}
}
}
如果线程安全不是问题,您可以省略 lock{}
块和 padLock
变量声明。
6.2 选择字段式语法和属性式语法的考虑因素
在选择语法替代方案时,请考虑属性式语法比字段式语法提供了对事件实现的更多控制。虽然字段式语法将被编译成与属性式语法生成的 IL 非常相似的 IL,但字段式语法并没有为您提供相同的机会来显式控制事件实现。
使用属性式语法可以更仔细地控制订阅者与事件处理程序(委托)的注册和取消注册。它还可以让您更轻松、更明确地实现您选择的特定锁定机制,以解决线程安全问题。属性式语法还可以让您实现除委托之外的自定义事件处理程序机制。您可能希望在需要支持许多可能的事件,而其中只有少数事件在任何给定时间都有订阅者的情况下这样做。在这种情况下,您的事件实现将使用哈希表或类似数据结构,而不是单个委托,来维护所有可能的事件和任何关联监听器的列表。
6.3 不使用事件的委托发布/订阅机制(切勿这样做)
应该清楚地理解,事件不是委托——尽管事件非常依赖于委托,并且在某些方面可以被视为委托实现的一种形式。事件也不是委托实例,即使它们可以以非常相似的方式使用。
虽然您可以省略 event 关键字(从而省略事件的正式声明),而只是使用公共委托来提供发布和订阅通知机制,但您绝不应该这样做。公共委托(与事件声明相比)的问题在于,发布类之外的方法可以导致公共作用域的委托调用其引用的方法。这违反了基本的封装原则,并且可能是难以调试的重大问题(竞争条件等)的来源。因此,您应该只通过使用 event 关键字来实现事件。当委托实现以支持事件时,委托——即使声明为定义类的公共成员——也只能在定义类内部调用(通过触发事件),而其他类只能通过事件订阅和取消订阅底层委托。
7. 事件触发代码
对于每个事件,发布者都应包含一个受保护的虚拟方法,负责触发事件。这将允许子类更容易地访问基类事件。当然,将此方法设为受保护和虚拟的建议仅适用于未密封类中的非静态事件。
protected virtual void OnMailArrived(MailArrivedEventArgs)
{
// Raise event here
}
一旦事件以及任何关联的委托和发布方法被定义,发布者就需要触发事件。触发事件通常应是两步过程。第一步是检查是否有任何订阅者。第二步是触发事件,但仅当存在任何订阅者时。
如果没有订阅者,那么委托将测试为 null
。以下逻辑会触发事件,但仅当事件有任何订阅者时。
if (MyEvent != null)
{
MyEvent(this, EventArgs.Empty);
}
存在一种可能性,即在检查 null
和实际触发事件的行之间,事件可能会被清除(由在另一个线程中执行的代码)。这种情况构成竞争条件。因此,建议创建、测试并触发事件事件处理程序(委托)的副本,如下所示:
MyEventHandler handler = MyEvent;
if (handler != null)
{
handler (this, EventArgs.Empty)
}
订阅者中事件处理方法中抛出的任何未处理异常都将传播到事件发布者。因此,触发事件应仅在 try/catch
块内进行尝试。
public void RaiseTheEvent(MyEventArgs eventArgs)
{
try
{
MyEventHandler handler = MyEvent;
if (handler != null)
{
handler (this, eventArgs)
}
}
catch
{
// Handle exceptions here
}
}
事件可以有多个订阅者——当事件处理程序被 [handler (this, eventArgs)
] 行调用时,事件处理程序(委托)会依次调用每个订阅者。当第一个未处理异常由订阅者抛出时,上面代码块中使用的事件处理程序将停止遍历其调用列表(已订阅的事件处理方法)。因此,例如,如果有 3 个订阅者,并且第二个订阅者在被委托调用时抛出未处理异常,那么第三个订阅者将永远不会收到事件通知。如果您希望每个订阅者都收到事件通知,即使其他订阅者抛出未处理异常,那么您可以使用以下逻辑,该逻辑显式循环遍历事件处理程序的调用列表:
public void RaiseTheEvent(MyEventArgs eventArgs)
{
MyEventHandler handler = MyEvent;
if (handler != null)
{
Delegate[] eventHandlers = handler.GetInvocationList();
foreach (Delegate currentHandler in eventHandlers)
{
MyEventHandler currentSubscriber = (MyEventHandler)currentHandler;
try
{
currentSubscriber(this, eventArgs);
}
catch (Exception ex)
{
// Handle exception here.
}
}
}
}
8. 事件订阅者注册和取消注册
根据设计,事件的发布者对任何订阅者一无所知。因此,订阅者的职责是向事件发布者注册或取消注册。
8.1 注册订阅者
要订阅事件,订阅者需要三样东西:
- 对发布感兴趣事件的对象的引用
- 事件所定义的委托的实例
- 当发布者触发事件时将被调用的方法
然后订阅者将其事件处理程序(委托)实例注册到发布者,如下所示:
thePublisher.EventName += new
MyEventHandlerDelegate(EventHandlingMethodName);
在上面这行中...
thePublisher
是对将触发感兴趣事件的对象的引用。请注意事件EventName
的访问方式,就像它是thePublisher
的公共属性一样。+=
运算符用于将委托实例添加到发布者中事件处理程序的调用列表。请记住,多个订阅者可以注册到事件。使用+=
运算符将当前订阅者附加到底层委托的调用列表。MyEventHandlerDelegate
是要使用的特定事件处理程序委托的引用(如果不是内置的EventHandler
委托之一)。- 最后,
EventHandlingMethodName
提供了订阅类中当事件触发时将被调用的方法的名称。
警告:在将事件订阅者注册到发布者时,不要使用 =
运算符。这样做会用当前订阅者替换任何/所有当前已注册的事件订阅者。相反,请务必使用 +=
运算符,以使当前订阅者**附加**到事件处理程序的调用列表。
8.2 取消注册订阅者
订阅者可以从发布者取消注册,如下所示:
thePublisher.EventName -=
EventHandlerDelegate(EventHandlingMethodName);
-=
运算符用于从发布者中的调用列表移除委托实例。
如果订阅者未明确地从事件中取消注册,则在对象被处置时会自动取消注册。
9. 事件处理方法
事件处理方法是事件订阅者中的方法,由事件发布者在触发事件时执行。请注意,一些描述 .NET 中事件的文献将这些方法称为“事件处理程序”,尽管从技术上讲,“事件处理程序”是事件所基于的委托,而不是该委托引用的任何方法。
事件处理方法的重要要求是其签名必须与事件所定义的事件处理程序(委托)的签名匹配。
您还应仔细考虑事件处理方法中可能抛出或捕获的任何异常的后果。在事件处理方法中未捕获的异常将传播到事件发布者。
10. .NET 1.x 与 2.0+ 的考虑因素
本节介绍的概念和功能是在 .NET Framework 2.0 版本中引入的。这些新功能相当于快捷方式,如果运用得当,可能会简化您的代码。
然而,存在一种风险,即不当使用其中一些功能可能会使您的事件实现代码更难以理解。例如,如果您使用了一个由 30 多行代码组成的“匿名方法”(下面介绍),那么您的事件实现可能会比一个将这 30 多行代码放在命名方法中的等效实现更难以阅读。
重要的是要理解,这些 2.0+ 概念和功能并没有对 .NET Framework 应用程序中事件的实现方式带来任何根本性的改变。相反,它们主要是为了简化我们实现事件的方式。
10.1 泛型
除了本文其他地方介绍的泛型特定功能(例如 System.EventHandler<T>
),需要注意的是,任何以任何方式依赖泛型的事件实现技术都无法在 .NET 1.x 应用程序中使用,因为泛型是在 .NET Framework 2.0 版本中首次引入的。
10.2 委托推断
C# 2.0(及更高版本)编译器足够智能,可以确定特定事件所实现的委托类型。这种“委托推断”功能使您可以在将事件处理方法注册到事件的代码中省略声明所需的委托。
考虑以下 1.x 代码,它将一个事件处理方法注册到一个事件。这段代码显式实例化了事件处理程序(委托),以便将关联的方法注册到事件中。
thePublisher.EventName += new MyEventHandlerDelegate(EventHandlingMethodName);
以下 2.0+ 代码使用委托推断将相同的方法注册到事件。请注意,以下代码似乎直接将事件处理方法注册到事件中。
thePublisher.EventName += EventHandlingMethodName;
当您像这样直接将方法名称分配给事件时,C# 编译器会确保方法签名与事件所基于的事件处理程序的签名匹配。然后,C# 编译器会将所需的委托注册代码(即 ... += new MyEventHandlerDelegate(EventHandlingMethodName);
)插入到输出程序集中。
这种简化的语法是由 C# 编译器实现的,而不是由于 .NET Framework 中事件实现方式的任何根本性改变。明确地说,C# 2.0(及更高版本)中的事件**不能**直接引用方法。编译器为我们做的是在输出程序集中提供(仍然)必需的委托语法——就像我们显式实例化了委托一样。
10.3 匿名方法
匿名方法是您传递给委托的代码块(而不是将方法的名称传递给委托以供引用)。当 C# 编译器遇到匿名方法时,它会在输出程序集中创建一个完整的方法,其中包含您提供的代码块。编译器为该方法提供一个名称,然后从关联的委托实例中引用该 [新] 方法(所有这些都发生在输出程序集中)。该方法被称为“匿名”是因为您在使用它时不知道它的名称(它在您的源代码中没有名称)。
匿名方法为您编写更简单代码提供了机会。考虑以下将简短事件处理方法注册到事件的代码:
static void EventHandlingMethod(object sender, EventArgs e)
{
Console.WriteLine("Handled by a named method");
}
thePublisher.EventName += new MyEventHandlerDelegate(EventHandlingMethod);
以上逻辑可以用匿名方法重写,如下所示:
thePublisher.EventName += delegate {
Console.WriteLine("Handled by anonymous method");
};
匿名方法旨在简化我们的代码。当代码块相对较短时,这种简化可以实现。在上面的示例中,使用匿名方法语法的逻辑版本易于阅读,因为我们无需查找任何单独的事件处理方法即可了解订阅者在事件触发时将如何响应。然而,在代码块包含许多行代码的情况下,匿名方法语法可能更难以阅读(比引用命名方法的逻辑更难)。一些作者建议,包含三四行以上代码的代码块不应实现为匿名方法。这些更长的代码块应改为放在命名方法中,以提高可读性。
总结到目前为止提出的替代方案,以下代码演示了将事件处理方法注册到事件的三种选项。第一种演示了适用于所有 .NET Framework 版本的显式方法。第二种演示了委托推断。第三种演示了匿名方法的使用:
// Option 1 - explicit delegate creation with a named method
thePublisher.EventName += new MyEventHandlerDelegate(EventHandlingMethod);
// Option 2 - delegate inference
thePublisher.EventName += EventHandlingMethod;
// Option 3 - anonymous method
thePublisher.EventName += delegate(object sender, EventArgs e) {
Console.WriteLine("handled by anonymous method");
// You can access the sender and e parameters here if necessary
};
// Event handling method used in options 1 and 2
static void EventHandlingMethod(object sender, EventArgs e)
{
Console.WriteLine("Handled by a named method");
}
10.4 分部类
分部类与事件的实现相关,因为 Visual Studio 会将事件注册代码和事件处理方法存根放在与给定 Windows 窗体类关联的分部类文件中。点击此处可查看第 15.1 节,该节更详细地介绍了分部类中事件的实现。
11. 约定
以下约定是从许多资源中收集而来,包括 .NET Framework 的作者和其他知名行业专家(完整列表请参阅本文末尾的参考文献列表)。
11.1 事件发布者约定
事件名称
- 选择一个清晰地传达事件所代表的状态变化的名称。
- 事件可以分为 (1) 状态改变发生前触发的事件;和 (2) 状态改变发生后触发的事件。因此,事件名称应选择以反映事件的“之前”或“之后”[状态改变]方面。
示例——状态更改前触发的事件
FileDownloading(文件正在下载)
TemperatureChanging(温度正在变化)
MailArriving(邮件正在到达)
示例——状态改变后触发的事件
FileDownloadCompleted(文件下载完成)
TemperatureChanged(温度已改变)
MailArrived(邮件已到达)
System.EventArgs 子类(适用时)
- 对于必须或可能(将来)携带自定义事件数据的事件,您应该创建一个新类,该类 (1) 扩展
System.EventArgs
,并且 (2) 实现包含和公开您的自定义事件数据所需的成员(例如,属性)。 - 唯一不应该子类化
EventArgs
的情况是,您确定您的事件永远不会携带事件数据。 - 您的
EventArgs
子类的名称应该是事件的名称,并附加“EventArgs”。
示例 EventArgs
子类名称
DownloadCompletedEventArgs
TemperatureChangedEventArgs
MailArrivedEventArgs
对于不包含数据且将来也不会包含数据的事件,建议传递 System.EventArgs.Empty
。这种推荐做法旨在即使对于没有事件数据的事件,也能保持事件实现约定。如果您的事件将来可能会携带事件数据,即使在最初实现时没有,那么您也应该创建 System.EventArgs
的子类,并在您的事件中使用它。此建议的主要好处是,您将来能够向您的子类添加数据(属性),而不会破坏与现有订阅者的兼容性。
事件处理程序(委托)名称
- 如果您使用的是 .NET Framework 1.x,那么您应该使用内置的
System.EventHandler
委托。 - 如果您正在使用 .NET Framework 2.0 或更高版本(适用于发布者和订阅者),那么您可以使用泛型
System.EventHandler<TEventArgs>
委托。 - 如果您创建自己的委托,则委托名称应由事件名称加上“Handler”一词组成。
自定义事件处理程序(委托)名称示例
DownloadCompletedHandler
TemperatureChangedHandler
MailArrivedHandler
事件处理程序(委托)签名
如上文“委托名称”所述,您应该使用 .NET Framework 提供的 System.EventHandler
委托之一。在这些情况下,委托签名当然已经为您确定,并自动符合返回 void
并接受推荐参数的约定。
以下建议在 .NET Framework 提供的 System.EventHandler
委托中实现。如果您创建自己的事件处理程序,则应遵循这些建议,以便与 .NET Framework 的实现保持一致。
- 委托应始终返回
void
。在事件处理程序的情况下,向事件发布者返回值根本没有意义。请记住,事件发布者,根据设计,对其订阅者一无所知。事实上,委托,根据设计,充当事件发布者与其订阅者之间的中介。因此,发布者不应该知道其订阅者的任何信息——包括接收返回值的可能性。是委托在调用每个订阅者,所以任何返回值都只会到达委托,并且无论如何都不会到达发布者。这个理由对于避免使用
out
或ref
参数修饰符的输出参数也同样适用。订阅者的输出参数永远不会传播到发布者。
- 第一个参数应为
object
类型,并命名为sender
。第一个参数用于保存对触发事件的对象的引用。传递对事件发布者的引用使事件订阅者能够区分给定事件的多个可能发布者。如果没有对发布者的引用,事件处理方法将无法识别或对触发特定事件的特定发布者采取行动。
sender
的数据类型是object
,因为几乎任何类都可以触发事件。避免强类型化的sender
参数允许在所有事件中采用一个一致的事件处理程序签名。在必要时,事件处理方法可以将sender
参数转换为特定的事件发布者类型。静态事件应将
null
作为sender
的值传递,而不是省略sender
参数。
- 第二个参数应命名为“
e
”,并且应为System.EventArgs
类型或您的自定义System.EventArgs
子类(例如MailArrivedEventArgs
)。在可取消事件的情况下,第二个参数要么是
System.ComponentModel.CancelEventArgs
类型,要么是其自定义子类。在不携带事件数据的事件情况下,您应该指定System.EventArgs
作为第二个参数类型。在这种情况下,当事件触发时,System.EventArgs.Empty
将被指定为该参数的值。推荐这种做法是为了保持符合约定——以便所有事件处理程序签名都包含EventArgs
参数——即使对于没有EventArgs
的事件也是如此。显然,根据约定,拥有一个一致的签名比拥有多个事件处理程序签名更重要——即使在其中一个参数永远不会被使用的情况下。
示例(事件未发送自定义数据)
delegate void DownloadCompletedHandler(object sender, EventArgs e);
delegate void TemperatureChangedHandler (object sender, EventArgs e);
delegate void MailArrivedHandler (object sender, EventArgs e);
示例(事件发送自定义数据)
delegate void DownloadCompletedHandler(object sender,
DownloadCompletedEventArgs e);
delegate void TemperatureChangedHandler (object sender,
TemperatureChangedEventArgs e);
delegate void MailArrivedHandler (object sender,
MailArrivedEventArgs e);
事件声明
- 假设事件要提供给发布类外部的代码使用,那么事件将使用
public
关键字声明(使其可供发布类外部的代码访问)。 - 事件所基于的事件处理程序被指定为事件的**类型**——类似于在典型的属性或方法声明中指定数据类型的方式。
示例(使用内置泛型 System.EventHandler<TEventArgs>
委托)
public event System.EventHandler<mailarrivedeventargs> MailArrived;
示例(使用自定义事件处理程序)
public delegate void MailArrivedHandler (object sender,
MailArrivedEventArgs e);
public event MailArrivedHandler<mailarrivedeventargs> MailArrived;
触发事件的方法
- 建议创建一个单独的方法来负责触发事件,而不是在代码中内联触发事件。然后,您可以根据需要在代码中调用该方法。
- 该方法的名称应为“
On
”后接事件名称。 - 如果您的事件使用自定义
EventArgs
子类,则触发事件的方法应至少接受一个参数,该参数是为自定义事件数据定义的特定EventArgs
子类类型。 - 对于未密封的非静态类,该方法应实现为
virtual
,可访问性指定为protected
,以便派生类可以轻松通知已注册到基类的客户端。 - 对于密封类,方法的访问权限当然应该设置为
private
,因为事件的触发不应该从类外部发起。
示例(每个都将自定义 EventArgs
子类类型作为参数)
OnDownloadCompleted(DownloadCompletedEventArgs)
{
// Raise event here
}
private OnTemperatureChanged(TemperatureChangedEventArgs)
{
// Raise event here
}
virtual OnMailArrived(MailArrivedEventArgs)
{
// Raise event here
}
11.2 事件订阅者约定
事件处理方法名称
- Visual Studio 在自动创建事件处理方法存根时实现的约定是:将方法命名为 (1) 触发事件的对象的名称;后跟 (2) 下划线字符;再接 (3) 事件名称。
示例
downloader_DownloadCompleted
weatherStation_TemperatureChanged
mailManager_OnMailArrived
- 确定事件处理方法名称的另一个约定与上面描述的用于指定发布者中触发事件的方法名称的约定相同。具体来说,方法的名称应该是“
On
”后接事件名称。
示例
OnDownloadCompleted
OnTemperatureChanged
OnMailArrived
事件处理方法签名
- 事件处理方法的签名必须与委托签名完全匹配。根据事件处理约定以及 .NET Framework 提供的
EventHandler
委托,事件处理方法必须返回void
,同时接受恰好两个参数:一个名为sender
的object
类型变量,以及一个名为“e
”的EventArgs
(或派生类)实例。
示例
void DownloadManager_DownloadCompleted(object sender,
DownloadCompletedEventArgs e)
{
// event handling code goes here
}
void WeatherStation_TemperatureChanged(object sender,
TemperatureChangedEventArgs e)
{
// event handling code goes here
}
void MailMonitor_MailArrived(object sender, MailArrivedEventArgs e)
{
// event handling code goes here
}
订阅事件(将事件处理方法注册到事件的代码)
- 要将方法注册到事件,请使用
+=
语法,遵循此模式:EventPublisherObject.EventName += new EventHandlerDelegateName(NameOfMethodToCall);
示例
m_MailMonitor.MailArrived += new EventHandler(
this.MailMonitor_MailArrived);
警告:在将事件订阅者注册到发布者时,不要使用 =
运算符。这样做会用当前订阅者替换任何/所有当前已注册的事件订阅者。相反,请务必使用 +=
运算符,以使当前订阅者**附加**到事件处理程序的调用列表。
取消订阅事件(从事件取消注册事件处理方法的代码)
- 要从事件中取消注册方法,请使用 -= 语法,遵循此模式:
EventPublisherObject.EventName -= new EventHandlerDelegateName(NameOfMethodToCall);
示例
m_MailMonitor.MailArrived -= new EventHandler(
this.MailMonitor_MailArrived);
11.3 命名约定
驼峰命名法
驼峰命名法是一种命名约定,其中第一个字母小写,随后的每个“单词部分”都以大写字母开头。按照惯例,变量名采用驼峰命名法。
驼峰命名法示例:someStringToWrite, ovenTemperature, latitude
帕斯卡命名法
帕斯卡命名法是一种命名约定,其中名称的每个“单词部分”都以大写字母开头,其他字母小写,并且没有下划线。按照惯例,类、事件、委托、方法和属性的名称应采用帕斯卡命名法。
帕斯卡命名法示例:MailArrivedEventHandler, AppClosing, MyClassName
12. 创建自定义事件的步骤
为了使以下步骤尽可能简短,没有或很少提供任何给定步骤的解释。每个步骤的解释、示例和约定在本文其他地方都有介绍。
12.1 准备事件发布者
步骤 1:EventArgs - 决定您的事件如何处理 EventArgs。
- 在自定义事件中包含
EventArgs
是为了符合事件发布标准。然而,EventArgs
并非技术要求——您可以创建、触发和处理不使用EventArgs
的自定义事件。 - 如果您的事件永远不会传递自定义事件数据,那么您可以通过决定使用内置的
System.EventArgs
类来满足此步骤。您稍后将在触发事件时指定值EventArgs.Empty
。 - 如果您的事件不可取消且包含自定义事件数据,那么您应该创建一个扩展
System.EventArgs
的类。您的自定义EventArgs
子类将包含任何额外的属性来包含事件数据。 - 如果您的事件是可取消的,那么您可以使用
System.ComponentModel.CancelEventArgs
——它包含布尔类型的Cancel
属性,客户端可以将其设置为 true 来取消事件。您可以创建CancelEventArgs
的子类,其中包含任何额外事件特定数据的属性。
步骤 2:事件处理程序 - 决定您的事件将使用哪个事件处理程序。
- 您有两种基本选择——创建您自己的事件处理程序(委托)或使用 .NET Framework 提供的
EventHandler
委托之一。如果您使用内置的事件处理程序之一,那么您将需要维护更少的代码,并且您的事件处理程序签名将自动符合返回void
并接受参数object sender
和EventArgs e
的约定。 - 如果使用 .NET 1.x,请考虑使用内置的
System.EventHandler
委托。 - 如果使用 .NET 2.0,请考虑使用内置的泛型
System.EventHandler<TEventArgs>
委托。
步骤 3:声明事件——决定使用哪种语法:字段式语法还是属性式语法。
- 字段式语法足以满足许多自定义事件的实现。
- 当您的类公开大量事件,但其中只有少数预计在任何给定时间被订阅时,请考虑使用属性式语法。
步骤 4:事件触发方法——决定是从方法中触发事件,还是内联触发。
- 通常建议从专门用于触发事件的方法中触发事件,而不是在代码中内联触发事件。
步骤 5:触发事件。
- 要么内联触发事件,要么调用触发事件的方法。
- 在引发事件之前,您需要有一个填充了事件特定数据的
EventArgs
子类的实例。如果没有使用任何EventArgs
子类,那么在引发方法时,您应该使用System.EventArgs.Empty
来代替自定义的EventArgs
类。
12.2 准备事件订阅者
因为本文介绍了在 .NET Framework 类中实现的事件模式(object sender, EventArgs e
),所以以下步骤将帮助您连接适用于几乎所有 .NET Framework 事件以及您按照相同模式创建的自定义事件的事件处理方法。
步骤 1:编写事件处理方法。
- 定义一个事件处理方法,其签名与定义该方法的委托完全匹配。
- 在事件声明中使用内置的非泛型
System.EventArgs
或泛型System.EventHandler<TEventArgs>
委托时,生成的签名会自动匹配返回void
并接受参数 (object sender, EventArgs e
) 的约定。
步骤 2:实例化事件发布者。
- 声明引用感兴趣的事件发布类或对象的类级别成员变量。
步骤 3:实例化事件处理程序(如果需要)。
- 如果感兴趣的事件基于自定义事件处理程序,则创建该事件处理程序的实例,并传入事件处理方法的名称。
- 此步骤可以通过使用 new 关键字在委托与事件注册的同一行实例化委托来与步骤 4(下一步)合并。
步骤 4:将订阅者(事件处理方法)注册到事件。
- 任何 .NET 版本:使用
+=
语法将事件处理程序注册到事件。 - .NET 2.0+:或者,通过委托推断,您可以直接将方法名称分配给事件。
- .NET 2.0+:或者,如果事件处理方法非常简短(大约 3 行代码),那么通过“匿名方法”注册事件处理逻辑可能会使您的实现更易于阅读。
步骤 5:从事件中注销订阅者(事件处理方法)。
- 当订阅者不再需要从发布者接收事件通知时,您可以从事件中注销订阅者。
- 鉴于订阅者在被处置时会自动从发布者注销,此步骤可以视为可选。
13. 示例事件实现
本示例使用自定义事件处理程序,在自定义 EventArgs
子类中携带事件数据,使用类似字段的语法声明事件,并符合推荐的事件实现标准和指南。为了尽可能明确地呈现,本示例不使用任何 .NET 2.0+ 功能(匿名方法等)。
当文件被“文件移动器”实用程序(在示例项目中)移动时,此示例事件会被引发。事件数据包括 (1) 移动的文件名,(2) 源文件夹路径,以及 (3) 目标文件路径。
13.1 示例事件发布者代码
步骤 1:子类化 EventArgs
在这里,我们从 EventArgs
派生一个新类 MoveFileEventArgs
,以封装发送给订阅者的事件数据。
public class MoveFileEventArgs : EventArgs
{
// Fields
private string m_FileName = string.Empty;
private string m_SourceFolder = string.Empty;
private string m_DestinationFolder = string.Empty;
// Constructor
public MoveFileEventArgs(string fileName, string sourceFolder,
string destinationFolder)
{
m_FileName = fileName;
m_SourceFolder = sourceFolder;
m_DestinationFolder = destinationFolder;
}
// Properties (read-only)
public string FileName
{
get { return m_FileName; }
}
public string SourceFolder
{
get { return m_SourceFolder; }
}
public string DestinationFolder
{
get { return m_DestinationFolder; }
}
}
步骤 2:事件处理程序(委托)
在这里,我们声明了一个新的委托,它符合事件约定,返回 void
并接受两个参数;一个名为 'sender
' 的 object
和一个名为 'e
' 的 EventArgs
。
public delegate void MoveFileEventHandler(object sender,
MoveFileEventArgs e);
步骤 3:声明事件
在这里,我们使用类似字段的语法声明事件。
public event MoveFileEventHandler MoveFile;
步骤 4:引发事件的方法
在这里,我们声明了引发事件的方法。
private void OnMoveFile()
{
if (MoveFile != null) // will be null if no subscribers
{
MoveFile(this, new MoveFileEventArgs("SomeFileName.txt",
@"C:\TempSource", @"C:\TempDestination"));
}
}
步骤 5:引发事件
这里有一个方法可以完成感兴趣的工作(移动文件)。工作完成后,会调用引发事件的方法。
public void UserInitiatesFileMove()
{
// code here moves the file.
// Then we call the method that raises the MoveFile event
OnMoveFile();
}
13.2 示例事件订阅者代码
步骤 1:编写事件处理方法
当 fileMover
实例引发 MoveFile
事件时,会调用此方法。它的签名与事件处理程序的签名完全匹配。
void fileMover_MoveFile(object sender, MoveFileEventArgs e)
{
MessageBox.Show(sender.ToString() + " moved the file, " +
e.FileName + ", from " +
e.SourceFolder + " to " + e.DestinationFolder);
}
步骤 2:实例化事件发布者
FileMover fileMover = new FileMover();
步骤 3:实例化事件处理程序,同时将订阅者(事件处理方法)注册到事件
此方法将第 14.2 节中列出的步骤 3 和 4 合并在一起。
fileMover.MoveFile += new FileMover.MoveFileEventHandler(
fileMover_MoveFile);
14. 处理 .NET Framework 组件引发的事件 - 演练和示例
本文描述的事件实现约定可以在 .NET Framework 自身的事件实现中找到。本演练的目的是展示一个 .NET Framework 组件如何公开其事件,以及如何编写在事件引发时运行的代码。您将看到所需的步骤仅仅是实现您自己的事件和事件处理方法所需推荐步骤的一个子集。
本节通过演练 FileSystemWatcher
组件的 Deleted
事件实现,指出了框架如何利用这些约定。FileSystemWatcher
是 .NET Framework 的 System.IO
命名空间中提供的一个类。此类别可用于在特定文件夹中发生特定磁盘 IO 活动(例如,创建新文件、修改或删除文件等)时通知您的应用程序。
14.1 FileSystemWatcher 的 'Deleted' 事件实现
System.IO.FileSystemWatcher
类公开了 Deleted
事件——当 FileSystemWatcher
实例检测到受监视文件夹中的文件已被删除时,该事件就会被引发。
事件声明
.NET Framework 的 FileSystemWatcher 类就是这样声明其 Deleted 事件的。
public event FileSystemEventHandler Deleted
委托声明
所使用的委托是 FileSystemEventHandler
,它在 .NET Framework 中声明为
public delegate void FileSystemEventHandler (Object sender,
FileSystemEventArgs e)
请注意,FileSystemEventHandler
委托符合接受两个参数的约定——第一个参数名为 sender
,类型为 System.Object
;第二个参数名为 'e
',类型为 System.EventArgs
或其派生类。在本例中,指定的 FileSystemEventArgs
是其派生类(如下文进一步描述)。
自定义 EventArgs
Deleted
事件通过 System.EventArgs
的子类传递有关已删除文件或目录的信息。
public class FileSystemEventArgs : EventArgs {}
FileSystemEventArgs
类通过添加以下属性扩展了 System.EventArgs
ChangedType
— 获取发生的目录更改类型(作为 Created, Deleted, Changed 等WatcherChangeType
枚举值传递)FullPath
— 获取受影响文件或目录的完全限定路径Name
— 获取受影响文件或目录的名称
当 Deleted
事件被引发时,FileSystemWatcher
实例会将路径、文件名等发送给事件订阅者。这意味着每个订阅者不仅知道文件被删除了,而且具体知道哪个文件被删除了。
请注意,事件处理程序的名称 FileSystemEventHandler
并不完全符合建议事件处理程序名称应为“事件名称后跟单词 Handler”的命名约定。请记住,约定不是法律或规则。相反,它们只是让您的代码更容易理解的建议。对于 FileSystemWatcher
类,实现了一个事件处理程序以支持多个事件,包括 Deleted
、Created
和 Changed
——因此,与命名约定的严格解释略有出入。严格遵守约定会导致创建 3 个除了名称之外都相同的委托(例如,DeletedHandler
、CreatedHandler
等)。或者,选择的名称可以是 DeletedOrCreatedOrChangedHandler
之类的——这会很荒谬。在这种情况下,[谢天谢地!]选择了一个合理的偏离约定。
14.2 处理 FileSystemWatcher.Deleted 事件
鉴于 .NET Framework 提供的上述事件实现,以下是您可以在类中编写的代码示例,该代码将订阅 FileSystemWatcher
实例的 Deleted
事件。此代码将使您的应用程序能够响应 C:\Temp 目录中删除的文件。
// Import the namespace
using System.IO;
// Declare fsWatcher variable - likely at the class level.
FileSystemWatcher fsWatcher = new FileSystemWatcher();
//Initialize the instance - possibly in the constructor or method called from
constructor.
fsWatcher.Path = "C:\Temp";
fsWatcher.Deleted += new FileSystemEventHandler(fsWatcher_Deleted);
// this Boolean property enables or disables the raising of events by the
FileSystemWatcher instance.
fsWatcher.EnableRaisingEvents = true;
// include if updating Windows Forms components
fsWatcher.SynchronizingObject = this;
// event handling method
void fsWatcher_Deleted(object sender, FileSystemEventArgs e)
{
MessageBox.Show(e.Name + " was deleted" );
}
15. Windows 窗体事件
Windows 窗体实现自己的事件,并促进处理窗体中或窗体类中容器控件中包含的控件引发的事件。此外,Windows 窗体类中事件的实现和处理需要仔细考虑,甚至需要采取独特的步骤,因为 Windows 窗体及其包含的控件表现出“线程亲和性”——这意味着它们的属性只能由在创建窗体或控件的同一线程中运行的代码更新。
本节将介绍这些以及其他特定于 Windows 窗体的注意事项。
15.1 .NET 1.x 和 2.0+ 的差异 - 分部类
“局部类”的概念是在 .NET Framework 2.0 版本中引入的。局部类是使用 partial
关键字声明的类,其部分定义在两个或多个源代码文件中。编译器从所有包含局部类的文件中检索所有定义局部类的源代码,并输出一个 [编译后的] 类。也就是说,一个局部类可以存在于两个或多个源代码文件中,但当应用程序编译时,这些“类片段”会编译成输出程序集中的一个类。
部分类的好处包括:(1) 多个开发人员可以同时处理同一类的不同部分,通过处理不同的源代码文件;(2) 自动化代码生成工具可以写入一个源代码文件,而人工开发人员可以在单独的文件中维护他们的代码,而无需担心他们的更改最终会被自动化代码生成工具覆盖。第二个好处在从 Visual Studio 2005 开始的 Windows 窗体项目中实现。当您向 Windows 窗体项目添加新窗体时,Visual Studio 会自动将该窗体创建为定义在两个源代码文件中的部分类。包含 Visual Studio 生成代码的文件名为 FormName.Designer.cs
,而用于开发人员代码的文件名为 FormName.cs。因此,例如,如果您使用 Visual Studio 2005 创建一个名为 MainForm 的窗体,那么将创建以下两个源代码文件:
MainForm.cs — 包含部分类定义
public partial class MainForm : Form
{
// developers write code here
}
MainForm.Designer.cs — 包含部分类定义
partial class MainForm
{
// Windows Forms designer writes code here
}
当您使用 Visual Studio Windows 窗体设计器向窗体添加控件时,设计器会将必要的代码添加到 FormName.Designer.cs 文件中。
开发人员不应直接修改 FormName.Designer.cs 文件中的源代码,因为设计器可能会覆盖此类更改。通常,所有开发人员代码都应写入 FormName.cs 文件。
.NET 1.x 根本没有局部类。所有源代码——无论是 Visual Studio 编写的还是开发人员编写的——都存储在一个源代码文件中。虽然 Visual Studio Windows 窗体设计器尝试只在该文件的一个部分中编写代码,但开发人员代码和生成的代码可能存在于同一部分中,并且 Windows 窗体设计器可能会覆盖开发人员编写的代码。
15.2 分部类和 Windows 窗体设计器对事件的考虑
当 Visual Studio 2005 为您创建事件处理实现时,事件处理程序/注册代码会写入 FormName.Designer.cs 文件,而只有事件处理方法存根会自动写入 FormName.cs 文件。这种安排的目的是让 Windows 窗体设计器编写所有可自动化的事件相关代码(将事件处理方法与事件处理程序连接等)。设计器无法为您创建的唯一部分是事件处理方法中要执行的特定编程逻辑。因此,当 Visual Studio 完成所有可以为您做的事情后,您将拥有 (1) 所有事件相关的“管道”代码隐藏在 FormName.Designer.cs 文件中;以及 (2) 一个在 FormName.cs 文件中等待您的事件处理方法存根。您所需要做的就是完成事件处理实现,在事件处理方法存根中编写必要的代码。
15.3 演练 - 处理 Windows 窗体事件
以下步骤将引导您完成名为 MainForm 的 Windows 窗体中 FormClosing
事件处理方法的实现。
- 使用 Visual Studio .NET 创建一个新的 Windows Forms 项目,并添加一个名为 MainForm 的新 Form。
- 在设计视图中打开 MainForm,右键单击窗体暴露的部分(而不是控件),然后从弹出菜单中选择“属性”。出现的“属性”对话框可以显示窗体属性或事件。如果尚未选择,请单击属性对话框顶部工具栏中的“事件”按钮(它带有闪电图标)。
- 在“事件”对话框中,找到您希望应用程序响应的事件。在我们的例子中,这是
FormClosing
事件。双击FormClosing
所在的行中的任意位置。
此时会发生两件事。首先,Windows Forms 设计器将以下行插入到 MainForm.Designer.cs 文件中。*
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(
this.MainForm_FormClosing);
其次,Windows Forms 设计器将以下方法存根插入到 MainForm.cs 文件中。*
private void MainForm_FormClosing(object sender,
FormClosingEventArgs e)
{
// your event handling code goes here
}
* 使用 .NET 1.x(没有部分类)时,会生成相同的代码,但会放入单个 MainForm.cs 文件中。
您可以看到,没有任何东西对您隐藏。实现事件处理逻辑所需的所有代码都存在并可供您查看和根据需要进行修改。Windows 窗体设计器所做的只是按照推荐的事件标准编写样板代码,并将该代码放置在部分类文件中,您可以在其中根据您的应用程序特定目的进行扩展。
如果您想更改 Windows 窗体设计器为您生成的事件处理方法的名称,您可以自由更改。只需确保在 MainForm.cs 文件中以及在 MainForm.Designer.cs 文件中注册到事件处理程序的位置更改方法名称。
另外,FormClosing
事件是一个可取消的“前置事件”。这意味着事件在窗体关闭之前引发,事件处理过程可以取消事件,从而阻止窗体关闭。
具体使此事件可取消的是 FormClosingEventArgs
参数,它是一个扩展 System.ComponentModel.CancelEventArgs
的类类型。要取消 FormClosing
事件,您可以将 FormClosingEventArgs
的布尔 Cancel
属性设置为 true
,如下所示
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
e.Cancel = true; // prevents the form from closing
}
15.4 Windows 窗体和线程考虑
Windows 窗体及其包含的控件的属性和方法只能由在创建控件的同一线程上运行的代码调用(即 Windows 窗体和控件表现出线程亲和性)。因此,当来自非 UI 线程的代码尝试执行修改 UI 组件的代码时,您可能会遇到意外行为或运行时异常。
请注意,即使您没有在应用程序中明确或有意地使用多线程,您也可能会遇到与此 UI 线程亲和性相关的线程问题。例如,FileSystemWatcher
类会自动为其自身目的生成一个额外的后台线程。因此,如果您的代码使用 FileSystemWatcher
,并且任何相关的事件处理方法最终导致更新 UI 控件,您可能会遇到线程问题。另外,您使用的任何第三方组件都可能生成您最初不知道的额外后台线程。
有几种方法可以缓解这些线程问题
- 设置
SynchronizingObject
属性(如果可用且相关) - 使用
Control.InvokeRequired
和Control.Invoke()
来调用更新 UI 的代码。 - 组件开发人员可以使用
SynchronizationContext
、AsyncOperatio
n 和AsyncOperationManager
类。
SynchronizingObject
一些 .NET 组件提供 SynchronizingObject
属性。这些组件的示例包括 FileSystemWatcher
、Timer
和 Process
类。设置 SynchronizingObject
使事件处理方法可以在创建要更新的 UI 组件的同一线程上调用。因此,例如,Timer 的 Elapsed
事件是从线程池线程引发的。当 Timer 组件的 SynchronizingObject
设置为 UI 组件时,Elapsed
事件的事件处理方法将在 UI 组件运行的同一线程上调用。然后,可以从 Elapsed
事件处理方法更新 UI 组件。
需要注意的是,Visual Studio 可能会自动将 SynchronizingObject
属性设置为包含该组件的控件。因此,您可能永远不会遇到需要显式设置 SynchronizingObject
属性的情况。
然而,在某些情况下,可能需要显式设置 SynchronizingObject
属性。例如,当您有一个类库在 Windows 窗体中实例化,并且该类库包含一个 FileSystemWatcher
实例时。FileSystemWatcher
会生成一个额外的后台线程,其事件就是从该线程引发的。这些事件随后将在类库中处理。到目前为止一切顺利。类库可以处理这些事件,因为它没有 Windows 窗体控件所具有的线程亲和性。类库可能会响应接收到的 FileSystemWatcher
事件,引发一个新事件,然后在包含的 Windows 窗体实例中处理该事件。如果 SynchronizingObject
未设置为该窗体(或其上的相关控件),或者 UI 更新代码未通过 Control.Invoke()
调用(如下所述),则会发生以下异常。
System.InvalidOperationException was unhandled:
Cross-thread operation not valid: Control 'ControlNameHere' accessed from a
thread other than the thread it was created on.
Control.Invoke() 和 InvokeRequired
关于“Windows 窗体控件不能从创建它们的线程以外的线程访问”的规则,有两个相关的例外。每个控件都继承 Invoke()
方法和 InvokeRequired
属性,它们可以从其他线程访问。Invoke()
接受一个参数,其类型为委托。当调用时,Invoke()
会导致委托调用与其注册的任何方法。显然,通过 Invoke()
调用的任何代码都将在控件所在的同一线程中执行。因此,要从另一个线程中的代码更新运行在某个线程上的 UI 控件,只需 (1) 将更新 UI 控件的代码分解到它自己的方法中;然后 (2) 将该方法注册到委托,然后 (3) 将该委托传递给 UI 控件的 Invoke()
方法。
当当前代码在创建控件的线程以外的线程上运行时,InvokeRequired
返回 true。您可以查询 InvokeRequired
的值以确定您的代码是否可以直接更新控件,或者此类更新是否必须通过 Invoke()
方法进行路由。
SynchronizationContext, AsyncOperation, 和 AsyncOperationManager
.NET Framework 2.0 版本新增的这些类为异步引发事件的组件开发人员提供了另一种解决上述线程问题的方法。使用 System.ComponentModel.AsyncOperation
的一个重要好处是,它在事件发布者(组件)中提供了线程问题的解决方案(如上所述),而上述两种替代方案(Control.Invoke
和 SynchronizingObject
)则将解决方案放在订阅者身上。
16. 可取消事件
可取消事件通常由即将执行某个可取消或可阻止发生的动作的组件引发。Windows 窗体类的 FormClosing
事件就是可取消事件的一个示例。您希望阻止窗体关闭的典型场景是用户尚未保存更改。在这种场景下,您的 FormClosing
事件处理方法可以实现检测未保存更改的逻辑。如果存在未保存更改,该逻辑可以提示用户保存其更改。如果用户选择保存其更改,您的逻辑将取消 FormClosing
事件。这将阻止窗体关闭,从而使用户有机会在再次尝试关闭窗体之前查看其更改并可能保存它们。
可取消事件的内部工作原理可以非常简单。考虑到事件经常向订阅者发出状态改变或其他活动“即将”发生的信号,这种“前置事件”为事件发布者提供了一个理想的机会,以确定它(发布者)是否应该允许状态改变(或活动)发生。如果允许活动发生(即,没有任何东西告诉发布者中止操作),发布者就会允许状态改变发生,然后(可选地)引发后置事件。
总而言之,可取消事件实际上是两个事件和发生在这些事件之间的一些活动。 “前置事件”发生在活动之前。然后活动发生(或不发生)。如果活动发生,则通常会引发“后置事件”。因此,准确地说,即使我们说我们有一个可取消事件,也没有事件被取消。相反,被取消的是发生在两个事件之间的活动——并且很可能被完全阻止开始。
为了支持上述可取消事件的概念,.NET Framework 为我们提供了 System.ComponentModel.CancelEventArgs
类,我们可以直接使用或扩展它以满足特定于应用程序的目的。CancelEventArgs
通过提供布尔型 Cancel
属性来扩展 System.EventArgs
,当事件订阅者将其设置为 true
时,事件发布者会使用它来取消事件。事件发布者代码创建 CancelEventArgs
实例,该实例在引发前置事件时发送给订阅者。默认情况下,事件处理方法(在任何/所有订阅者中)同步运行。因此,由前置事件发出的状态更改(或活动)只有在所有事件处理方法运行完成后才能发生。当然,事件发布者在引发事件后仍保留对 CancelEventArgs
实例的引用。因此,如果任何事件处理方法将 Cancel
属性设置为 true
,事件发布者将在尝试继续进行状态更改之前看到这一点,因此可以相应地做出响应。
活动序列可能如下:
- 事件发布者实例化
System.ComponentModel.CancelEventArgs
(或其子类),名称为 'e
' - 然后,事件引发方法引发事件,将 '
e
' 传递给事件订阅者(默认将Cancel
设置为false
) - 事件处理方法(当然在事件订阅者中)然后将
e.Cancel
的值设置为true
,可能是通过提示用户 - 事件引发方法随后获取
e.Cancel
的值,并相应地做出响应。在e.Cancel = true
的情况下,逻辑将阻止状态更改或活动发生(例如,窗体关闭)。
在事件有多个订阅者的情况下,如果任何事件处理方法将 e.Cancel = true
,则事件将被取消。更具体地说,当最后一个事件处理方法返回时(它们是同步调用的),事件发布者将看到 e.Cancel = true
。
归根结底,CancelEventArgs
为我们所做的只是提供一种机制,让事件订阅者向事件发布者传递一个 true
| false
值。“取消事件”的实际工作和含义完全取决于您,因为您将编写响应 e.Cancel
值的逻辑。
取消长时间运行的操作
上述事件取消场景未提供任何机制来阻止活动一旦开始就继续进行(或状态更改)。这是因为事件发布活动都同步(或按顺序)发生。前置事件可用于阻止活动开始,但一旦开始,它将运行到完成,因为订阅者的前置事件处理方法已运行到完成,因此无法再与事件发布者通信。
如果您需要允许订阅者在操作开始后(例如,在前置事件被引发和处理之后)取消操作,那么上述基本的事件发布机制将不足够。您需要采用更强大的事件发布机制,其中事件发布者异步执行其活动(例如,在后台线程上)。基本思想是,客户端代码(在订阅者/观察者中)将请求事件发布者中发生某些活动。然后,事件发布者将在后台线程上启动其活动。当后台任务运行时,客户端代码可以自由地继续其他工作,例如处理用户的手势——这可能包括请求取消正在进行的异步操作。发布者需要实现逻辑,定期检查客户端是否已请求取消,如果是,则停止其工作。
开始异步处理的一个相对简单且安全的方法是熟悉 System.ComponentModel.BackgroundWorker
组件。BackgroundWorker
组件使您能够异步运行任务,报告任务进度(完成百分比),在任务开始后取消任务,并报告任务完成情况(带返回值)。本文超出范围,无法进一步介绍异步处理模型和多线程替代方案以及相关问题。
17. ASP.NET Web 窗体事件
ASP.NET Web 应用程序中事件、事件处理程序和事件处理方法的创建所涉及的核心概念并没有根本上的独特性。本文中介绍的关于创建自定义事件和事件处理程序的所有内容都同样适用于 ASP.NET Web 应用程序、Windows 窗体应用程序和 C# 代码库。ASP.NET Web 应用程序的根本不同之处在于事件的定义、引发和处理的上下文。HTTP 的无状态性质及其请求/响应模型、ASP.NET HTTP 请求管道的作用、ViewState 的作用等都发挥作用——并对事件的引发和处理产生影响。除了本文中介绍的事件基础知识之外,还有 ASP.NET 特定的事件相关概念,例如客户端事件(用 ECMA Script、VBScript 或 JavaScript 编写)、回发和事件冒泡。
本文不打算在 ASP.NET Web 应用程序的上下文中讨论事件,因为一个合理的处理会使本文的长度增加一倍以上(而且本文已经足够长了!)。但是,应该指出的是,本文中介绍的基础知识将为初级 Web 应用程序开发人员提供坚实的基础。
18. 来源
书籍
- 框架设计指南 - 可重用 .NET 库的约定、习语和模式,Krzysztof Cwalina 和 Brad Abrams,2006 年。
- CLR via C#,第二版,Jeffrey Richter,2006 年。
- 深入 C#,Jon Skeet 著。点击此处获取早期访问版信息。
- Pro C# 2005 和 .NET 2.0 平台,Andrew Troelsen,2005 年。
- 编程 C#,第 4 版,Jesse Liberty,2005 年。
- 编程 .NET 组件,第二版,Juval Lowy,2005 年。
- 设计模式 - 可重用面向对象软件的元素,Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides,1995 年。
在线文章
- 探索观察者设计模式,Doug Purdy 和 Jeffrey Richter (MSDN):http://msdn2.microsoft. com/en-us/library/ms954621.aspx
- 在 .NET 中实现观察者 (MSDN 文章):http://msdn2.microsoft. com/en-us/library/ms998543.aspx
- 委托类 (MSDN 文档):http://msdn2.microsoft.com/en-us/library/system.delegate.aspx
- 委托和事件,Jon Skeet:www.yoda.arachsys.com/csharp/e vents.html
- C# 编码标准 - 指南和最佳实践,2.21 版,Juval Lowy,2007 年。可在 www.idesign.net 获取
历史
- 版本 1.0 - 2007 年 9 月 13 日
- 版本 2.0 - 2007 年 10 月 22 日 - 添加了可取消事件的新章节,并扩展了 1.x 与 2.0+ 差异的介绍。