2024年5月24日 星期五

6.VB.NET 進階篇 筆記 - WithEvents 關鍵字

VB.NET WithEvents 關鍵字 筆記(進階篇)

VB.NET WithEvents 關鍵字 筆記(進階篇)

Windows Forms 的程式多半不是一次從上到下執行完就結束,而是先顯示畫面,接著等待事件發生。按鈕被點擊、Timer 到時間、自訂物件完成工作,都是事件發生的時機。

事件發生後,需要有一段 Sub 負責接收並處理。WithEvents 的核心作用,就是讓某個類別層級變數成為可被 Handles 追蹤的事件來源。換句話說,WithEvents 不是事件本身,而是讓「某個物件的事件」可以被表單中的事件處理程序接住。

整件事可整理成一句話:物件發出事件,WithEvents 保留事件來源,Handles 指定事件發生後要執行哪個方法。

先理解事件完整流程

事件不是單一語法,而是一條連線

WithEvents 要放在事件流程中理解。完整事件流程包含四個角色:

  • 事件來源:會發出事件的物件,例如 ButtonTimer、自訂的 JobRunner
  • 事件本身:物件對外提供的通知,例如 ClickTickJobFinished
  • 事件處理程序:事件發生後要執行的 Sub
  • 事件連線:指定哪個事件發生時,要執行哪個 Sub

WithEvents 與 Handles 的分工:

  1. WithEvents 放在變數宣告上,表示這個變數可作為事件來源。
  2. Handles 放在事件處理程序後面,表示這個方法要接收哪個事件。
  3. RaiseEvent 放在事件來源類別內部,用來真正發出事件。

最小結構:看懂事件如何接起來

以下程式碼先不看完整應用,只看事件來源與事件處理程序如何連接。

VB.NET
Private WithEvents runner As JobRunner

Private Sub runner_JobFinished(ByVal summary As String) Handles runner.JobFinished
    LabelStatus.Text = summary
End Sub
邏輯解析
  • runner 是表單保留的事件來源變數。
  • WithEventsrunner 的事件可以寫在 Handles 後面。
  • runner.JobFinished 代表 runner 發出的 JobFinished 事件。
  • runner_JobFinished 是事件發生後要執行的方法。
  • 整體意思是:當 runner 發出 JobFinished 時,執行 runner_JobFinished

為什麼一般變數不能直接 Handles?

Handles runner.JobFinished 需要編譯器知道 runner 是可追蹤事件的來源。一般類別層級變數雖然也能存放物件,但沒有標示 WithEvents 時,不能直接被 Handles 使用。

因此,WithEvents 的重點不是讓物件擁有事件,而是讓這個變數能和 Handles 建立事件接收關係。

為什麼事件處理程序通常是 Sub

事件是通知,不是詢問

在 VB.NET 的事件模型中,事件來源通常只是通知外部「某件事發生了」。事件來源不會期待事件處理程序回傳一個值,因此接收事件的方法通常寫成 Sub,而不是 Function

  • Button.Click:通知按鈕被點擊。
  • Timer.Tick:通知時間間隔已到。
  • JobFinished:通知工作已完成。
  • BatchChecked:通知批次檢核已完成。

事件處理程序使用 Sub 的原因

JobFinished 為例,事件來源只負責把結果送出。表單收到事件後更新畫面。這個更新動作不需要把值回傳給 JobRunner

VB.NET
Private Sub runner_JobFinished(ByVal summary As String) Handles runner.JobFinished
    LabelStatus.Text = summary
End Sub
邏輯解析
  • runner_JobFinished 是事件發生後要執行的處理程序。
  • summary 是事件來源傳進來的資料。
  • LabelStatus.Text = summary 是接收到事件後要做的事情。
  • 整個流程只需要處理通知,不需要回傳值,因此使用 Sub

需要回傳結果時,不適合用事件處理程序回傳

如果流程需要「呼叫某個方法並取得計算結果」,通常應使用一般 Function、委派 Delegate Function,或直接呼叫服務方法。事件比較適合用來通知狀態,而不是要求接收端回傳計算結果。

宣告條件與語法結構

