2024年7月30日 星期二

23.VB.NET 筆記 進階篇 - BeginInvoke

VB.NET BeginInvoke 筆記(進階篇)

VB.NET BeginInvoke 筆記(進階篇)

BeginInvoke 常用在 Windows Forms 的跨執行緒 UI 更新。背景執行緒不能直接修改 LabelProgressBarListBox 等控制項,因此需要把畫面更新動作交回控制項所屬的 UI 執行緒。

BeginInvoke 的核心是「非同步排入」。它會把指定的 UI 更新工作排進 UI 執行緒佇列,呼叫端不等待該工作完成,就會繼續往下執行。這讓它很適合做進度回報、狀態通知與清單紀錄,但不適合把真正耗時的工作放進 UI 更新區塊。

先理解 BeginInvoke 在解決什麼

BeginInvoke:把一段委派程式非同步排入控制項所屬的 UI 執行緒執行。它的目的不是建立背景工作,而是讓背景工作能安全通知 UI 更新畫面。

BeginInvoke 的重點

  • 切回 UI 執行緒:背景執行緒不直接操作控制項。
  • 非同步排入:呼叫後不等待 UI 更新完成。
  • 適合通知型更新:進度、狀態、清單紀錄、完成提示。
  • 不適合放重型工作:委派內容仍然會在 UI 執行緒執行。
  • 不能立刻依賴結果:下一行程式執行時,畫面不一定已經完成更新。

基本安全模式

VB.NET / Windows Forms
If LabelStatus.InvokeRequired Then
    LabelStatus.BeginInvoke(New MethodInvoker(Sub()
                                                  LabelStatus.Text = "更新文字"
                                              End Sub))
Else
    LabelStatus.Text = "更新文字"
End If

InvokeRequired 用來判斷目前是否需要切回 UI 執行緒。若已經在 UI 執行緒,就可直接更新;若在背景執行緒,就使用 BeginInvoke 排回 UI 執行緒。

項目 BeginInvoke 實務理解
執行方式 非同步排入。 排進 UI 佇列後,呼叫端繼續執行。
是否等待完成 不等待。 不能假設下一行時畫面已更新。
主要用途 跨執行緒 UI 更新。 背景流程完成後通知畫面。
委派內容 在 UI 執行緒執行。 只放畫面更新,不放耗時計算。

基本使用:背景完成後更新狀態

場景一:櫃台資料檢查完成通知

這個範例用背景執行緒模擬資料檢查。檢查完成後,不直接修改 LabelStatus,而是透過共用方法把狀態文字排回 UI 執行緒。

需要的主控項
  • ButtonStartCheck:啟動資料檢查。
  • LabelStatus:顯示目前狀態。
範例程式碼
VB.NET / Windows Forms
Imports System.Threading

Public Class Form1
    Private Sub ButtonStartCheck_Click(sender As Object, e As EventArgs) Handles ButtonStartCheck.Click
        LabelStatus.Text = "資料檢查中..."

        Dim worker As New Thread(AddressOf RunCheckJob)
        worker.IsBackground = True
        worker.Start()
    End Sub

    Private Sub RunCheckJob()
        Thread.Sleep(900)
        QueueStatusText("資料檢查完成:可進行下一步")
    End Sub

    Private Sub QueueStatusText(ByVal message As String)
        If LabelStatus.IsDisposed OrElse Not LabelStatus.IsHandleCreated Then
            Return
        End If

        If LabelStatus.InvokeRequired Then
            Try
                LabelStatus.BeginInvoke(New MethodInvoker(Sub()
                                                              If Not LabelStatus.IsDisposed Then
                                                                  LabelStatus.Text = message
                                                              End If
                                                          End Sub))
            Catch ex As InvalidOperationException
                ' 表單關閉中,忽略尚未完成的畫面更新。
            End Try
        Else
            LabelStatus.Text = message
        End If
    End Sub
End Class
畫面輸出結果(LabelStatus.Text)
資料檢查中... 資料檢查完成:可進行下一步
邏輯解析
  • RunCheckJob 在背景執行緒執行。
  • QueueStatusText 集中處理跨執行緒 UI 更新。
  • BeginInvoke 只排入畫面更新,不等待 Label 實際更新完成。
  • IsHandleCreatedIsDisposed 可降低表單關閉時排入更新的風險。

