使用 WithEvents 进行静态事件绑定
原文地址:http://www.microsoft.com/china/msdn/library/langtool/vbnet/WithEvents.mspx
本月的 Basic Instincts 是以我的上三个专栏为基础的,在这三个专栏中,我引入并解释了与委托和事件有关的基本概念和语法。上个月,我向您展示了如何设计和编写一个简单类来定义和引发事件。您还看到了如何使用 AddHandler 关键字将一个事件处理程序动态地绑定到一个事件。本月我将讨论静态事件绑定(用于注册事件处理程序的一种可选技术)。
使用 WithEvents 关键字
以前具有 Visual Basic® 方面经验的编程人员可能发现静态事件绑定非常简单,因为他们熟悉其使用 WithEvents 关键字的语法。让我回到上个月的专栏中所使用的 BankAccount 类的示例。该类将事件定义为公共成员,并从方法定义内引发该事件,如下所示:
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
从 BankAccount 类创建的对象公开了此 LargeWithdraw 事件。可以看到,只要提款金额大于 $5,000,BankAccount 对象都会包含逻辑以引发 LargeWithdraw 事件。
想像一下,您需要使用静态事件绑定创建名为 AccountAuditor1 的新类作为事件侦听器。可以通过添加一个或多个用 WithEvents 关键字定义的字段来将一个类定义为事件侦听器:
Class AccountAuditor1 Private WithEvents account As BankAccount '*** other members omitted End Class
应该注意,使用 WithEvents 关键字定义的字段必须基于一个类,该类定义一个或多个实例事件,否则它将导致编译时错误。在本例中,可以使用 WithEvents 关键字定义命名为帐户的字段,因为 BankAccount 类定义一个名为 LargeWithdraw 的实例事件。
使用 WithEvents 关键字定义帐户字段的要点在于,允许这些在 AccountAuditor1 类中定义的方法注册为由 BankAccount 对象所引发的事件的事件处理程序。现在,我已经定义了一个 WithEvents 字段,并将创建一个方法,该方法将作为 LargeWithdraw 事件的事件处理程序。
当您定义将作为事件处理程序的方法时,该方法必须具有适当的调用签名。例如,将作为 LargeWithdraw 事件的事件处理程序的方法必须具有匹配 LargeWithdrawHandler 的调用签名。请看以下类定义:
Class AccountAuditor1 Private WithEvents account As BankAccount Sub Handler1(ByVal amount As Decimal) Handles account.LargeWithdraw '*** handler method implementation End Sub End Class
在本例中,我已经添加了名为 Handler1 的新方法。正如您所看到的,此处理程序方法具有一个匹配委托类型 LargeWithdrawHandler 的调用签名。还请注意,Handler1 方法的定义包含 Handles 子句:
Sub Handler1(ByVal amount As Decimal) Handles account.LargeWithdraw
可以看出,Handles 子句是基于 WithEvents 字段和事件的。.此 Handles 子句非常重要,因为它为 Visual Basic .NET 编译器提供了提示。更明确地说,Handles 子句的存在触发 Visual Basic .NET 编译器生成额外的代码,该代码将创建并注册事件处理程序。在这个特例中,编译器生成代码来创建绑定到 Handler1 方法的 LargeWithdrawHandler 类型的事件处理程序。然后,该编译器将生成代码,以将其注册到已分配到该帐户字段的 BankAccount 对象。
请注意,在 Visual Basic .NET 中,Handler关键字是新增的。较早版本的 Visual Basic 要求您对处理程序方法使用特定的命名规则。例如,Visual Basic 6.0 会要求前一示例中的处理程序方法以名称 account_LargeWithdraw 来定义。但是,在 Visual Basic .NET 中,事件处理程序的名称并不重要,重要的是用 Handles 子句定义的处理程序方法。
当您将事件源对象分配到 WithEvents 字段时,实际的“魔法”是通过 Visual Basic .NET 编译器施展的。稍后我将讨论编译器如何施展该“魔法”。现在,我将再次为 AccountAuditor1 添加内容,以便该类可被用作事件侦听器。
一个侦听器对象需要一个事件源对象。例如,除非已经有了将作为事件源的 BankAccount 对象,否则创建 AccountAuditor1 对象是无意义的。因此,我将添加构造函数,以便使用 BankAccount 对象将每个 AccountAuditor1 对象初始化为其事件源,如下所示:
Class AccountAuditor1 Private WithEvents account As BankAccount Sub Handler1(ByVal amount As Decimal) Handles account.LargeWithdraw '*** handler method implementation End Sub Sub New(ByVal SourceAccount As BankAccount) Me.account = SourceAccount '*** triggers binding of event handler End Sub End Class
请注意,该构造函数将 SourceAccount 参数分配到命名为账户的 WithEvents 字段。这就是动态绑定事件处理程序的代码行。现在,AccountAuditor1 类可用于创建侦听器对象。例如,设想您编写了下列应用程序:
'*** create event source Dim account1 As New BankAccount() '*** create listener object and bind event handler Dim listener1 As New AccountAuditor1(account1) '*** do something that triggers event account1.Withdraw(5001)
当 AccountAuditor1 类的构造函数执行时,Visual Basic .NET 编译器已生成了代码来创建绑定到 Handler1 方法的事件处理程序对象。该编译器还通过调用注册方法 add_LargeWithdraw 生成代码来注册带 BankAccount 对象的事件处理程序。因此,只要 Withdraw 方法引发 LargeWithdraw 事件,Handler1 方法就会执行。
刚才您已经看到了如何使用静态事件绑定来创建侦听器对象的基本内容。图 1 显示了一个完整的使用静态事件绑定实现回调设计的应用程序,这与您在上三个 Basic Instincts 专栏中所看到的其他技术是相似的。您应该注意,静态事件绑定和动态事件绑定是常用于达到相同目的的两种不同方法。
现在,您已经了解了静态事件绑定和动态事件绑定,您可能想知道应该使用哪一种方法。理想情况下,您应该学习如何使用这两种方法。当您请求 Visual Studio .NET IDE 为事件处理程序生成主干定义时,Visual Studio® .NET IDE 使用静态事件绑定。因此,当您使用事件驱动的应用程序框架(例如 Windows®)时,您对静态事件绑定的理解将非常重要。窗体或 ASP.NET Web 窗体
然而,了解如何使用动态事件绑定(上个月我所展示的)也很重要,因为它具有灵活性。例如,只有在事件源是一个对象的情况下,才能使用静态事件绑定。而当事件源是一个类时,就无法使用静态事件绑定。换句话说,静态事件绑定可与实例事件一起使用,但不能和共享事件一起使用。另一方面,动态事件绑定将允许您绑定到实例事件或共享事件。
在 Visual Studio .NET IDE 不能生成应用程序所需的全部事件处理代码的情况下,您必须编写代码来手动创建和注册事件处理程序。在此类情况下,您将发现动态事件绑定更简单且更易于使用。毕竟,它仅采用单一 AddHandler 语句来从方法创建事件处理程序,并将其绑定到事件。唯一的要求是处理程序方法必须具有相关事件所需的调用签名。
另外需要注意的一件更有趣的事情是,静态事件绑定是一种特殊编程功能,该编程功能是 Visual Basic .NET 所独有的。其他托管语言(例如 C#)支持动态事件绑定,但是不支持等同的静态事件绑定。如果您将在托管语言之间切换,或者需要将代码从 C# 移植到 Visual Basic .NET,则应该考虑依赖动态事件绑定。
静态事件绑定的“魔力”
现在,我将讨论 Visual Basic .NET 编译器如何支持静态事件绑定的低级详细信息。您可以使用静态事件绑定而无需理解我将解释的所有详细信息,但是为了满足您对事情如何在幕后工作的好奇心,我将详细解释编译器在做什么。
让我们从您编译 WithEvents 字段的类定义时所发生的事情开始。假定您已编写了下列类定义:
Class AccountAuditor Private WithEvents account As BankAccount '*** other members omitted End Class
当您编译该类时发生了什么事情?Visual Basic .NET 编译器生成在 ILDasm 中显示的类定义(请参见图 2)。正如您所看到的,由该编译器生成的类定义与您编写的类定义有很大的不同。在编译该类定义之后,就不再有以帐户命名的字段。作为替代,有以 _account 命名的私有字段和以帐户命名的读/写属性。该编译器还生成帐户属性的 Set 和 Get 方法的特殊实现。所有“魔力”都在 Set 方法实现中。

图 2 该编译器的类定义
那么,当您将事件源对象分配到 WithEvents 字段时,实际发生了什么事情?例如,当您将 BankAccount 对象分配到 AccountAuditor 类的构造函数中的帐户字段时,发生了什么事情?
Class AccountAuditor Private WithEvents account As BankAccount Sub Handler1(ByVal amount As Decimal) Handles account.LargeWithdraw *** handler method implementation End Sub Sub New(ByVal source As BankAccount) Me.account = source '*** triggers call to add_LargeWithdraw End Sub End Class
了解您确实未将该事件源对象分配到一个字段是非常重要的。相反,您将该事件源对象分配到一个属性,该属性导致名为 set_account 的Set 方法执行。生成 set_account 的实现,以便为每个用 Handles 子句定义的方法创建和注册事件处理程序。图 3 显示编译器将如何转换您编写的代码。
检查在图 3 中显示的 AccountAuditor 类的 Set 方法内的代码。该代码查看是否有 _account 字段所引用的现有事件源对象。如果有,则该代码通过调用注销方法 remove_LargeWithdraw 来移除它。接下来,Set 方法检查 Value 参数,以查看对于事件源对象或 Nothing 值来说,已分配的值是否为有效引用。如果该 Value 参数具有对事件源对象的有效引用,则 Set 方法创建绑定到 Handler1 方法的事件事件处理程序,并通过调用 add_LargeWithdraw 来注册该事件处理程序。
因此,确实没有涉及到任何“魔力”。当将有效的对象引用分配到 WithEvents 字段时,编译器会生成代码来创建委托对象,并将它们绑定到已经用 Handles 子句定义的每个方法。该编译器还生成代码来注册这些委托对象(通过调用通知源定义的事件注册方法)。
请注意,在初始化过程中,您不需要将侦听器对象绑定到事件源。在侦听器对象的生存期中,您可以在任何时候将一个值分配到一个 WithEvents 字段。请考虑图 4 中的类定义。使用这个新设计,在初始化一个 AccountAuditor 对象之后,就不能再将其绑定到事件源。然而,您还可以在任何时候调用 StartListening,以将侦听器对象绑定到事件源对象。不过请注意,调用 StartListening 将使侦听器对象与现有的事件源断开连接,以便该侦听器可以连接到新的事件源。调用 StopListening 将使侦听器与现有的事件源断开连接,此外,还将该侦听器保留为未绑定状态。
在这一点上,老实说,当您使用静态事件绑定时,Visual Basic .NET 编译器为您在幕后做了大量工作。此外,该示例仅涉及单一处理程序方法。实际上,您将经常使用与单一 WithEvents 字段相关的许多处理程序方法。
在继承类中使用 WithEvents
在 Microsoft® .NET Framework 中,基类引发事件是很常见的,这些事件被设计为由派生类进行处理。这个想法就是,通过添加事件处理程序来响应基类引发的事件,派生类作者可以自定义行为。此设计技术提供了一种使用可重写的方法的可选技术。它也是一种您需要理解的重要技术,因为它广泛地被诸如 Windows 窗体和 ASP.NET 之类的应用程序框架使用。
让我们看一个简单的示例来了解它如何工作。您已经看到了,根据 WithEvents 字段可以编写 Handles 子句。您还可以使用 MyBase 关键字编写 Handles 子句,以便处理在基类中定义的事件,如以下代码所示:
Class CheckingAccount : Inherits BankAccount Sub Handler1(ByVal amount As Decimal) Handles MyBase.LargeWithdraw '*** handler method implementation End Sub End Class
Visual Basic .NET 编译器再次生成创建和注册事件处理程序所需的代码。然而,与生成和 WithEvents 字段相关的事件代码相比,生成基类事件代码的方式略有不同。在本例中,Visual Basic .NET 编译器将绑定到 LargeWithdraw 方法的代码添加到 CheckingAccount 类的构造函数中。
当您使用公开事件的基类时,还应记住另一件事情。虽然派生类可以处理基类事件,但是它无法引发基类事件。请看以下示例:
Class CheckingAccount : Inherits BankAccount Sub SomeOtherMethod() RaiseEvent LargeWithdraw(5001) '*** compile error End Sub End Class
因为事件的实现需要私有字段,所以只能从定义了该事件的类中引发该事件。因此,当您试图使用 RaiseEvent 语句引发基类,或者尝试通过名称访问该私有字段时,将遇到编译时错误。
小结
我已经向您展示了两种用于为事件注册事件处理程序的不同方法:动态事件绑定和静态事件绑定。要增进对 Visual Basic .NET 的了解,您必须灵活地运用这两种技术。
在本专栏的下一期中,我将通过研究 Framework Class Libraries 中最常用的一些委托类型和事件来继续关于事件的讨论。特别地,我将集中讨论名为 System.EventHandler 的委托类型,并且展示它如何为您将在 Windows 窗体和 ASP.NET 中使用的大多数事件提供基础。
请将您给 Ted 的问题和建议发送到:instinct@microsoft.com。
Ted Pattison 是 DevelopMentor (http://www.develop.com) 的讲师兼研究员,他与别人共同在那里讲授 Visual Basic 课程。他也是 Programming Distributed Applications with COM and Microsoft Visual Basic 6.0 (Microsoft Press, 2000) 一书的作者。
浙公网安备 33010602011771号