語法要看成事件接線

Private WithEvents obj As 某類型 表示表單保留一個可接收事件的來源。Handles obj.SomeEvent 表示某個 Sub 接收這個來源的某個事件。前者準備事件來源,後者指定事件發生後要執行哪段程式。

項目 可否使用 說明
方法內區域變數 不可 WithEvents 必須宣告在類別層級,不能寫在 SubFunction 內部。
類別層級欄位 這是 WithEvents 的主要使用位置,例如 Private WithEvents runner As JobRunner
Handles 搭配 WithEvents Handles runner.JobFinished 代表該方法會接收 runner 的事件。
AddHandler 搭配 WithEvents 可,但需避免重複 同一事件若同時用 HandlesAddHandler 綁到同一方法,事件處理程序可能執行兩次。

實務應用場景

Windows Forms 物件配置

以下範例皆以 Windows Forms 為主。不同場景會使用不同控制項名稱,測試時可依場景建立對應控制項。

場景一:自訂物件完成工作後通知表單

此範例建立一個 JobRunner 類別。按下按鈕後,JobRunner 執行工作並發出 JobFinished 事件,表單接收事件後更新 LabelStatus

需要的主控項
  • ButtonStart:按下後執行工作。
  • LabelStatus:顯示事件通知內容。
範例程式碼
VB.NET / Windows Forms
Public Class JobRunner
    Public Event JobFinished(ByVal summary As String)

    Public Sub Execute()
        Dim completedTaskCount As Integer = 4
        Dim reportText As String = "資料整理完成,共處理 " & completedTaskCount.ToString() & " 個區段。"

        RaiseEvent JobFinished(reportText)
    End Sub
End Class

Public Class Form1
    Private WithEvents runner As JobRunner

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        runner = New JobRunner()
        LabelStatus.Text = "等待執行"
    End Sub

    Private Sub ButtonStart_Click(sender As Object, e As EventArgs) Handles ButtonStart.Click
        runner.Execute()
    End Sub

    Private Sub runner_JobFinished(ByVal summary As String) Handles runner.JobFinished
        LabelStatus.Text = summary
    End Sub
End Class
畫面輸出結果(LabelStatus.Text)
資料整理完成,共處理 4 個區段。
事件流程解析
  • JobRunner 是事件來源。
  • Public Event JobFinished(...) 宣告 JobRunner 會發出 JobFinished 事件。
  • RaiseEvent JobFinished(reportText) 是事件真正發出的地方。
  • Private WithEvents runner As JobRunner 讓表單保留可被追蹤的事件來源。
  • Handles runner.JobFinished 把事件和 runner_JobFinished 接起來。
  • 按鈕只負責呼叫 runner.Execute(),真正更新畫面的是事件處理程序。

場景二:替換 WithEvents 參考後,事件來源跟著改變

WithEvents 變數可以改指向另一個物件。當變數指向新物件後,Handles 會追蹤新物件發出的事件。這種做法常用於切換裝置、切換資料來源或切換工作物件。

需要的主控項
  • ButtonUseNorth:切換到北區感測器。
  • ButtonUseSouth:切換到南區感測器。
  • ButtonRead:執行讀值。
  • LabelResult:顯示目前來源與讀值結果。
範例程式碼
VB.NET / Windows Forms
Public Class SensorReader
    Public Event ReadingCompleted(ByVal deviceName As String, ByVal measuredValue As Decimal)

    Private ReadOnly _deviceName As String
    Private ReadOnly _baseValue As Decimal

    Public Sub New(ByVal deviceName As String, ByVal baseValue As Decimal)
        _deviceName = deviceName
        _baseValue = baseValue
    End Sub

    Public Sub ReadNow()
        Dim finalValue As Decimal = _baseValue + 1.7D
        RaiseEvent ReadingCompleted(_deviceName, finalValue)
    End Sub
End Class

