Framework 类库的事件编程
发布日期: 1/24/2005 | 更新日期: 1/24/2005
本页内容
本月的内容是专门介绍事件编程的系列专栏(共三期)的最后一期。在前两期专栏中,我已经介绍了如何定义和引发事件(请参见 Basic Instincts:Programming with Events Using .NET 和 Basic Instincts:Static Event Binding Using WithEvents)。我还解释了如何使用动态和静态事件绑定来绑定事件处理程序。本月,我将通过一些在 Microsoft .NET Framework 中处理较常用的事件处理实例来总结我对事件的介绍。
EventHandler 委托
当您使用 Windows® 窗体或 ASP.NET 构建应用程序时,您会看到,在所遇到的事件中有相当大的比率是根据一个名为 EventHandler 的通用委托类型定义的。EventHandler 类型存在于 System 命名空间中并具有以下定义:
Delegate Sub EventHandler(sender As Object, e As EventArgs)
委托类型 EventHandler 在它的调用签名中定义了两个参数。第一个参数(名为 sender)是基于通用 Object 类型的。sender 参数用于传递指向事件源对象的引用。例如,当 Button 对象引发基于 EventHandler 委托类型的事件时,作为事件源的它将传递一个对自身的引用。
由 EventHandler 定义的第二个参数名为 e,它是 EventArgs 类型的对象。在许多情况下,事件源传递的参数值等于 EventArgs.Empty,这表明没有额外参数信息。如果事件源希望在 e 参数中传递额外的参数化信息,则它应该传递一个从 EventArgs 类的派生类创建的对象。
图 1 所示的示例在 Windows 窗体应用程序中包含了两个事件处理程序,它们使用静态事件绑定来绑定。Form 类的 Load 事件和 Button 类的 Click 事件都是根据委托类型 EventHandler 定义的。
您还应该注意到,图 1中的两个事件处理程序方法的名称和格式与 Visual Studio .NET IDE 为您生成的一致。例如,如果您在设计视图中双击某个窗体或命令按钮,Visual Studio .NET 将自动创建类似的事件处理程序方法主干。您需要做的仅仅是填充这些方法的实现,以便为您的事件处理程序赋予预期的行为。
您也许会注意到,Visual Studio .NET IDE 是使用 Visual Basic 6.0 要求的命名方案来生成处理程序方法的。然而,您应当记住的是,Visual Basic .NET 中的静态事件绑定并不真正与处理程序方法的名称有关。与其相关的是 Handles 子句。您可以随意将处理程序方法重命名为所需的任何名称。
您可以重写这两个事件处理程序,以便它们使用动态事件绑定(而非静态事件绑定)来绑定。例如,图 2 中从 Form 派生的类提供了与图 1中从 Form 派生的类完全相同的事件绑定行为。唯一的区别是,后者使用了动态事件绑定,并且不需要 WithEvents 关键字或 Handles 关键字。在许多情况下,您将根据 EventHandler 委托类型来编写处理程序方法的实现,而不是引用 sender 参数或 e 参数。例如,当您为从 Form 派生的类的 Load 事件编写处理程序时,这些参数值并没有实际的作用。sender 不会提供任何值,因为它只是传递 Me 引用。e 参数传递 EventArgs.Empty:
Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
'*** these tests are always true
Dim test1 As Boolean = sender Is Me
Dim test2 As Boolean = e Is EventArgs.Empty
End Sub
您也许想知道,为什么 Load 事件的调用签名没有针对其需要进行更多自定义。毕竟,如果 Load 事件根本不包含任何参数,情况将不会这么令人困惑。要找到其他基于 EventHandler 委托类型的事件(并且其 sender 参数或 e 参数不传递任何值)的示例很容易。
请回答以下问题。如果该委托类型具有这样的通用调用签名,为什么您会认为有这么多事件根据 EventHandler 建模?.NET Framework 的设计者为什么不根据具有适合其需要的调用签名的自定义委托来为每个事件建模?如您所知,.NET Framework 开发中的一个设计目标就是限制用于事件处理的委托的数量。以下几条是更进一步的解释。
最小化委托类型数量的第一个目的是,为了更有效地利用应用程序所使用的内存。加载更多类型意味着占用更多内存。如果由 Windows 窗体框架中的类定义的每个事件都基于一个自定义委托,则每次运行 Windows 窗体应用程序时都必须将上百个委托类型加载到内存中。Windows 窗体框架可依赖很少的委托类型在 Form 类和各种控件类中定义上百个事件,从而提供更好的内存利用率。
最小化委托类型数量的第二个目的是,利用可插接式处理程序方法来增加实现多态性的可能。当您使用与 EventHandler 委托匹配的调用签名来编写处理程序方法时,可以将其绑定到大多数由窗体及其控件引发的事件上。
让我们来看一些编写通用事件处理程序的示例。首先介绍这样一个示例:在这个示例中,可以通过将用户输入改为大写来响应窗体中多个文本框的 TextChanged 事件。没必要为每个控件都创建单独的事件处理程序。相反,您可以只创建一个事件处理程序,然后将其绑定到多个不同文本框的 TextChanged 事件上(请参见图 3)。
对于这个示例,首先应该注意的是,Handles 子句并不仅限于一个事件。您可以在 Handles 关键字后面使用由逗号分隔的列表来包括任意数量的事件。在本示例中,使用了 TextChangedHandler 方法来创建三个不同的事件处理程序。因此,当用户更改这三个文本框中任意一个的文本时,都将执行这个方法。
当执行 TextChangedHandler 方法时,如何知道是哪个 TextBox 对象引发该事件呢?这就是 sender 参数要解决的问题。请记住,sender 参数是根据通用类型 Object 传递的。这意味着,在针对其编程之前,必须将它转换成一个更具体的类型。在前面的示例中,要访问 sender 参数的 Text 属性,就必须将该参数转换为 TextBox。
如果您曾经使用 Visual Basic 的早期版本生成了基于窗体的应用程序,则您可能习惯于使用控件数组。在 Visual Basic 6.0 中使用控件数组的主要优势在于,此功能使得创建一个能够响应由多个不同控件引发的事件的处理程序方法成为可能。Visual Basic .NET 不支持控件数组。然而,您无需过度紧张,因为您刚才已经看到,Visual Basic .NET 提供了一种替代技术,可以将一个处理程序方法绑定到多个不同的事件上。
.NET Framework 的事件体系结构还为您提供了控件数组无法实现的功能。例如,您可以创建一个处理程序方法来响应由多个不同类型的控件所引发的事件。图 4 显示了一个处理程序方法示例,它绑定到三个不同控件类型上的三个不同的事件上。
正如您所看到的,将处理程序方法绑定到事件的方案相当灵活。唯一的要求是,处理程序方法和它绑定到的事件应基于相同的委托类型。而 .NET Framework 中有相当多的事件都是基于 EventHandler 委托类型的,这使得编写通用处理程序方法十分简单。
当您编写通用处理程序方法时,有时需要编写代码来执行条件操作,而这些操作只在事件源是某种特定类型的对象时才执行。例如,您的处理程序方法可以使用 TypeOf 运算符来检查 sender 参数。这使得您的处理程序方法可以在事件源为 Button 对象时执行一组操作,而在事件源为 CheckBox 对象时执行另一组操作,如下所示:
Sub GenericHandler1(sender As Object, e As EventArgs)
If (TypeOf sender Is Button) Then
Dim btn As Button = CType(sender, Button)
'*** program against btn
ElseIf (TypeOf sender Is CheckBox) Then
Dim chk As CheckBox = CType(sender, CheckBox)
'*** program against chk
End If
End Sub
自定义的事件参数
基于 EventHandler 委托的事件通知通常不在 e 参数中发送任何有意义的信息。e 参数通常是无用的,因为它包含 EventArgs.Empty 值或 Nothing 值。然而,.NET Framework 的设计者创建了一个将参数化信息从事件源传递到其事件处理程序的约定。此约定包括自定义事件参数类和自定义委托类型的创建。
由 Form 类引发的鼠标事件为应该如何使用此约定提供了一个很好的示例。有关鼠标位置和按下哪个鼠标键的参数化信息在一个名为 MouseEventArgs的类中建模。MouseEventArgs 类包含了用于跟踪鼠标位置的 X 和 Y 属性,以及用于指示按下哪个鼠标键的 Button 属性。请注意,按照约定,MouseEventArgs 类必须从通用类 EventArgs 继承。
在事件通知中传递参数化信息的约定需要一个自定义委托来补充自定义事件参数类。因此,有一个名为 MouseEventHandler 的委托用于补充 MouseEventArgs 类。该处理程序委托的定义如下:
Delegate Sub MouseEventHandler(sender As Object, e As MouseEventArgs)
现在,假设您希望对一个与鼠标有关的事件(如 Form 类的 MouseDown 事件)作出响应。您可以编写如图 5 所示的处理程序方法。
请注意,e 参数在该处理程序方法的实现中非常有用。e 参数用于确定鼠标位置以及按下哪个鼠标键。所有这些参数化信息都可以通过设计 MouseEventArgs 类来实现。
您可以找到在 Windows 窗体框架中使用的这种参数化约定的其他示例。例如,有一个名为 KeyPressEventArgs 的类,它由一个名为 KeyPressEventHandler 的委托类型补充。此外,ItemChangedArgs 类由一个名为 ItemChangedHandler 的委托类型补充。您可能会遇到其参数化信息也遵循这个约定的其他事件。
参数化自定义事件
作为练习,我们来设计一个自定义事件,以遵循此约定进行参数化。我将使用一个类似于我在最近几期专栏中使用的示例,它包括一个 BankAccount 类。请考虑以下代码片段:
Class BankAccount
Sub Withdraw(ByVal Amount As Decimal)
'*** send notifications if required
If (Amount > 5000) Then
'*** raise event
End If
'*** perform withdrawal
End Sub
End Class
假设要求 BankAccount 对象在每次遇到提款金额大于 $5,000 的情况时都引发一个事件。在引发该事件时,要求您将提款金额作为参数传递给所有已注册的事件处理程序。首先,您应该创建一个新的事件参数类,它从 EventArgs 类继承:
Public Class LargeWithdrawArgs : Inherits EventArgs
Public Amount As Decimal
Sub New(ByVal Amount As Decimal)
Me.Amount = Amount
End Sub
End Class
自定义事件参数类应该设计为:对于事件源需要传递给其事件处理程序的每个参数化值,它都包含一个公共字段。在本例中,LargeWithdrawArgs 类被设计为包含一个名为 Amount 的 Decimal 字段。接下来,您必须创建一个新的委托类型以补充新的事件参数类:
Delegate Sub LargeWithdrawHandler(ByVal sender As Object, _
ByVal e As LargeWithdrawArgs)
按照约定,此委托类型被定义为包含一个名为 sender 的 Object 参数作为第一个参数。第二个参数 e 则基于自定义事件参数类。
现在,您已经创建了自定义事件参数类和补充的委托类型,可以将它们投入使用了。请考察下面的类定义:
Class BankAccount
Public Event LargeWithdraw As LargeWithdrawHandler
Sub Withdraw(ByVal Amount As Decimal)
'*** send notifications if required
If (Amount > 5000) Then
Dim args As New LargeWithdrawArgs(Amount)
RaiseEvent LargeWithdraw(Me, args)
End If
'*** perform withdrawal
End Sub
End Class
它对 LargeWithdraw 事件进行了修改,可以使用 .NET Framework 中的标准约定在事件通知中传递参数化信息。当在 Withdraw 方法中引发 LargeWithdraw 事件时,有必要创建一个新的 LargeWithdrawArgs 类实例,并将其作为参数传递。由于 BankAccount 对象引发了该事件,所以可以使用 Me 关键字来传递 sender 参数,如下所示:
Dim args As New LargeWithdrawArgs(Amount)
RaiseEvent LargeWithdraw(Me, args)
既然您已经了解了如何创建事件源,接下来我们将注意力转到如何为这个事件创建处理程序方法。处理程序方法应该能够通过 e 参数检索它需要的参数化信息。在本例中,处理程序方法将使用 e 参数来检索 Amount 字段的值:
Sub Handler1(sender As Object, e As LargeWithdrawArgs)
'*** retrieve parameterized information
Dim Amount As Decimal = e.Amount
End Sub
图 6 显示了完整的应用程序,当提取大笔金额时,BankAccount 对象将发送事件通知。请注意,此应用程序符合在事件中传递参数化信息的标准公共语言运行库约定。
小结
本节总结了使用 Visual Basic .NET 进行事件编程的基础知识系列内容。前两期专栏介绍了引发和处理事件的机制。本月的专栏则着重介绍在 .NET Framework 中定义的通用事件和委托的编程实例。
您通过 Visual Basic .NET 处理的大多数事件很可能是基于 EventHandler 委托的。如您所见,可以将多个事件绑定到一个处理程序方法上。在这种情况下,知道何时以及如何使用 sender 参数非常重要。您还了解了其他一些使用自定义参数类传递参数化信息的事件。总之,您现在应该可以使用事件驱动的框架(比如 Windows 窗体或 ASP.NET)来进行一些开发工作。
请将给 Ted 的问题和意见发送到 instinct@microsoft.com。
Ted Pattison 是 DevelopMentor (http://www.develop.com) 的教师兼课程作者。他已经撰写了几本关于 Visual Basic 和 COM 的书籍,他目前正在撰写一本名为 Building Applications and Components with Visual Basic .NET (Addison-Wesley, 2003) 的书籍。