技术积累

明日复明日,明日何其多,我生待明日,万事成蹉跎。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

使用 .NET 对事件进行编程

Posted on 2005-07-03 20:35  追风逐云.NET  阅读(408)  评论(0编辑  收藏  举报
 本月的“基本功能”专栏建立在本人上两期专栏的基础之上,在上两期“基本功能”专栏中,我讨论了与委托相关的概念和编程技巧。本文假定读者已经阅读了该专栏的上两期,并且理解委托在 Microsoft®.NET Framework 中所扮演的角色。如果您尚未阅读上两期专栏,请参阅 Implementing Callback Notifications Using Delegates 和 Implementing Callbacks with a Multicast Delegate。您还应该知道如何设计和编写使用多路广播委托将回调通知发送到一组处理程序方法的简单应用程序。
  
  您可能已经对事件进行编程若干年了,但是迁移到 .NET Framework 仍然需要您重新检查事件的内部工作,因为 .NET Framework 中的事件位于委托的顶层。对委托了解得越多,对事件进行编程时所具有的驾驭能力就越强。在开始使用公共语言运行库 (CLR) 中的一个事件驱动框架(例如 Windows® Forms 或 ASP.NET)时,理解事件在较低的级别如何工作至关重要。本月,我的目标是使您理解事件在较低的级别如何工作。
  
  事件究竟是什么?
  
  事件只是一种形式化的软件模式,在该模式中,通知源对一个或多个处理程序方法进行回调。因此,事件类似于接口和委托,因为它们提供了设计使用回调方法的应用程序的方法。但是,事件大大提高了工作效率,因为它们使用起来比接口或委托更容易。事件允许编译器和 Visual Studio®.NET IDE 在幕后为您做大量的工作。
  
  涉及事件的设计基于事件源和一个或多个事件处理程序。事件源可以是一个类也可以是一个对象。事件处理程序是绑定到处理程序方法的委托对象。图 1 显示了一个绑定到其处理程序方法的事件源的高级别视图。
  [img]http://www.microsoft.com/china/MSDN/library/netFramework/netframework/art/Eventfig01.gif[/img
  图 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 编译器为 add_LargeWithdraw 生成一个实现,该实现将委托对象作为参数接受,并通过调用 Delegate 类的 Combine 方法将委托对象添加到处理程序的列表中。该编译器还为 remove_LargeWithdraw 生成一个实现,该实现通过调用 Delegate 类中的 Remove 方法从列表中删除一个处理程序方法。
  
  第四个也是最后一个添加到类定义中的成员是表示事件本身的成员。在图 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
  
  虽然该语法与早期版本的 Visual Basic 相同,但是现在引发事件时所发生的事情则截然不同。在您使用 RaiseEvent 关键字引发事件时,Visual Basic .NET 编译器会生成执行每个事件处理程序所需的代码。例如,在编译以下代码时,您认为会发生什么事情?
  
  RaiseEvent LargeWithdraw(Amount)
  
  Visual Basic .NET 编译器将该表达式扩展为在保留多路广播委托对象的私有字段上调用 Invoke 的代码。换句话说,使用 RaiseEvent 关键字与编写以下代码片段具有完全相同的效果:
  
  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 语法相比,该方法具有明显的优势,在 RaiseEvent 语法中,一个事件处理程序引发的异常将阻止执行随后出现在调用列表中的任何事件处理程序。
  Figure 3 Using the Private Delegate Field
  
  Sub Withdraw(ByVal Amount As Decimal)
   '*** send notifications if required
   If (Amount > 5000) AndAslo (Not LargeWithdrawEvent Is Nothing) Then
   Dim handler As LargeWithdrawHandler
   For Each handler In LargeWithdrawEvent.GetInvocationList()
   Try
   handler.Invoke(Amount)
   Catch ex As Exception
   '*** deal with exceptions as they occur
   End Try
   Next
   End If
   '*** perform withdrawal
  End Sub
  
  
  创建和注册事件处理程序
  
  现在,您已经知道如何定义和引发事件,下面该讨论如何创建事件处理程序并在给定源中注册它了。在 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
  
  现在,您已经了解了使用事件实现回调设计所需的所有步骤。图 4 中的代码显示了一个完整的应用程序,在该应用程序中,已经注册了两个事件处理程序,以接收来自 BankAccount 对象的 LargeWithdraw 事件的回调通知。
  Figure 4 An Event-based Design for Callback Notifications
  
  Delegate Sub LargeWithdrawHandler(ByVal Amount As Decimal)
  
  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
  
  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
  
  Module MyApp
   Sub Main()
   '*** create bank account object
   Dim account1 As New BankAccount()
   '*** register event handlers
   AddHandler account1.LargeWithdraw, _
   AddressOf AccountHandlers.LogWithdraw
   AddHandler account1.LargeWithdraw, _
   AddressOf AccountHandlers.GetApproval
   '*** do something that triggers callback
   account1.Withdraw(5001)
   End Sub
  End Module
  
  
  小结
  
  虽然使用事件的动机和某些语法与早期版本的 Visual Basic 相同,但是您必须承认现在的情况大大不同了。正如您所看到的那样,与以前相比,您对如何响应事件的控制能力更强了。如果您希望降低级别并根据委托进行编程,则更是如此。
  
  在下一期的“基本功能”专栏中,我打算继续进行有关事件的讨论。我将向您说明 Visual Basic .NET 如何通过您熟悉的 WithEvents 关键字语法支持静态事件绑定,并将讨论 Handles 子句。要真正控制事件,您必须能够轻松驾驭动态事件注册和静态事件注册。
  
  请将给 Ted 的问题和意见发送至 instinct@microsoft.com。
  
  Ted Pattison 是 DevelopMentor (http://www.develop.com) 的讲师兼研究员,他在 DevelopMentor 与别人共同管理 Visual Basic 课程。他是 Programming Distributed Applications with COM and Microsoft Visual Basic 6.0 (Microsoft Press, 2000) 一书的作者。