2011年12月19日 星期一

使用 Delegate 解決 [跨執行緒作業無效] 的問題!

今天在寫程式時遇到錯誤訊息
System.InvalidOperationException: 跨執行緒作業無效: 存取控制項 'txtLog' 時所使用的執行緒與建立控制項的執行緒不同。

這個問題很明顯是在多執行緒的物件中,要在另一個執行緒物件裡做動作是會有問題的
我寫了一個小程式重現一下這個問題,
程式很簡單,畫面如下


會出現錯誤的程式碼如下:
Public Class Form3
    Dim _testTimer As Timers.Timer
    Dim _timerInterval As Integer = 3
    Dim _timerCount As Integer

    Private Sub btnStopTimer_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStopTimer.Click
        ''停止時間
        Me._testTimer.Stop()
    End Sub
    Private Sub btnStartTimer_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStartTimer.Click
        ''重新開始時間計數
        Me._testTimer.Start()
    End Sub
    Private Sub txtCmd_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles txtInterval.TextChanged
        ''重設時間常數
        Me._timerInterval = CInt(txtInterval.Text)
    End Sub

    Private Sub Form3_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        txtInterval.Text = Me._timerInterval.ToString

        Me._testTimer = New Timers.Timer
        Me._testTimer.Interval = Me._timerInterval * 1000
        AddHandler _testTimer.Elapsed, AddressOf Me.TimerOnElapsed
        Me._testTimer.Start()
        txtLog.Text = "Time is Start"
    End Sub

#Region "Handle Timer Event"
    Private Sub TimerOnElapsed(ByVal sender As Object, ByVal e As Timers.ElapsedEventArgs)
        Try
            Me._testTimer.Stop()
            _timerCount = _timerCount + 1
            ''錯誤發生在下面那行
            txtLog.AppendText(Me._timerCount.ToString & vbCrLf) 
        Catch ex As Exception
            MsgBox(ex.ToString)
        Finally
            Me._testTimer.Interval = Me._timerInterval * 1000
            Me._testTimer.Start()
        End Try
    End Sub
#End Region
End Class

因為我要 Handle Timer 的 Elapsed 事件,
所以將事件另外 Handle
    AddHandler _testTimer.Elapsed, AddressOf Me.TimerOnElapsed

但是要把計數值寫入 TextBox 中會出現"跨執行緒作業無效"的錯誤訊息,
所以多加入委派的 Sub 將寫入 Textbox 的事件重新導向
Public Class Form3
    Dim _testTimer As Timers.Timer
    Dim _timerInterval As Integer = 3
    Dim _timerCount As Integer

    ''委派加入 TextBox 字串的動作
    Private Delegate Sub AppendTextBoxCallback(ByVal txtBox As TextBox, ByVal strValue As String)

    Private Sub AppendTextBox(ByVal txtBox As TextBox, ByVal strValue As String)
        If Me.InvokeRequired Then
            Me.Invoke(New AppendTextBoxCallback(AddressOf Me.AppendTextBox), New Object() {txtBox, strValue})
        Else
            txtBox.AppendText(vbCrLf & strValue)
        End If
    End Sub

    Private Sub btnStopTimer_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStopTimer.Click
        ''停止時間
        Me._testTimer.Stop()
    End Sub
    Private Sub btnStartTimer_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnStartTimer.Click
        ''重新開始時間計數
        Me._testTimer.Start()
    End Sub
    Private Sub txtCmd_TextChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles txtInterval.TextChanged
        ''重設時間常數
        Me._timerInterval = CInt(txtInterval.Text)
    End Sub

    Private Sub Form3_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        txtInterval.Text = Me._timerInterval.ToString

        Me._testTimer = New Timers.Timer
        Me._testTimer.Interval = Me._timerInterval * 1000
        AddHandler _testTimer.Elapsed, AddressOf Me.TimerOnElapsed
        Me._testTimer.Start()
        txtLog.Text = "Time is Start"
    End Sub

#Region "Handle Timer Event"
    Private Sub TimerOnElapsed(ByVal sender As Object, ByVal e As Timers.ElapsedEventArgs)
        Try
            Me._testTimer.Stop()
            _timerCount = _timerCount + 1
            'txtLog.AppendText(Me._timerCount.ToString & vbCrLf)
            '交由委派的 Sub 去工作
            AppendTextBox(txtLog, Me._timerCount.ToString)
        Catch ex As Exception
            MsgBox(ex.ToString)
        Finally
            Me._testTimer.Interval = Me._timerInterval * 1000
            Me._testTimer.Start()
        End Try
    End Sub
#End Region
End Class

講了那麼多,主要就是要加入 Delegate 這行,
    Delegate Sub AppendTextBoxCallback(ByVal txtBox As TextBox, ByVal strValue As String)

並在要做的動作寫在委派的 Sub 中就可以了。
    Private Sub AppendTextBox(ByVal txtBox As TextBox, ByVal strValue As String)
        If Me.InvokeRequired Then
            Me.Invoke(New AppendTextBoxCallback(AddressOf Me.AppendTextBox), New Object() {txtBox, strValue})
        Else
            ''想要做的事情寫在這裡
        End If
    End Sub

沒有留言:

張貼留言