作者:Ted Pattison
|
您可能已经对事件进行编程若干年了,但是迁移到 .NET Framework 仍然需要您重新检查事件的内部工作,因为 .NET Framework 中的事件位于委托的顶层。 对委托的了解越多,对事件进行编程时所具有的驾驭能力越强。 开始使用公共语言运行库 (CLR) 的某个事件驱动框架(例如 Windows® Forms 或 ASP.NET)时,理解事件在较低的级别如何工作至关重要。 本月我的目标是使您理解事件在较低的级别如何工作。 什么是事件? 事件是一种形式化的软件模式,在该模式中,通知源将对一个或多个处理程序方法进行回调。 因此,事件类似于接口和委托,因为它们提供了设计使用回调方法的应用程序的方法。 但是,事件极大地提高了工作效率,因为它们使用起来比接口或委托更容易。 事件允许编译器和 Visual Studio® .NET IDE 在幕后为您做大量的工作。 涉及事件的设计基于事件源和一个或多个事件处理程序。 事件源可以是一个类也可以是一个对象。 事件处理程序是绑定到处理程序方法的委托对象。 图 1 显示了绑定到其处理程序方法的事件源的高级别视图。 图 1 事件源和处理程序 每个事件都是根据特定的委托类型定义的。 对于事件源定义的每个事件,有一个基于事件的基础委托类型的私有字段。 该字段用于跟踪多路广播委托对象。 事件源还提供允许您注册所需数量的事件处理程序的公用注册方法。 当您创建事件处理程序(委托对象)并在事件源中注册它时,事件源只是将新的事件处理程序追加到列表的结尾。 然后,事件源可以使用私有字段在多路广播委托上调用 Invoke,该多路广播委托将依次执行所有注册的事件处理程序。 事件的真正的妙处在于对其进行设置的大量工作都已经为您做好了。 正如您很快就会看到的,无论任何时候您定义事件时,Visual Basic® .NET 编译器都会通过自动添加私有委托字段和公用注册方法帮助您工作。 您还将看到 Visual Studio .NET 可以通过代码生成器提供更多的帮助,代码生成器可以自动发出适用于您的处理程序方法的主干定义。 对事件进行编程 由于 .NET 中的事件建立在委托的顶层,因此它们的基础的管道详细信息与较低版本的 Visual Basic 中所一直使用的截然不同。 但是,Visual Basic .NET 的语言设计者们在保持事件编程的语法与较低版本的 Visual Basic 一致方面做得很好。 在很多情况下,对事件进行编程涉及的语法与您习惯使用的熟悉的老语法相同。 例如,您将使用 Event、RaiseEvent 和 WithEvents 等关键字,而它们的行为方式与其在较低版本的 Visual Basic 中的行为方式几乎完全相同。 让我们通过创建一个基于事件的简单的回调设计开始。 首先,我需要通过使用 Event 关键字在类定义内定义一个事件。 必须根据特定的委托类型定义每个事件。 下面是定义自定义委托类型和用来定义事件的类的一个示例: Delegate Sub LargeWithdrawHandler(ByVal Amount As Decimal) Class BankAccount Public Event LargeWithdraw As LargeWithdrawHandler '*** other members omitted End Class 在本示例中,LargeWithdraw 事件被定义为实例成员。 在本设计中,BankAccount 对象将充当事件源。 如果希望类而不是对象充当事件源,应该使用 Shared 关键字将事件定义为共享成员。 对事件进行编程时,知道编译器在幕后为您做了大量额外的工作这一点很重要。 例如,当您将我刚才向给您看过的 BankAccount 类的定义编译到程序集时,您认为编译器会做什么? 图 2 显示了在中间语言反汇编程序 ILDasm.exe 中检查生成的类定义时,该定义是什么样的。 该视图毫无保留地向您显示了 Visual Basic .NET 编译器在幕后做了多少工作来帮助您。 图 2 ILDasm 中的类定义 当您定义事件时,编译器在类定义内生成四个成员。 第一个成员是基于委托类型的私有字段。 该字段用于跟踪对委托对象的引用。 编译器通过采用事件本身的名称并添加后缀“Event”生成该私有字段的名称。 这意味着创建名为 LargeWithdraw 的事件将导致创建名为 LargeWithdrawEvent 的私有字段。 编译器还生成两个方法,帮助注册和注销将成为事件处理程序的委托对象。 这两个方法使用标准的命名规则进行命名。 用于注册事件处理程序的方法使用事件的名称,并带有前缀“add_”。 用于注销事件处理程序的方法使用事件的名称,并带有前缀“remove_”。 因此,为 LargeWithdraw 事件创建的两个方法名为 add_LargeWithdraw 和 remove_LargeWithdraw。 Visual Basic .NET 编译器通过调用 Delegate 类的 Combine 方法为将委托对象作为参数接受并将其添加到处理程序列表中的 add_LargeWithdraw 生成一个实现。 编译器通过在 Delegate 类中调用 Remove 方法为从列表中删除一个处理程序方法的 remove_LargeWithdraw 生成一个实现。 第四个也是最后一个添加到类定义中的成员是表示事件本身的成员。 在图 2 中,您应该能够找到名为 LargeWithdraw 的事件成员。 它是旁边带有一个倒三角的成员。 但是,您应该注意到,该事件并不象其它三个成员一样真的是一个物理成员。 相反,它是一个仅包含元数据的成员。 此仅包含元数据的事件成员很有价值,因为它可以向该类支持的编译器和其他开发工具通知 .NET Framework 中事件注册的标准模式。 该事件成员还包含注册方法和注销方法的名称。 这使得 Visual Basic .NET 和 C# 等托管语言的编译器可以在编译时查找注册方法的名称。 Visual Studio .NET 是查找此仅包含元数据的事件成员的开发工具的另一个很好的示例。 当 Visual Studio .NET 发现类定义包含事件时,它将自动生成处理程序方法的主干定义以及将它们作为事件处理程序进行注册的代码。 在开始讨论激发事件之前,我想提出一个有关用于定义事件的委托类型的限制。 用于定义事件的委托类型不能有返回值。 您必须使用 Sub 关键字而不是 Function 关键字定义委托类型,如下所示: '*** can be used for events Delegate Sub BaggageHandler() Delegate Sub MailHandler(ItemID As Integer) '*** cannot be used for events Delegate Function QuoteOfTheDayHandler(Funny As Boolean) As String 对此限制有很充分的原因。 当涉及与若干处理程序方法绑定的多路广播委托时,处理返回值相当困难。 在多路广播委托上调用 Invoke 返回与调用列表中的最后一个处理程序方法相同的值。 但是,捕获较早在列表中出现的处理程序方法的返回值并不那么简单。 不需要捕获多个返回值只会使事件更容易使用。 激发事件 现在,让我们修改 BankAccount 类使其在提款数量超出 $5000 阈值时能够激发一个事件。 激发 LargeWithdraw 事件的最简单的方法是在一个方法、属性或构造函数的实现中使用 RaiseEvent 关键字。 您可能会觉得该语法很熟悉,因为它类似于您在较低版本的 Visual Basic 中使用的语法。 下面是从 Withdraw 方法激发 LargeWithdraw 事件的一个示例: Class BankAccount Public Event LargeWithdraw As LargeWithdrawHandler Sub Withdraw(ByVal Amount As Decimal) '*** send notifications if required If (Amount > 5000) Then RaiseEvent LargeWithdraw(Amount) End If '*** perform withdrawal End Sub End Class
|
RaiseEvent LargeWithdraw(Amount)
Visual Basic .NET 编译器将此表达式扩展为在保留多路广播委托对象的私有字段上调用 Invoke 的代码。 换句话说,使用 RaiseEvent 关键字与在以下 snippet 中编写代码具有完全相同的效果:
If (Not LargeWithdrawEvent Is Nothing) Then LargeWithdrawEvent.Invoke(Amount) End If
注意,Visual Basic .NET 编译器生成的代码执行检查以确保 LargeWithdrawEvent 字段包含对某个对象的有效引用。 这是因为 LargeWithdrawEvent 字段的值在第一个处理程序方法注册之前一直为 Nothing。 因此,除非当前至少有一个处理程序方法已注册,否则生成的代码并不尝试调用 Invoke。
您应该能够对激发事件进行观察。 使用 RaiseEvent 关键字或者根据编译器自动生成的 LargeWithdrawEvent 私有字段直接进行编程通常并没有什么分别。 两种方法都生成相同的代码:
'*** this code RaiseEvent LargeWithdraw(Amount) '*** is the same as this code If (Not LargeWithdrawEvent Is Nothing) Then LargeWithdrawEvent.Invoke(Amount) End If
在很多情况下,您可能喜欢使用 RaiseEvent 关键字语法,因为它需要的键入较少,生成的代码较简洁。 但是,在某些情况下,当您需要较多控制时,根据 LargeWithdrawEvent 私有字段进行明确编程可能会有意义。 让我们看一个这种情况的示例。
想象以下情况:BankAccount 对象有三个事件处理程序已注册以接收 LargeWithdraw 事件的通知。 如果使用 RaiseEvent 关键字触发事件并且调用列表中的第二个事件处理程序出现异常,将会出现什么情况? 包含 RaiseEvent 语句的代码行将接收运行时异常,但是您可能没办法确定哪个事件处理程序导致异常。 而且,可能没有办法处理第二个事件处理程序导致的异常,也没有办法按预期方式在执行第三个事件处理程序的位置继续进行。
但是,如果您愿意根据 LargeWithdrawEvent 私有字段进行编程,则可以更适当的方式处理事件处理程序导致的异常。 检查 图 3 中的代码。 正如您所看到的,降至一个较低的级别并根据该私有委托字段进行编程可以提供额外程度的控制。 您可以恰当地处理异常,然后继续执行较晚出现在列表中的事件处理程序。 与 RaiseEvent 语法相比,该方法具有明显的好处,在该方法中一个事件处理程序导致的异常将阻止执行较晚出现在调用列表中的任何事件处理程序。
创建和注册事件处理程序
现在,您已经知道如何定义和激发事件,该是讨论如何创建事件处理程序并在给定源中注册它的时候了。 有两种不同的方法可以在 Visual Basic .NET 中完成以上操作。 第一种方法称为动态事件绑定,涉及 AddHandler 关键字的使用。 第二种方法称为静态事件绑定,涉及熟悉的 Visual Basic 关键字 WithEvents 的使用。 我打算在下一期讨论静态事件绑定。 所以现在,让我们来看一看动态事件绑定的工作原理。
请记住,事件处理程序是一个委托对象。 因此,可以通过从事件所基于的委托类型实例化一个委托对象,创建一个事件处理程序。 创建此委托对象时,必须将其绑定到要成为事件处理程序的目标处理程序方法。
创建事件处理程序后,必须通过在事件源上调用特定的注册方法在特定的事件中注册它。 回忆一下,LargeWithdraw 事件的注册方法名为 add_LargeWithdraw。 当您调用 add_LargeWithdraw 方法并将委托对象作为参数传递时,事件源将委托对象添加到将接收事件通知的事件处理程序列表中。
有关事件注册会出现混淆的是您从未直接调用 add_LargeWithdraw 等方法。 实际上,如果您按名称访问事件注册方法,Visual Basic .NET 编译器将生成编译时错误。 但是,您可以使用包括 AddHandler 语句的替代语法。 当您使用 AddHandler 语句时,Visual Basic .NET 编译器生成为您调用事件注册方法的代码。
让我们来看一个使用动态事件注册绑定几个事件处理程序的示例。 想象您已经在 AccountHandlers 类中编写了以下共享方法的集合:
Class AccountHandlers Shared Sub LogWithdraw(ByVal Amount As Decimal) '*** write withdrawal info to log file End Sub Shared Sub GetApproval(ByVal Amount As Decimal) '*** block until manager approval End Sub End Class
如果要将这些方法用作 BankAccount 类的 LargeWithdraw 事件的事件处理程序,您应该做什么? 让我们从创建绑定到处理程序 LogWithdraw 的事件处理程序开始。 首先,您必须创建将成为事件处理程序的委托对象:
Dim handler1 As LargeWithdrawHandler handler1 = AddressOf AccountHandlers.LogWithdraw
然后,您必须使用 AddHandler 语句在事件源中注册该新的委托对象。 当您使用 AddHandler 语句注册事件处理程序时,您需要传递两个参数,类似以下内容:
AddHandler <event>, <delegate object>
AddHandler 需要的第一个参数是对类或对象的事件进行求值的表达式。 第二个参数是对将被绑定为事件处理程序的委托对象的引用。 下面是使用 AddHandler 语句在 BankAccount 对象的 LargeWithdraw 事件中注册事件处理程序的一个示例:
'*** create bank account object Dim account1 As New BankAccount() '*** create and register event handler Dim handler1 As LargeWithdrawHandler handler1 = AddressOf AccountHandlers.LogWithdraw AddHandler account1.LargeWithdraw, handler1
当您使用 AddHandler 关键字注册 LargeWithdraw 事件的事件处理程序时,Visual Basic .NET 编译器将扩展此代码以调用注册方法 add_LargeWithdraw。 执行包含 AddHandler 语句的代码后,您的事件处理程序已就位,并已准备就绪可以进行通知。 因此,无论任何时候 BankAccount 对象激发 LargeWithdraw 事件时,都将执行 LogWithdraw 方法。
在上一示例中,我使用了较长形式的语法以便确切地说明创建和注册事件处理程序时所发生的事情。 但是,明白了原理之后,您可能希望使用更简洁的语法来实现同样的目标,如下所示:
'*** create bank account object Dim account1 As New BankAccount() '*** register event handlers AddHandler account1.LargeWithdraw, AddressOf AccountHandlers.LogWithdraw AddHandler account1.LargeWithdraw, AddressOf AccountHandlers.GetApproval
由于 AddHandler 语句期望将委托对象作为第二个参数引用,因此您可以使用 AddressOf 运算符的速记语法,后跟目标处理程序方法。 当 Visual Basic .NET 编译器发现这种情况后,它就会生成额外的代码以创建将成为事件处理程序的委托对象。
Visual Basic .NET 语言的 AddHandler 语句由 RemoveHandler 语句补充。 RemoveHandler 需要的两个参数与 AddHandler 相同,但是它具有相反的效果。 它通过调用事件源提供的 remove_LargeWithdraw 方法从已注册处理程序列表中删除目标处理程序方法:
Dim account1 As New BankAccount() '*** register event handler AddHandler account1.LargeWithdraw, AddressOf AccountHandlers.LogWithdraw '*** unregister event handler RemoveHandler account1.LargeWithdraw, AddressOf AccountHandlers.LogWithdraw
现在,您已经看到了使用事件实现回调设计所需的所有步骤。上面的代码显示了一个完整的应用程序,在该应用程序中两个事件处理程序已注册以接收来自 BankAccount 对象的 LargeWithdraw 事件的回调通知。
小结
虽然使用事件的动机和某些语法与较低版本的 Visual Basic 相比都没有变,但是,您必须承认现在情况有些不同了。 正如您所看到的,您对如何响应事件的控制能力比以前更强了。 如果您希望降低级别并根据委托进行编程,则更是如此。
在下一期的 Basic Instincts 栏目中,我打算继续有关事件的此讨论。 我将向您说明 Visual Basic .NET 如何通过您熟悉的 WithEvents 关键字语法支持静态事件绑定,并将讨论 Handles 子句。 要真正控制事件,您必须能够轻松驾驭动态事件注册和静态事件注册。