Public Class Form1
    Private WithEvents currentReader As SensorReader

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        currentReader = New SensorReader("北區感測器", 24.3D)
        LabelResult.Text = "目前來源:北區感測器"
    End Sub

    Private Sub ButtonUseNorth_Click(sender As Object, e As EventArgs) Handles ButtonUseNorth.Click
        currentReader = New SensorReader("北區感測器", 24.3D)
        LabelResult.Text = "已切換至北區感測器"
    End Sub

    Private Sub ButtonUseSouth_Click(sender As Object, e As EventArgs) Handles ButtonUseSouth.Click
        currentReader = New SensorReader("南區感測器", 27.8D)
        LabelResult.Text = "已切換至南區感測器"
    End Sub

    Private Sub ButtonRead_Click(sender As Object, e As EventArgs) Handles ButtonRead.Click
        currentReader.ReadNow()
    End Sub

    Private Sub currentReader_ReadingCompleted(ByVal deviceName As String,
                                                ByVal measuredValue As Decimal) Handles currentReader.ReadingCompleted
        LabelResult.Text = deviceName & " 讀值完成:" & measuredValue.ToString("0.0") & " °C"
    End Sub
End Class
畫面輸出結果(按下 ButtonUseSouth,再按 ButtonRead)
南區感測器 讀值完成:29.5 °C
事件流程解析
  • currentReaderWithEvents 變數,也是 Handles 追蹤的事件來源。
  • 按下 ButtonUseSouth 後,currentReader 改指向南區感測器。
  • 之後呼叫 currentReader.ReadNow(),發出事件的是南區感測器。
  • Handles currentReader.ReadingCompleted 接收目前 currentReader 指向物件發出的事件。
  • 這個特性讓事件處理程序不用重寫,只需替換 WithEvents 變數指向的物件。

場景三:搭配自訂 EventArgs 傳遞完整資料

事件如果只傳一段文字,資料結構會太鬆散。當事件需要傳遞批號、合格數、異常數等多欄資料時,適合建立自訂 EventArgs 類別。

需要的主控項
  • ButtonGenerate:產生批次檢核結果。
  • LabelSummary:顯示檢核摘要。
範例程式碼
VB.NET / Windows Forms
Public Class BatchCheckedEventArgs
    Inherits EventArgs

    Public Sub New(ByVal batchNo As String, ByVal passedCount As Integer, ByVal failedCount As Integer)
        Me.BatchNo = batchNo
        Me.PassedCount = passedCount
        Me.FailedCount = failedCount
    End Sub

    Public Property BatchNo As String
    Public Property PassedCount As Integer
    Public Property FailedCount As Integer
End Class

Public Class BatchInspector
    Public Event BatchChecked As EventHandler(Of BatchCheckedEventArgs)

    Public Sub CheckBatch()
        Dim info As New BatchCheckedEventArgs("LOT-2408", 18, 2)
        RaiseEvent BatchChecked(Me, info)
    End Sub
End Class

Public Class Form1
    Private WithEvents inspector As BatchInspector

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        inspector = New BatchInspector()
        LabelSummary.Text = "等待檢核"
    End Sub

    Private Sub ButtonGenerate_Click(sender As Object, e As EventArgs) Handles ButtonGenerate.Click
        inspector.CheckBatch()
    End Sub

    Private Sub inspector_BatchChecked(sender As Object,
                                       e As BatchCheckedEventArgs) Handles inspector.BatchChecked
        LabelSummary.Text = "批號:" & e.BatchNo & vbCrLf &
                            "合格:" & e.PassedCount.ToString() & vbCrLf &
                            "異常:" & e.FailedCount.ToString()
    End Sub
End Class
畫面輸出結果(LabelSummary.Text)
批號:LOT-2408 合格:18 異常:2
事件流程解析
  • BatchCheckedEventArgs 封裝事件要傳出的資料。
  • EventHandler(Of BatchCheckedEventArgs) 是標準 .NET 事件寫法。
  • RaiseEvent BatchChecked(Me, info) 發出事件,並把檢核資料放在 e 裡。
  • Private WithEvents inspector As BatchInspector 讓表單可以用 Handles inspector.BatchChecked 接收事件。
  • 事件處理程序可直接讀取 e.BatchNoe.PassedCounte.FailedCount

場景四:多個動態控制項共用事件時使用 AddHandler

