作者:Ted Pattison
|
您可能已經(jīng)對事件進行編程若干年了,但是遷移到 .NET Framework 仍然需要您重新檢查事件的內(nèi)部工作,因為 .NET Framework 中的事件位于委托的頂層。 對委托的了解越多,對事件進行編程時所具有的駕馭能力越強。 開始使用公共語言運行庫 (CLR) 的某個事件驅(qū)動框架(例如 Windows? Forms 或 ASP.NET)時,理解事件在較低的級別如何工作至關重要。 本月我的目標是使您理解事件在較低的級別如何工作。 什么是事件? 事件是一種形式化的軟件模式,在該模式中,通知源將對一個或多個處理程序方法進行回調(diào)。 因此,事件類似于接口和委托,因為它們提供了設計使用回調(diào)方法的應用程序的方法。 但是,事件極大地提高了工作效率,因為它們使用起來比接口或委托更容易。 事件允許編譯器和 Visual Studio? .NET IDE 在幕后為您做大量的工作。 涉及事件的設計基于事件源和一個或多個事件處理程序。 事件源可以是一個類也可以是一個對象。 事件處理程序是綁定到處理程序方法的委托對象。 圖 1 顯示了綁定到其處理程序方法的事件源的高級別視圖。 圖 1 事件源和處理程序 每個事件都是根據(jù)特定的委托類型定義的。 對于事件源定義的每個事件,有一個基于事件的基礎委托類型的私有字段。 該字段用于跟蹤多路廣播委托對象。 事件源還提供允許您注冊所需數(shù)量的事件處理程序的公用注冊方法。 當您創(chuàng)建事件處理程序(委托對象)并在事件源中注冊它時,事件源只是將新的事件處理程序追加到列表的結尾。 然后,事件源可以使用私有字段在多路廣播委托上調(diào)用 Invoke,該多路廣播委托將依次執(zhí)行所有注冊的事件處理程序。 事件的真正的妙處在于對其進行設置的大量工作都已經(jīng)為您做好了。 正如您很快就會看到的,無論任何時候您定義事件時,Visual Basic? .NET 編譯器都會通過自動添加私有委托字段和公用注冊方法幫助您工作。 您還將看到 Visual Studio .NET 可以通過代碼生成器提供更多的幫助,代碼生成器可以自動發(fā)出適用于您的處理程序方法的主干定義。 對事件進行編程 由于 .NET 中的事件建立在委托的頂層,因此它們的基礎的管道詳細信息與較低版本的 Visual Basic 中所一直使用的截然不同。 但是,Visual Basic .NET 的語言設計者們在保持事件編程的語法與較低版本的 Visual Basic 一致方面做得很好。 在很多情況下,對事件進行編程涉及的語法與您習慣使用的熟悉的老語法相同。 例如,您將使用 Event、RaiseEvent 和 WithEvents 等關鍵字,而它們的行為方式與其在較低版本的 Visual Basic 中的行為方式幾乎完全相同。 讓我們通過創(chuàng)建一個基于事件的簡單的回調(diào)設計開始。 首先,我需要通過使用 Event 關鍵字在類定義內(nèi)定義一個事件。 必須根據(jù)特定的委托類型定義每個事件。 下面是定義自定義委托類型和用來定義事件的類的一個示例: 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 中的類定義 當您定義事件時,編譯器在類定義內(nèi)生成四個成員。 第一個成員是基于委托類型的私有字段。 該字段用于跟蹤對委托對象的引用。 編譯器通過采用事件本身的名稱并添加后綴“Event”生成該私有字段的名稱。 這意味著創(chuàng)建名為 LargeWithdraw 的事件將導致創(chuàng)建名為 LargeWithdrawEvent 的私有字段。 編譯器還生成兩個方法,幫助注冊和注銷將成為事件處理程序的委托對象。 這兩個方法使用標準的命名規(guī)則進行命名。 用于注冊事件處理程序的方法使用事件的名稱,并帶有前綴“add_”。 用于注銷事件處理程序的方法使用事件的名稱,并帶有前綴“remove_”。 因此,為 LargeWithdraw 事件創(chuàng)建的兩個方法名為 add_LargeWithdraw 和 remove_LargeWithdraw。 Visual Basic .NET 編譯器通過調(diào)用 Delegate 類的 Combine 方法為將委托對象作為參數(shù)接受并將其添加到處理程序列表中的 add_LargeWithdraw 生成一個實現(xiàn)。 編譯器通過在 Delegate 類中調(diào)用 Remove 方法為從列表中刪除一個處理程序方法的 remove_LargeWithdraw 生成一個實現(xiàn)。 第四個也是最后一個添加到類定義中的成員是表示事件本身的成員。 在圖 2 中,您應該能夠找到名為 LargeWithdraw 的事件成員。 它是旁邊帶有一個倒三角的成員。 但是,您應該注意到,該事件并不象其它三個成員一樣真的是一個物理成員。 相反,它是一個僅包含元數(shù)據(jù)的成員。 此僅包含元數(shù)據(jù)的事件成員很有價值,因為它可以向該類支持的編譯器和其他開發(fā)工具通知 .NET Framework 中事件注冊的標準模式。 該事件成員還包含注冊方法和注銷方法的名稱。 這使得 Visual Basic .NET 和 C# 等托管語言的編譯器可以在編譯時查找注冊方法的名稱。 Visual Studio .NET 是查找此僅包含元數(shù)據(jù)的事件成員的開發(fā)工具的另一個很好的示例。 當 Visual Studio .NET 發(fā)現(xiàn)類定義包含事件時,它將自動生成處理程序方法的主干定義以及將它們作為事件處理程序進行注冊的代碼。 在開始討論激發(fā)事件之前,我想提出一個有關用于定義事件的委托類型的限制。 用于定義事件的委托類型不能有返回值。 您必須使用 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 對此限制有很充分的原因。 當涉及與若干處理程序方法綁定的多路廣播委托時,處理返回值相當困難。 在多路廣播委托上調(diào)用 Invoke 返回與調(diào)用列表中的最后一個處理程序方法相同的值。 但是,捕獲較早在列表中出現(xiàn)的處理程序方法的返回值并不那么簡單。 不需要捕獲多個返回值只會使事件更容易使用。 激發(fā)事件 現(xiàn)在,讓我們修改 BankAccount 類使其在提款數(shù)量超出 $5000 閾值時能夠激發(fā)一個事件。 激發(fā) LargeWithdraw 事件的最簡單的方法是在一個方法、屬性或構造函數(shù)的實現(xiàn)中使用 RaiseEvent 關鍵字。 您可能會覺得該語法很熟悉,因為它類似于您在較低版本的 Visual Basic 中使用的語法。 下面是從 Withdraw 方法激發(fā) 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
|
RaiseEvent LargeWithdraw(Amount)
Visual Basic .NET 編譯器將此表達式擴展為在保留多路廣播委托對象的私有字段上調(diào)用 Invoke 的代碼。 換句話說,使用 RaiseEvent 關鍵字與在以下 snippet 中編寫代碼具有完全相同的效果:
If (Not LargeWithdrawEvent Is Nothing) Then LargeWithdrawEvent.Invoke(Amount) End If
注意,Visual Basic .NET 編譯器生成的代碼執(zhí)行檢查以確保 LargeWithdrawEvent 字段包含對某個對象的有效引用。 這是因為 LargeWithdrawEvent 字段的值在第一個處理程序方法注冊之前一直為 Nothing。 因此,除非當前至少有一個處理程序方法已注冊,否則生成的代碼并不嘗試調(diào)用 Invoke。
您應該能夠?qū)ぐl(fā)事件進行觀察。 使用 RaiseEvent 關鍵字或者根據(jù)編譯器自動生成的 LargeWithdrawEvent 私有字段直接進行編程通常并沒有什么分別。 兩種方法都生成相同的代碼:
'*** this code RaiseEvent LargeWithdraw(Amount) '*** is the same as this code If (Not LargeWithdrawEvent Is Nothing) Then LargeWithdrawEvent.Invoke(Amount) End If
在很多情況下,您可能喜歡使用 RaiseEvent 關鍵字語法,因為它需要的鍵入較少,生成的代碼較簡潔。 但是,在某些情況下,當您需要較多控制時,根據(jù) LargeWithdrawEvent 私有字段進行明確編程可能會有意義。 讓我們看一個這種情況的示例。
想象以下情況:BankAccount 對象有三個事件處理程序已注冊以接收 LargeWithdraw 事件的通知。 如果使用 RaiseEvent 關鍵字觸發(fā)事件并且調(diào)用列表中的第二個事件處理程序出現(xiàn)異常,將會出現(xiàn)什么情況? 包含 RaiseEvent 語句的代碼行將接收運行時異常,但是您可能沒辦法確定哪個事件處理程序?qū)е庐惓!?而且,可能沒有辦法處理第二個事件處理程序?qū)е碌漠惓#矝]有辦法按預期方式在執(zhí)行第三個事件處理程序的位置繼續(xù)進行。
但是,如果您愿意根據(jù) LargeWithdrawEvent 私有字段進行編程,則可以更適當?shù)姆绞教幚硎录幚沓绦驅(qū)е碌漠惓!?檢查 圖 3 中的代碼。 正如您所看到的,降至一個較低的級別并根據(jù)該私有委托字段進行編程可以提供額外程度的控制。 您可以恰當?shù)靥幚懋惓#缓罄^續(xù)執(zhí)行較晚出現(xiàn)在列表中的事件處理程序。 與 RaiseEvent 語法相比,該方法具有明顯的好處,在該方法中一個事件處理程序?qū)е碌漠惓⒆柚箞?zhí)行較晚出現(xiàn)在調(diào)用列表中的任何事件處理程序。
創(chuàng)建和注冊事件處理程序
現(xiàn)在,您已經(jīng)知道如何定義和激發(fā)事件,該是討論如何創(chuàng)建事件處理程序并在給定源中注冊它的時候了。 有兩種不同的方法可以在 Visual Basic .NET 中完成以上操作。 第一種方法稱為動態(tài)事件綁定,涉及 AddHandler 關鍵字的使用。 第二種方法稱為靜態(tài)事件綁定,涉及熟悉的 Visual Basic 關鍵字 WithEvents 的使用。 我打算在下一期討論靜態(tài)事件綁定。 所以現(xiàn)在,讓我們來看一看動態(tài)事件綁定的工作原理。
請記住,事件處理程序是一個委托對象。 因此,可以通過從事件所基于的委托類型實例化一個委托對象,創(chuàng)建一個事件處理程序。 創(chuàng)建此委托對象時,必須將其綁定到要成為事件處理程序的目標處理程序方法。
創(chuàng)建事件處理程序后,必須通過在事件源上調(diào)用特定的注冊方法在特定的事件中注冊它。 回憶一下,LargeWithdraw 事件的注冊方法名為 add_LargeWithdraw。 當您調(diào)用 add_LargeWithdraw 方法并將委托對象作為參數(shù)傳遞時,事件源將委托對象添加到將接收事件通知的事件處理程序列表中。
有關事件注冊會出現(xiàn)混淆的是您從未直接調(diào)用 add_LargeWithdraw 等方法。 實際上,如果您按名稱訪問事件注冊方法,Visual Basic .NET 編譯器將生成編譯時錯誤。 但是,您可以使用包括 AddHandler 語句的替代語法。 當您使用 AddHandler 語句時,Visual Basic .NET 編譯器生成為您調(diào)用事件注冊方法的代碼。
讓我們來看一個使用動態(tài)事件注冊綁定幾個事件處理程序的示例。 想象您已經(jīng)在 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 事件的事件處理程序,您應該做什么? 讓我們從創(chuàng)建綁定到處理程序 LogWithdraw 的事件處理程序開始。 首先,您必須創(chuàng)建將成為事件處理程序的委托對象:
Dim handler1 As LargeWithdrawHandler handler1 = AddressOf AccountHandlers.LogWithdraw
然后,您必須使用 AddHandler 語句在事件源中注冊該新的委托對象。 當您使用 AddHandler 語句注冊事件處理程序時,您需要傳遞兩個參數(shù),類似以下內(nèi)容:
AddHandler <event>, <delegate object>
AddHandler 需要的第一個參數(shù)是對類或?qū)ο蟮氖录M行求值的表達式。 第二個參數(shù)是對將被綁定為事件處理程序的委托對象的引用。 下面是使用 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 編譯器將擴展此代碼以調(diào)用注冊方法 add_LargeWithdraw。 執(zhí)行包含 AddHandler 語句的代碼后,您的事件處理程序已就位,并已準備就緒可以進行通知。 因此,無論任何時候 BankAccount 對象激發(fā) LargeWithdraw 事件時,都將執(zhí)行 LogWithdraw 方法。
在上一示例中,我使用了較長形式的語法以便確切地說明創(chuàng)建和注冊事件處理程序時所發(fā)生的事情。 但是,明白了原理之后,您可能希望使用更簡潔的語法來實現(xiàn)同樣的目標,如下所示:
'*** 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 語句期望將委托對象作為第二個參數(shù)引用,因此您可以使用 AddressOf 運算符的速記語法,后跟目標處理程序方法。 當 Visual Basic .NET 編譯器發(fā)現(xiàn)這種情況后,它就會生成額外的代碼以創(chuàng)建將成為事件處理程序的委托對象。
Visual Basic .NET 語言的 AddHandler 語句由 RemoveHandler 語句補充。 RemoveHandler 需要的兩個參數(shù)與 AddHandler 相同,但是它具有相反的效果。 它通過調(diào)用事件源提供的 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
現(xiàn)在,您已經(jīng)看到了使用事件實現(xiàn)回調(diào)設計所需的所有步驟。上面的代碼顯示了一個完整的應用程序,在該應用程序中兩個事件處理程序已注冊以接收來自 BankAccount 對象的 LargeWithdraw 事件的回調(diào)通知。
小結
雖然使用事件的動機和某些語法與較低版本的 Visual Basic 相比都沒有變,但是,您必須承認現(xiàn)在情況有些不同了。 正如您所看到的,您對如何響應事件的控制能力比以前更強了。 如果您希望降低級別并根據(jù)委托進行編程,則更是如此。
在下一期的 Basic Instincts 欄目中,我打算繼續(xù)有關事件的此討論。 我將向您說明 Visual Basic .NET 如何通過您熟悉的 WithEvents 關鍵字語法支持靜態(tài)事件綁定,并將討論 Handles 子句。 要真正控制事件,您必須能夠輕松駕馭動態(tài)事件注冊和靜態(tài)事件注冊。
更多文章、技術交流、商務合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
