使用 .NET 對事件進行編程

來源:互聯網
上載者:User
 本月的“準系統”專欄建立在本人上兩期專欄的基礎之上,在上兩期“準系統”專欄中,我討論了與委託相關的概念和編程技巧。本文假定讀者已經閱讀了該專欄的上兩期,並且理解委託在 Microsoft.NET Framework 中所扮演的角色。如果您尚未閱讀上兩期專欄,請參閱 Implementing Callback Notifications Using Delegates 和 Implementing Callbacks with a Multicast Delegate。您還應該知道如何設計和編寫使用多路廣播委託將回調通知發送到一組處理常式方法的簡單應用程式。
  
  您可能已經對事件進行編程若干年了,但是遷移到 .NET Framework 仍然需要您重新檢查事件的內部工作,因為 .NET Framework 中的事件位於委託的頂層。對委託瞭解得越多,對事件進行編程時所具有的駕馭能力就越強。在開始使用公用語言運行庫 (CLR) 中的一個事件驅動架構(例如 Windows Forms 或 ASP.NET)時,理解事件在較低的層級如何工作至關重要。本月,我的目標是使您理解事件在較低的層級如何工作。
  
  事件究竟是什嗎?
  
  事件只是一種形式化的軟體模式,在該模式中,通知源對一個或多個處理常式方法進行回調。因此,事件類別似於介面和委託,因為它們提供了設計使用回調方法的應用程式的方法。但是,事件大大提高了工作效率,因為它們使用起來比介面或委託更容易。事件允許編譯器和 Visual Studio.NET IDE 在幕後為您做大量的工作。
  
  涉及事件的設計基於事件來源和一個或多個事件處理常式。事件來源可以是一個類也可以是一個對象。事件處理常式是綁定到處理常式方法的委派物件。圖 1 顯示了一個綁定到其處理常式方法的事件來源的進階別視圖。
  [img]http://www.microsoft.com/china/MSDN/library/netFramework/netframework/art/Eventfig01.gif[/img
  圖 1 事件來源和處理常式
  
  
  每個事件都是根據特定的委託類型定義的。對於事件來源定義的每個事件,都有一個基於事件的基礎委託類型的私人欄位。該欄位用於跟蹤多路廣播委派物件。事件來源還提供允許您註冊所需數量的事件處理常式的公用註冊方法。
  
  當您建立事件處理常式(委派物件)並在某個事件來源中註冊它時,該事件來源只是將新的事件處理常式追加到列表末尾。然後,事件來源可以使用私人欄位在多路廣播委託上調用 Invoke,該多路廣播委託將依次執行所有登入的事件處理常式。
  
  事件的真正好處在於,對其進行的大量設定工作已經為您做好了。正如您很快就會看到的那樣,無論何時您定義事件,Visual Basic.NET 編譯器都會通過自動添加私人委託欄位和公用註冊方法來協助您工作。您還將看到 Visual Studio .NET 可以通過一個代碼產生器提供更多的協助,該代碼產生器可以自動發出適用於您的處理常式方法的主幹定義。
  
  對事件進行編程
  
  由於 .NET 中的事件建立在委託之上,因此它們的基礎結構詳細資料與在早期版本的 Visual Basic 中使用事物的方式截然不同。但是,Visual Basic .NET 的語言設計者在保持事件編程的文法與早期版本的 Visual Basic 相一致方面做得很好。在很多情況下,對事件進行編程會涉及到您習慣使用的熟悉的舊文法。例如,您將使用 Event、RaiseEvent 和 WithEvents 等關鍵字,而它們的行為方式與其在早期版本的 Visual Basic 中的行為方式幾乎完全相同。
  
  下面讓我們先建立一個基於事件的簡單回調設計。首先,我需要使用 Event 關鍵字在類定義中定義一個事件。必鬚根據特定的委託類型定義每個事件。下面是定義自訂委託類型和用來定義事件的類的一個樣本:
  
  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 中的類定義
  
  
  在您定義事件時,編譯器會在類定義內產生四個成員。第一個成員是基於委託類型的私人欄位。該欄位用於跟蹤對委派物件的引用。該編譯器通過採用事件本身的名稱並添加尾碼“Event”來產生該私人欄位的名稱。這意味著,建立名為 LargeWithdraw 的事件將導致建立名為 LargeWithdrawEvent 的私人欄位。
  
  該編譯器還會產生兩個方法,以協助註冊和登出要用作事件處理常式的委派物件。這兩個方法都使用標準命名規範進行命名。用於註冊事件處理常式的方法通過在事件名稱前添加首碼“add_”來命名。用於登出事件處理常式的方法通過在事件名稱前添加首碼“remove_”來命名。因此,為 LargeWithdraw 事件建立的兩個方法名為 add_LargeWithdraw 和 remove_LargeWithdraw。
  
  Visual Basic .NET 編譯器為 add_LargeWithdraw 產生一個實現,該實現將委派物件作為參數接受,並通過調用 Delegate 類的 Combine 方法將委派物件添加到處理常式的列表中。該編譯器還為 remove_LargeWithdraw 產生一個實現,該實現通過調用 Delegate 類中的 Remove 方法從列表中刪除一個處理常式方法。
  
  第四個也是最後一個添加到類定義中的成員是表示事件本身的成員。在圖 2中,您應該能夠找到名為 LargeWithdraw 的事件成員。該成員旁邊帶有一個倒三角。但是,您應該注意到,該事件成員並不像其它三個那樣是一個實際的物理成員。相反,它是一個僅包含中繼資料的成員。
  
  這個僅包含中繼資料的事件成員很有價值,因為它可以通知編譯器和其他開發工具該類支援 .NET Framework 中事件註冊的標準模式。該事件成員還包含註冊方法和登出方法的名稱。這使得 Visual Basic .NET 和 C# 等託管語言的編譯器能夠在編譯時間發現註冊方法的名稱。
  
  Visual Studio .NET 是尋找這個僅包含中繼資料的事件成員的開發工具的另一個很好的樣本。當 Visual Studio .NET 發現類定義包含事件時,它將自動產生處理常式方法的主幹定義,以及將它們作為事件處理常式進行註冊的代碼。
  
  在開始討論引發事件之前,我想提出一個與建立要用於定義事件的委託類型有關的限制。用於定義事件的委託類型不能有傳回值。您必須使用 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
  
  此限制有很充分的原因。當涉及與若干處理常式方法綁定的多路廣播委託時,處理傳回值相當困難。在多路廣播委託上調用 Invoke 將返回與調用列表中最後一個處理常式方法相同的值。但是,捕獲先前在列表中出現的處理常式方法的傳回值並不是那麼簡單。消除對捕獲多個傳回值的需要只會使事件更加便於使用。
  
  引發事件
  
  現在,讓我們修改 BankAccount 類,使其能夠在提款數額超出 $5000 閾值時引發一個事件。引發 LargeWithdraw 事件的最簡單方法是,在一個方法、屬性或建構函式的實現中使用 RaiseEvent 關鍵字。您可能會覺得該文法很熟悉,因為它類似於您在早期版本的 Visual Basic 中使用的文法。下面是從 Withdraw 方法引發 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
  
  雖然該文法與早期版本的 Visual Basic 相同,但是現在引發事件時所發生的事情則截然不同。在您使用 RaiseEvent 關鍵字引發事件時,Visual Basic .NET 編譯器會產生執行每個事件處理常式所需的代碼。例如,在編譯以下代碼時,您認為會發生什麼事情?
  
  RaiseEvent LargeWithdraw(Amount)
  
  Visual Basic .NET 編譯器將該運算式擴充為在保留多路廣播委派物件的私人欄位上調用 Invoke 的代碼。換句話說,使用 RaiseEvent 關鍵字與編寫以下程式碼片段具有完全相同的效果:
  
  If (Not LargeWithdrawEvent Is Nothing) Then
   LargeWithdrawEvent.Invoke(Amount)
  End If
  
  請注意,Visual Basic .NET 編譯器產生的程式碼將執行一個檢查,以確保 LargeWithdrawEvent 欄位包含對某個對象的有效引用。這是因為 LargeWithdrawEvent 欄位的值在第一個處理常式方法註冊之前一直為 Nothing。因此,除非當前至少有一個處理常式方法登入,否則產生的程式碼將不會嘗試調用 Invoke。
  
  您應該能夠對引發事件進行觀察。使用 RaiseEvent 關鍵字或者針對編譯器自動產生的 LargeWithdrawEvent 私人欄位直接進行編程,通常沒有什麼分別。這兩種方法會產生相同的代碼:
  
  '*** this code
  RaiseEvent LargeWithdraw(Amount)
  
  '*** is the same as this code
  If (Not LargeWithdrawEvent Is Nothing) Then
   LargeWithdrawEvent.Invoke(Amount)
  End If
  
  在很多情況下,您可能喜歡使用 RaiseEvent 關鍵字的文法,因為它要求鍵入的內容較少,並且產生的程式碼更簡潔。但是,在某些需要較多控制的情況下,針對 LargeWithdrawEvent 私人欄位進行顯式編程可能很有意義。讓我們看一個這種情況的樣本。
  
  請想象以下情況:BankAccount 對象有三個事件處理常式登入,以接收 LargeWithdraw 事件的通知。如果使用 RaiseEvent 關鍵字觸發該事件,並且調用列表中的第二個事件處理常式引發了一個異常,將會發生什麼事情?包含 RaiseEvent 語句的程式碼將接收一個運行時異常,但是您可能無法確定是哪個事件處理常式引發的異常。而且,可能無法處理第二個事件處理常式引發的異常,也沒有辦法按預期方式在執行第三個事件處理常式的位置繼續進行。
  
  但是,如果您願意根據 LargeWithdrawEvent 私人欄位進行編程,則可以用更適當的方式來處理事件處理常式引發的異常。請查看圖 3 中的代碼。正如您所看到的那樣,降至一個較低的層級並根據該私人委託欄位進行編程可以提供額外的控制層級。您可以恰當地處理異常,然後繼續執行隨後出現在列表中的事件處理常式。與 RaiseEvent 文法相比,該方法具有明顯的優勢,在 RaiseEvent 文法中,一個事件處理常式引發的異常將阻止執行隨後出現在調用列表中的任何事件處理常式。
  Figure 3 Using the Private Delegate Field
  
  Sub Withdraw(ByVal Amount As Decimal)
   '*** send notifications if required
   If (Amount > 5000) AndAslo (Not LargeWithdrawEvent Is Nothing) Then
   Dim handler As LargeWithdrawHandler
   For Each handler In LargeWithdrawEvent.GetInvocationList()
   Try
   handler.Invoke(Amount)
   Catch ex As Exception
   '*** deal with exceptions as they occur
   End Try
   Next
   End If
   '*** perform withdrawal
  End Sub
  
  
  建立和註冊事件處理常式
  
  現在,您已經知道如何定義和引發事件,下面該討論如何建立事件處理常式並在給定源中註冊它了。在 Visual Basic .NET 中有兩種不同的方法可以完成上述操作。第一種方法稱為動態事件綁定,它涉及 AddHandler 關鍵字的使用。第二種方法稱為靜態事件綁定,它涉及您熟悉的 Visual Basic 關鍵字 WithEvents 的使用。我打算在以後的專欄中討論靜態事件綁定。所以現在讓我們來看一下動態事件綁定的工作原理。
  
  請記住,事件處理常式是一個委派物件。因此,您可以通過從事件所基於的委託類型執行個體化一個委派物件,來建立一個事件處理常式。建立該委派物件時,必須將其綁定到要用作事件處理常式的目標處理常式方法。
  
  建立事件處理常式後,必須通過在事件來源上調用特定的註冊方法,以便在特定的事件中註冊它。回想一下,LargeWithdraw 事件的註冊方法名為 add_LargeWithdraw。當您調用 add_LargeWithdraw 方法並將委派物件作為參數傳遞時,事件來源會將委派物件添加到要接收事件通知的事件處理常式的列表中。
  
  有關事件註冊的混淆情況是,您從不直接調用 add_LargeWithdraw 等註冊方法。實際上,如果您嘗試按名稱訪問事件註冊方法,則 Visual Basic .NET 編譯器將引發編譯時間錯誤。但是,您可以使用包含 AddHandler 語句的替代文法。當您使用 AddHandler 語句時,Visual Basic .NET 編譯器將產生為您呼叫事件註冊方法的代碼。
  
  讓我們來看一個使用動態事件註冊綁定幾個事件處理常式的樣本。假定您已經在 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 事件的事件處理常式,您應該做什嗎?讓我們從建立綁定到處理常式 LogWithdraw 的事件處理常式開始。首先,您必須建立要用作事件處理常式的委派物件:
  
  Dim handler1 As LargeWithdrawHandler
  handler1 = AddressOf AccountHandlers.LogWithdraw
  
  然後,您必須使用 AddHandler 語句在事件來源中註冊這個新的委派物件。當您使用 AddHandler 語句註冊事件處理常式時,您需要傳遞兩個參數,如下所示:
  
  AddHandler <event>, <delegate object>
  
  AddHandler 需要的第一個參數是對類或對象的事件進行求值的運算式。第二個參數是對將綁定為事件處理常式的委派物件的引用。下面是一個使用 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 編譯器將擴充此代碼,以調用註冊方法 add_LargeWithdraw。一旦執行包含 AddHandler 語句的代碼後,您的事件處理常式就已經準備就緒,可以接收通知了。因此,無論何時 BankAccount 對象引發 LargeWithdraw 事件,都將執行 LogWithdraw 方法。
  
  在上一個樣本中,我使用了較長形式的文法,以便確切說明在您建立和註冊事件處理常式時所發生的事情。但是,瞭解原理之後,您可能希望使用更簡潔的文法來實現同樣的目標,如下所示:
  
  '*** 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 語句期望將委派物件作為第二個參數引用,因此您可以使用 AddressOf 操作符的簡化文法,後跟目標處理常式方法的名稱。當 Visual Basic .NET 編譯器發現這種情況後,它就會產生額外的代碼以建立要用作事件處理常式的委派物件。
  
  Visual Basic .NET 語言的 AddHandler 語句由 RemoveHandler 語句補充。RemoveHandler 需要的兩個參數與 AddHandler 相同,但是它具有相反的效果。它通過呼叫事件源提供的 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
  
  現在,您已經瞭解了使用事件實現回調設計所需的所有步驟。圖 4 中的代碼顯示了一個完整的應用程式,在該應用程式中,已經註冊了兩個事件處理常式,以接收來自 BankAccount 對象的 LargeWithdraw 事件的回調通知。
  Figure 4 An Event-based Design for Callback Notifications
  
  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
  
  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
  
  Module MyApp
   Sub Main()
   '*** create bank account object
   Dim account1 As New BankAccount()
   '*** register event handlers
   AddHandler account1.LargeWithdraw, _
   AddressOf AccountHandlers.LogWithdraw
   AddHandler account1.LargeWithdraw, _
   AddressOf AccountHandlers.GetApproval
   '*** do something that triggers callback
   account1.Withdraw(5001)
   End Sub
  End Module
  
  
  小結
  
  雖然使用事件的動機和某些文法與早期版本的 Visual Basic 相同,但是您必須承認現在的情況大大不同了。正如您所看到的那樣,與以前相比,您對如何響應事件的控制能力更強了。如果您希望降低層級並根據委託進行編程,則更是如此。
  
  在下一期的“準系統”專欄中,我打算繼續進行有關事件的討論。我將向您說明 Visual Basic .NET 如何通過您熟悉的 WithEvents 關鍵字文法支援靜態事件綁定,並將討論 Handles 子句。要真正控制事件,您必須能夠輕鬆駕馭動態事件註冊和靜態事件註冊。
  
  請將給 Ted 的問題和意見發送至 instinct@microsoft.com。
  
  Ted Pattison 是 DevelopMentor (http://www.develop.com) 的講師兼研究員,他在 DevelopMentor 與別人共同管理 Visual Basic 課程。他是 Programming Distributed Applications with COM and Microsoft Visual Basic 6.0 (Microsoft Press, 2000) 一書的作者。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.