WithEvents + Handles 適合固定來源;AddHandler 適合執行期間才建立的來源。此範例用迴圈建立三顆按鈕,並讓它們共用同一個事件處理程序。

需要的主控項
  • Panel1:作為動態按鈕容器。
  • LabelInfo:顯示被點擊的按鈕名稱。
範例程式碼
VB.NET / Windows Forms
Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Panel1.Controls.Clear()

        For index As Integer = 1 To 3
            Dim optionButton As New Button()
            optionButton.Name = "ButtonOption" & index.ToString()
            optionButton.Text = "方案 " & index.ToString()
            optionButton.Size = New Size(110, 36)
            optionButton.Location = New Point(12, 12 + ((index - 1) * 46))

            AddHandler optionButton.Click, AddressOf DynamicButton_Click
            Panel1.Controls.Add(optionButton)
        Next

        LabelInfo.Text = "請選擇方案"
    End Sub

    Private Sub DynamicButton_Click(sender As Object, e As EventArgs)
        Dim clickedButton As Button = DirectCast(sender, Button)
        LabelInfo.Text = "目前選取:" & clickedButton.Text
    End Sub
End Class
畫面輸出結果(點擊第二顆動態按鈕)
目前選取:方案 2
事件流程解析
  • 三顆按鈕是在 Form1_Load 中動態建立。
  • AddHandler optionButton.Click, AddressOf DynamicButton_Click 讓每顆按鈕共用同一個事件處理程序。
  • sender 代表實際被點擊的按鈕,因此可用 DirectCast(sender, Button) 取得按鈕文字。
  • 這類執行期建立的事件來源,不需要事先宣告成 WithEvents

WithEvents 與 AddHandler 的差異

兩者都能讓事件發生後執行指定方法,但表達的重點不同。WithEvents + Handles 把事件關係寫在方法宣告後方;AddHandler 則是在執行期間用程式碼主動掛上事件。

比較面向 WithEvents + Handles AddHandler / RemoveHandler
事件關係寫在哪裡 寫在事件處理程序後面的 Handles 寫在執行流程中的 AddHandler
事件來源 適合固定或少量事件來源。 適合動態、大量或執行期建立的事件來源。
閱讀方式 查看方法宣告即可看到 Handles obj.Event 需查看程式何處執行 AddHandler
移除事件 通常由物件參考與生命週期管理。 可用 RemoveHandler 明確移除。
常見情境 固定表單控制項、自訂工作物件、固定資料來源。 動態按鈕、動態控制項、可變數量物件、臨時事件綁定。

使用限制與注意事項

常見限制

  • WithEvents 只能用在類別層級,不能宣告在方法內部。
  • WithEvents 變數尚未指向有效物件時,不會收到事件。
  • 同一事件若同時使用 HandlesAddHandler 綁到同一方法,可能造成事件處理程序執行兩次。
  • 大量動態物件或需要明確移除事件時,應評估 AddHandlerRemoveHandler
  • 事件處理程序若包含耗時工作,仍需考慮背景執行,避免表單停滯。

整體流程整理:

  1. 物件用 Public Event 宣告事件。
  2. 物件在適當時機用 RaiseEvent 發出事件。
  3. 表單用 WithEvents 保留事件來源。
  4. 表單用 Handles 來源.事件 指定事件發生後要執行的處理程序。
  5. 動態建立的事件來源,可改用 AddHandler 在執行期掛接事件。

重點整理

  1. WithEvents 是可被 Handles 追蹤事件的類別層級物件參考。
  2. Handles 變數名.事件名 可把事件處理程序和事件來源連起來。
  3. WithEvents 不能宣告在方法內,只能宣告在類別層級。
  4. 事件處理程序通常使用 Sub,因為事件是通知,不是要求接收端回傳結果。
  5. RaiseEvent 是自訂事件真正發出的地方。
  6. 事件來源固定時,WithEvents + Handles 可讀性較高。
  7. 動態建立大量事件來源時,AddHandler 通常比 WithEvents 更適合。
  8. 事件資料較完整時,應使用自訂 EventArgs 傳遞資料。