進度更新:背景流程持續回報 UI

場景二:文件掃描進度回報

背景工作分段執行時,可以每完成一段就排入一次 UI 更新。這個範例會更新 ProgressBarLabel,但真正的掃描流程仍留在背景執行緒。

需要的主控項
  • ButtonScan:開始掃描。
  • ProgressBarScan:顯示進度。
  • LabelProgress:顯示進度文字。
範例程式碼
VB.NET / Windows Forms
Imports System.Threading

Public Class Form1
    Private Sub ButtonScan_Click(sender As Object, e As EventArgs) Handles ButtonScan.Click
        ProgressBarScan.Value = 0
        LabelProgress.Text = "掃描進度:0%"

        Dim worker As New Thread(AddressOf RunScan)
        worker.IsBackground = True
        worker.Start()
    End Sub

    Private Sub RunScan()
        For pageNo As Integer = 1 To 5
            Thread.Sleep(350)
            QueueScanProgress(pageNo * 20)
        Next
    End Sub

    Private Sub QueueScanProgress(ByVal percent As Integer)
        If ProgressBarScan.IsDisposed OrElse Not ProgressBarScan.IsHandleCreated Then
            Return
        End If

        If ProgressBarScan.InvokeRequired Then
            ProgressBarScan.BeginInvoke(New MethodInvoker(Sub()
                                                              ProgressBarScan.Value = percent
                                                              LabelProgress.Text = "掃描進度:" & percent.ToString() & "%"
                                                          End Sub))
        Else
            ProgressBarScan.Value = percent
            LabelProgress.Text = "掃描進度:" & percent.ToString() & "%"
        End If
    End Sub
End Class
畫面輸出結果(完成後)
掃描進度:100%
邏輯解析
  • 背景執行緒用迴圈模擬分段掃描。
  • QueueScanProgress 每次只排入簡短 UI 更新。
  • 多個控制項可以放在同一個 BeginInvoke 區塊中一起更新。

更新頻率提醒:進度更新不需要每毫秒排入一次。若更新過於頻繁,UI 執行緒會忙著處理大量畫面訊息,反而造成卡頓。

清單紀錄:把背景訊息追加到 ListBox

場景三:批次標籤產生紀錄

背景工作產生多筆紀錄時,可以透過 BeginInvoke 將訊息逐筆追加到 ListBox。背景執行緒只負責產生資料,畫面追加交給 UI 執行緒。

需要的主控項
  • ButtonBuildTags:開始產生標籤。
  • ListBoxLog:顯示紀錄。
  • LabelLogStatus:顯示狀態。
範例程式碼
VB.NET / Windows Forms
Imports System.Threading

Public Class Form1
    Private Sub ButtonBuildTags_Click(sender As Object, e As EventArgs) Handles ButtonBuildTags.Click
        ListBoxLog.Items.Clear()
        LabelLogStatus.Text = "標籤產生中..."

        Dim worker As New Thread(AddressOf BuildTags)
        worker.IsBackground = True
        worker.Start()
    End Sub

    Private Sub BuildTags()
        For tagNo As Integer = 1 To 4
            Thread.Sleep(250)
            QueueLogText("標籤完成:TAG-" & tagNo.ToString("000"))
        Next

        QueueLogText("全部標籤已完成")
    End Sub

    Private Sub QueueLogText(ByVal message As String)
        If ListBoxLog.IsDisposed OrElse Not ListBoxLog.IsHandleCreated Then
            Return
        End If

        If ListBoxLog.InvokeRequired Then
            ListBoxLog.BeginInvoke(New MethodInvoker(Sub()
                                                        ListBoxLog.Items.Add(message)
                                                        LabelLogStatus.Text = message
                                                    End Sub))
        Else
            ListBoxLog.Items.Add(message)
            LabelLogStatus.Text = message
        End If
    End Sub
