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 类的派生类创建的对象。
Imports System
Imports System.Windows.Forms
Public Class MyApp : Inherits Form
'*** static event handler for base class event
Private Sub Form1_Load(ByVal sender As Object,
_
ByVal e As EventArgs)_
Handles MyBase.Load
'*** event handler code
End Sub
'*** button defined as WithEvents field
Friend WithEvents cmdDoTask As Button
'*** static event handler for button
Private Sub cmdDoTask_Click(ByVal sender As Object,
_
ByVal e As System.EventArgs) _
Handles cmdDoTask.Click
'*** event handler code
End Sub
End Class
|
图1
图 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 子句。您可以随意将处理程序方法重命名为所需的任何名称。
Imports System
Imports System.Windows.Forms
Public Class MyApp : Inherits Form
Friend cmdDoTask As Button
Sub New()
'*** other initialization code omitted
AddHandler MyBase.Load, AddressOf Me.Handler1
AddHandler cmdDoTask.Click, AddressOf Me.Handler2
End Sub
Private Sub Handler1(ByVal sender As Object, ByVal
e As EventArgs)
'*** event handler code
End Sub
Private Sub Handler2(ByVal sender As Object,
_
ByVal e As System.EventArgs)
'*** event handler code
End Sub
End Class
|
图2
您可以重写这两个事件处理程序,以便它们使用动态事件绑定(而非静态事件绑定)来绑定。例如,图 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)。
Public Class MyApp : Inherits Form
Friend WithEvents TextBox1 As TextBox
Friend WithEvents TextBox2 As TextBox
Friend WithEvents TextBox3 As TextBox
'*** create event handler bound to several TextChanged
events
Private Sub TextChangedHandler(ByVal sender As System.Object,
_
ByVal e As System.EventArgs) _
Handles TextBox1.TextChanged, _
TextBox2.TextChanged, _
TextBox3.TextChanged
'*** convert sender to TextBox
Dim txt As TextBox = CType(sender, TextBox)
txt.Text = txt.Text.ToUpper()
End Sub
End Class
|
图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 显示了一个处理程序方法示例,它绑定到三个不同控件类型上的三个不同的事件上。
Public Class MyApp : Inherits Form
Friend WithEvents TextBox1 As TextBox
Friend WithEvents CheckBox1 As CheckBox
Friend WithEvents ListBox1 As ListBox
'*** define form-wide dirty flag
Friend DirtyFlag As Boolean
'*** set dirty flag when various events are raised
Private Sub DirtyFlagHandler(ByVal sender As Object,
_
ByVal e As EventArgs) _
Handles TextBox1.TextChanged, _
CheckBox1.CheckedChanged, _
ListBox1.SelectedIndexChanged
'*** set form-wide dirty flag
DirtyFlag = True
End Sub
End Class
|
图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 所示的处理程序方法。
Private Sub Form1_MouseDown(ByVal sender As Object,
_
ByVal e As MouseEventArgs) _
Handles MyBase.MouseDown
'*** capture mouse position
Dim x_position As Integer = e.X
Dim y_position As Integer = e.Y
'*** take action depending on which button was
pressed
Select Case e.Button
Case MouseButtons.Left
'*** do something
Case MouseButtons.Right
'*** do something else
End Select
End Sub
|
图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 对象将发送事件通知。请注意,此应用程序符合在事件中传递参数化信息的标准公共语言运行库约定。
'*** custom event arguments class
Class LargeWithdrawArgs : Inherits EventArgs
Public Amount As Decimal
Sub New(ByVal Amount As Decimal)
Me.Amount = Amount
End Sub
End Class
'*** delegate to complement custom event arguments
class
Delegate Sub LargeWithdrawHandler(ByVal sender
As Object, _
ByVal e As LargeWithdrawArgs)
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
Class AccountAuditor
Private WithEvents account As BankAccount
Sub Handler1(ByVal sender As Object, _
ByVal e As LargeWithdrawArgs) _
Handles account.LargeWithdraw
'*** retrieve parameterized information
Dim Amount As Decimal = e.Amount
End Sub
Sub New(ByVal SourceAccount As BankAccount)
Me.account = SourceAccount ''*** triggers binding
of event handler
End Sub
End Class
Module MyApp
Sub Main()
'*** create bank account object
Dim account1 As New BankAccount()
'*** register event handlers
Dim listener1 As New AccountAuditor(account1)
'*** do something that triggers callback
account1.Withdraw(5001)
End Sub
End Module
|
图6