事件是你的代码兵器库中的主要部分,无论你用Visual Basic? 6.0,Visual Basic .NET 2002,Visual Basic .NET 2003,还是Visual Basic 2005 。
使用.NET事件设计模式
虽然如你在Visual Basic 6.0和我前面的例子中所可能做的一样引发事件并没有什么错误,.NET Framework已经为事件采用了一种特别的设计模式,一种你应该在你的应用程序中采用的设计模式。在这个模式里,所有事件提供两个参数:一个对象,提供一个对引发事件(一般命名为sender)对象的引用,和一个EventArgs对象(或者一个继承于EventArgs的对象),提供相关信息给事件(一般命名为e)。
标准.NET Framework事件的设计模式添加了三个建议。首先,如果你的事件需要传递任何信息到它的侦听器,你应该创建一个继承于EventArgs的类并且它包含附加信息。你可以使用你的类的构造器来接受并存储信息。在示例项目中,FileFoundEventArgs类如图 8 所示。
第二,提供一个引发事件的过程。大多.NET Framework类从一个重载的protected过程引发事件,一般命名为OnEventName(在FileFound事件的情况下,过程会被命名为OnFileFound)。需要引发事件的代码调用OnEventName过程,它将接着引发事件。使之成为一个protected方法意味着它可为当前类型的对象所用和基于继承自当前类的任何对象所用。使之重载意味着继承类可以改变事件的行为: 一个继承类可以添加运行于调用基类的OnEventName过程之前或之后的代码,或者可以全部跳过它们。在这个示例项目中,FileSearch5类提供以下protected过程:
’ From FileFoundEventsArgs.vb Public Class FileFoundEventArgs Inherits EventArgs Private mfi As FileInfo
Public ReadOnly Property FileFound() As FileInfo Get Return mfi End Get End Property
Public Sub New(ByVal fi As FileInfo) ’ Store the FileInfo object for later use. mfi = fi End Sub End Class |
’’ 来自 FileSearch9.vb Protected Overridable Sub OnFileFound(ByVal fi As FileInfo) RaiseEvent FileFound(Me, New FileFoundEventArgs(fi)) End Sub |
这个过程用RaiseEvent语句的第一个参数传递关键词Me。这个关键词引用在当前运行的代码中的对象,它当然就是那个引发事件的对象。
第三,你可能发现创建你自己的事件委托是很有用的。尽管你无须定义一个显式事件委托就可获得事件委托,但在自己创建时可获得一些灵活性。在你创建一个委托时,你正在为过程定义一个“类型”。如果你有不止一个事件,它们需要同一套参数,创建一个定义这个类型的委托将会有用。如果你需要修改这些参数,你可以只要修改委托,而不用修改事件声明。
举个例子,你可以声明这个FileFound事件,不用事件委托,如下:
Public Event FileFound( _ ByVal sender As Object, ByVal e As FileFoundEventArgs) |
如果你这时想要声明其他事件,使用相同参数,你将必须重复整个声明:
Public Event FileFoundSomeOtherEvent( _ ByVal sender As Object, ByVal e As FileFoundEventArgs) |
作为选择,你也可以声明一个新的委托类型,它描述了你的事件的参数签名:
Public Delegate Sub FileFoundEventHandler( _ ByVal sender As Object, ByVal e As FileFoundEventArgs) |
这时你应该声明这个类型的声明事件:
Public Event FileFound As FileFoundEventHandler Public Event FileFoundSomeOtherEvent As FileFoundEventHandler |
如果你没有采取这个额外的步骤,Visual Basic .NET编译器将为你做这些工作,添加新的委托类型到类的元数据中去。FileSearch5.Search方法利用了这一机制,在每个文件找到时调用OnFileFound方法:
’’ 来自 FileSearch5.vb Dim afi() As FileInfo = diLocal.GetFiles(Me.FileSpec) For Each fi As FileInfo In afi OnFileFound(fi) Next |
点击示例窗体上的Event Design Pattern按钮创建一个FileSearch5类的实例并调用它的Execute方法(正如所有前面的例子一样)。在此情况下,FileSearch5.FileFound事件的事件处理程序是有点不同:不是只接受一个FileInfo对象,这个事件处理程序看起来像一个标准的.NET事件处理程序;它接受两个参数并使用了FileFoundEventArgs参数的 FileFound属性来显示找到的文件名称:
’’ From frmMain.vb Private Sub fs5_FileFound( _ ByVal sender As Object, ByVal e As FileFoundEventArgs) _ Handles fs5.FileFound AddText(e.FileFound.FullName) End Sub |
尽管你不需要使用标准.NET事件处理设计模式,它总是使得你自己的事件与内部.NET对象引发的事件匹配的最好。你获得在你的事件侦听器中“通晓”的好处,并且它们看起来像其他事件。创建你自己的事件委托是可选的,但是如果你有多个事件传递相同的参数,使用事件委托可以简化你的代码。另外,因为事件参数的更改只能在一个地方进行,所以根据你的需要在任何时候修改你的代码将会更容易。
动态添加和移除处理程序 到目前为止你所看到的每个事件侦听器都需要你在设计时关联到事件处理程序。Handles子句是关联使用WithEvents关键字的对象引发事件的方便而简单的方法,但是它不能在运行时提供任何弹性。另外,当多个过程处理相同的事件,Handles子句不会给你事件处理程序执行顺序的控制权。(当然,为了避免这个问题你可以使用你前面看到的技巧,遍历调用GetInvocationList返回项。这个技术要求引发事件的类的附加代码,不是在关联事件侦听器的代码中。)
为了在何时及以什么顺序调用事件处理程序上获得完全主动,你可以使用AddHandler(和RemoveHandler)语句而不是Handles子句。AddHandler和RemoveHandler语句允许你提供一个特定的事件和准备响应事件被调用过程的地址。每个对AddHandler的调用使过程和事件相关联以使得.NET Framework在事件发生时调用过程。另外,AddHandler总是添加事件处理程序到事件调用列表的末尾。这意味着你控制了事件被处理的顺序。
当然,如果你再多考虑片刻,你会理解当你有多个过程时,相同的事件都有一个Handles子句,Visual Basic编译器为事件处理程序创建一个多路广播委托实例而不允许你控制它们被添加进的顺序。引发的事件调用委托实例的Invoke方法,这时就按每个事件侦听器被添加的顺序(并且你无权控制这个顺序)调用它们。当你使用AddHandler和RemoveHandler语句而不是Handles子句,你只要简单控制各项加入多路广播委托的顺序。每次你的应用程序对相同事件调用AddHandler语句,你就为这个事件添加了一个新的侦听器到列表的最后。当你引发这个事件,.NET运行时按顺序调用每个侦听器。
如果你点击了示例窗体的Add/RemoveHandler按钮,一个新的FileSearch5类的实例被创建,同时实例的FileFound事件的多个事件处理程序被整合(hooked up)。这时,当代码调用实例的Execute方法,示例窗体的listbox控件显示出结果:
’’ 来自 frmMain.vb Dim fs5 As New FileSearch5( _ Me.txtSearchPath.Text, Me.txtFileSpec.Text, _ Me.chkSearchSubfolders.Checked) AddHandler fs5.FileFound, AddressOf EventHandler7 AddHandler fs5.FileFound, AddressOf EventHandler6 AddHandler fs5.FileFound, AddressOf EventHandler5 AddText("Note the order of invocation:") fs5.Execute() |
然后,代码将EventHandler7从回调列表中移去并再次调用execute方法:
RemoveHandler fs5.FileFound, AddressOf EventHandler7 AddText(String.Empty) AddText("And then there were two:") fs5.Execute() |
最后,代码移除剩余的事件处理程序:
RemoveHandler fs5.FileFound, AddressOf EventHandler6 RemoveHandler fs5.FileFound, AddressOf EventHandler5 |
记住,在你调用AddHandler和RemoveHandler语句时你提供地址的过程必须有正确的委托类型。因此,除非你提供给AddHandler 和 RemoveHandler地址的过程参数签名与事件的参数相符(就是说,除非它们有正确的委托的类型),否则你的代码将不能被编译。
图9 控制事件处理程序回调的顺序 |
图 9显示了点击Add/RemoveHandler按钮后的显示。正如你所能看到的,事件过程按你将它们添加到回调列表的顺序被调用。