End Class
畫面輸出結果(ListBoxLog)
標籤完成:TAG-001 標籤完成:TAG-002 標籤完成:TAG-003 標籤完成:TAG-004 全部標籤已完成
邏輯解析
  • 背景執行緒逐步產生標籤紀錄。
  • QueueLogText 將清單追加與狀態顯示集中處理。
  • 這種寫法適合背景流程需要多次回報畫面的情境。

BeginInvoke 與 Invoke 的差異

比較項目 BeginInvoke Invoke
執行模式 非同步排入 UI 執行緒。 同步切回 UI 執行緒。
是否等待完成 不等待。 會等待委派執行完成。
適合用途 狀態通知、進度回報、清單紀錄。 後續流程必須等待 UI 更新完成。
常見風險 排入後立刻讀取 UI,可能讀到舊狀態。 不當互相等待時可能造成卡住。

場景四:排入狀態後繼續背景流程

這個範例刻意在背景流程中先排入畫面更新,再繼續做背景端紀錄。因為 BeginInvoke 不等待 UI 完成,所以背景流程可以先往下執行。

需要的主控項
  • ButtonQueueState:開始排入狀態。
  • ListBoxTrace:顯示流程紀錄。
範例程式碼
VB.NET / Windows Forms
Imports System.Threading

Public Class Form1
    Private Sub ButtonQueueState_Click(sender As Object, e As EventArgs) Handles ButtonQueueState.Click
        ListBoxTrace.Items.Clear()

        Dim worker As New Thread(AddressOf RunQueueState)
        worker.IsBackground = True
        worker.Start()
    End Sub

    Private Sub RunQueueState()
        QueueTrace("背景流程:準備排入 UI 更新")
        QueueTrace("UI 更新:狀態文字已排入")
        QueueTrace("背景流程:排入後繼續往下執行")
    End Sub

    Private Sub QueueTrace(ByVal message As String)
        If ListBoxTrace.InvokeRequired Then
            ListBoxTrace.BeginInvoke(New MethodInvoker(Sub()
                                                          ListBoxTrace.Items.Add(message)
                                                      End Sub))
        Else
            ListBoxTrace.Items.Add(message)
        End If
    End Sub
End Class
畫面輸出結果(ListBoxTrace)
背景流程:準備排入 UI 更新 UI 更新:狀態文字已排入 背景流程:排入後繼續往下執行
邏輯解析
  • BeginInvoke 的重點是排入,不是立即完成。
  • 背景流程不會等 UI 實際完成更新才繼續。
  • 若下一步一定要依賴 UI 更新結果,就不適合直接假設 BeginInvoke 已完成。

共用方法:集中處理 BeginInvoke

場景五:多個背景流程共用紀錄方法

若很多地方都要更新同一個控制項,建議把 InvokeRequiredBeginInvoke 包成共用方法。背景流程只呼叫方法,不重複撰寫跨執行緒判斷。

需要的主控項
  • ButtonStartLog:開始寫入紀錄。
  • ListBoxMessage:顯示紀錄。
範例程式碼
VB.NET / Windows Forms
Imports System.Threading

Public Class Form1
    Private Sub ButtonStartLog_Click(sender As Object, e As EventArgs) Handles ButtonStartLog.Click
        ListBoxMessage.Items.Clear()

        Dim worker As New Thread(AddressOf WriteMessages)
        worker.IsBackground = True
        worker.Start()
    End Sub

    Private Sub WriteMessages()
        Dim messages() As String = {
            "讀取設定完成",
            "建立暫存資料完成",
            "輸出結果完成"
        }

        For Each message As String In messages
            Thread.Sleep(300)
            AddMessage(message)
        Next
    End Sub

    Private Sub AddMessage(ByVal message As String)
        If ListBoxMessage.IsDisposed OrElse Not ListBoxMessage.IsHandleCreated Then
            Return
        End If

        If ListBoxMessage.InvokeRequired Then
            ListBoxMessage.BeginInvoke(New MethodInvoker(Sub()
                                                            ListBoxMessage.Items.Add(message)
                                                        End Sub))
        Else
            ListBoxMessage.Items.Add(message)
        End If
    End Sub
End Class
畫面輸出結果(ListBoxMessage)
讀取設定完成 建立暫存資料完成 輸出結果完成
邏輯解析
  • AddMessage 集中管理 UI 執行緒切換。
  • 背景流程只負責準備訊息與呼叫方法。
  • 共用方法可降低重複程式,也能統一加入表單關閉保護。

