Framework 類庫的事件編程發布日期: 1/24/2005 | 更新日期: 1/24/2005
Ted Pattison
本頁內容
|
EventHandler 委託 |
|
自訂的事件參數 |
|
參數化自訂事件 |
|
小結 |
本月的內容是專門介紹事件編程的系列專欄(共三期)的最後一期。在前兩期專欄中,我已經介紹了如何定義和引發事件(請參見 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.EmptyEnd 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 IfEnd 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 SubEnd Class
假設要求 BankAccount 對象在每次遇到提款金額大於 $5,000 的情況時都引發一個事件。在引發該事件時,要求您將提款金額作為參數傳遞給所有登入的事件處理常式。首先,您應該建立一個新的事件參數類,它從 EventArgs 類繼承:
Public Class LargeWithdrawArgs : Inherits EventArgs Public Amount As Decimal Sub New(ByVal Amount As Decimal) Me.Amount = Amount End SubEnd 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 SubEnd 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.AmountEnd 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) 的書籍。