表單關閉與生命週期處理

場景六:背景監看停止後不再排入 UI

背景執行緒執行期間,表單可能已經關閉。若控制項控制代碼已不存在,仍繼續呼叫 BeginInvoke,可能發生例外。關閉表單時應停止背景流程,排入前也要檢查控制項狀態。

需要的主控項
  • ButtonStartWatch:開始背景監看。
  • LabelWatch:顯示監看狀態。
範例程式碼
VB.NET / Windows Forms
Imports System.Threading

Public Class Form1
    Private keepWatching As Boolean = False

    Private Sub ButtonStartWatch_Click(sender As Object, e As EventArgs) Handles ButtonStartWatch.Click
        If keepWatching Then
            Return
        End If

        keepWatching = True
        Dim worker As New Thread(AddressOf WatchStatus)
        worker.IsBackground = True
        worker.Start()
    End Sub

    Private Sub WatchStatus()
        Dim count As Integer = 0

        While keepWatching
            Thread.Sleep(500)
            count += 1
            QueueWatchText("監看次數:" & count.ToString())
        End While
    End Sub

    Private Sub QueueWatchText(ByVal message As String)
        If LabelWatch.IsDisposed OrElse Not LabelWatch.IsHandleCreated Then
            Return
        End If

        Try
            If LabelWatch.InvokeRequired Then
                LabelWatch.BeginInvoke(New MethodInvoker(Sub()
                                                            If Not LabelWatch.IsDisposed Then
                                                                LabelWatch.Text = message
                                                            End If
                                                        End Sub))
            Else
                LabelWatch.Text = message
            End If
        Catch ex As InvalidOperationException
            ' 表單關閉中,忽略排入失敗。
        End Try
    End Sub

    Private Sub Form1_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
        keepWatching = False
    End Sub
End Class
畫面輸出結果(LabelWatch.Text)
監看次數:1 監看次數:2 監看次數:3
邏輯解析
  • keepWatching 控制背景迴圈是否繼續。
  • 表單關閉時設定 keepWatching = False,讓背景流程停止。
  • Try...Catch 用來保護表單關閉過程中的排入失敗。

實務判斷與常見誤區

常見問題整理

  • 把 BeginInvoke 當成背景執行:BeginInvoke 的委派內容仍在 UI 執行緒執行。
  • 背景執行緒直接改控制項:應先使用 InvokeRequired 判斷,再切回 UI 執行緒。
  • 排入後立刻讀取 UI 結果:BeginInvoke 不等待完成,可能讀到舊狀態。
  • 大量高頻率排入:太多 UI 更新會讓訊息佇列擁塞。
  • 表單關閉仍繼續排入:控制項可能已釋放,需停止背景流程並檢查生命週期。
  • 把耗時計算放進 BeginInvoke:會讓 UI 執行緒變慢,甚至造成畫面卡住。
需求 建議做法 原因
背景工作通知 UI BeginInvoke 排入後背景流程可繼續。
需要等待 UI 更新完成 Invoke 同步等待委派完成。
大量進度更新 降低更新頻率。 避免 UI 訊息佇列過多。
多處更新同一控制項 建立共用方法。 集中跨執行緒判斷與生命週期保護。
表單可能關閉 停止背景流程並檢查控制項狀態。 避免釋放後仍排入 UI 更新。

重點整理

  1. BeginInvoke 用來把 UI 更新非同步排回控制項所屬的 UI 執行緒。
  2. BeginInvoke 不會等待委派執行完成,因此不能立刻依賴 UI 更新結果。
  3. 背景工作應在背景執行緒完成,BeginInvoke 區塊只放畫面更新。
  4. InvokeRequired 可判斷目前是否需要切回 UI 執行緒。
  5. BeginInvoke 適合進度回報、狀態通知與清單紀錄。
  6. Invoke 適合必須等待 UI 更新完成後才繼續的流程。
  7. 多處重複 UI 更新時,建議封裝成共用方法。
  8. 表單關閉時應停止背景工作,並避免控制項釋放後仍排入更新。