バックグラウンドスレッドからでも、コントロールの設定を変更したい
スレッドセーフに扱えるようになるといいね。
目次
コントロールを操作したい場合は Invoke() を使うんダヨー
非同期処理内では、コントロールを操作しようとすると InvalidOperationException が発生します。どうしても操作したい場合は Invoke() 経由で操作します。これはもうそういうルールだからショーガナイとして覚えるしかないです。
Public Class Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Me.BackgroundWorker1.RunWorkerAsync() End Sub Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork ' できれば、ProgressChanged, RunWorkerCompleted イベント側で頑張ってほしいところ ' InvalidOperationException 'Me.Button1.Text = "button1" ' 当初の書き方 Dim method As SetTextDelegate = AddressOf SetText Me.Invoke(method, New Object() {Me.Button1, "hello"}) ' 最近の書き方 Dim method2 As Action(Of Control, String) = AddressOf SetText Me.Invoke(method2, New Object() {Me.Button1, "hello"}) ' ラムダ式が使える場合 Me.Invoke(Sub() Me.Button1.Text = "hello") End Sub Private Delegate Sub SetTextDelegate(ByVal ctrl As Control, ByVal value As String) Private Sub SetText(ByVal ctrl As Control, ByVal value As String) ctrl.Text = value End Sub End Class
なんでこんなにいろいろ書かないといけないのよ?
バックグラウンドスレッド上からでも、さらっとコントロールを扱いたいものです。というわけで、スレッドセーフな機能を作っていきましょう!答えは拡張メソッドです。
拡張メソッドの準備
ここでのポイントは、Control.IsHandleCreated プロパティと Control.InvokeRequired プロパティですが後で説明します。Control.Invoke() を使うことで、UI スレッド上で実行することができるようになります。あとはラムダ式とジェネリックデリゲートの組み合わせです。
Imports System.Runtime.CompilerServices Module ControlExtensions <Extension()> Public Sub SetText(ByVal self As Control, ByVal value As String) ' コントロールが破棄されたのであれば、そのコントロールにアクセスするのは危険なので、処理を止める If Not self.IsHandleCreated Then Exit Sub End If ' わかるならこっち self.Invoke(Sub() self.Text = value) ' よくわかんない場合はこっち ' InvokeRequired が True の場合(=別スレッドの中にいるので)、Invoke() してもらって、 ' もう一回 UI スレッドで SetText() を実行するようにお願いする ' UI スレッドにいる場合は、いつも通りにアクセスしても大丈夫 Dim ac As Action(Of Control, String) = AddressOf SetText If self.InvokeRequired Then self.Invoke(ac, New Object() {self, value}) Else self.Text = value End If End Sub End Module
使う側
完全に書き方同じというわけではないですが、見やすくなったと思います。
Public Class Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Me.BackgroundWorker1.RunWorkerAsync() End Sub Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork Me.Button1.SetText("hello") End Sub End Class
Control.InvokeRequired プロパティ
これは、現在の実行スレッドで Invoke() する必要があるかどうか(=つまりこのメソッドが別スレッドで動いているのかどうか)を判断してくれるプロパティです。めんどいから判定しないで常に Invoke() してもいいですが、今回みたいな場面では必要です。以下のようなことにならないように使っています。
Private Sub BackgroundWorker1_DoWork(sender As Object, e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork Me.Button1.Text = "hello" End Sub
実行結果
System.InvalidOperationException: '有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'Button1' がアクセスされました。'
Control.IsHandleCreated プロパティ
Is Handle Created(ハンドルが作成されているかどうか)を確認するプロパティです。詳しくは説明しませんが簡単に言うと、コントロールが準備完了しているかどうかを確認するためのプロパティです。例えば以下のサンプルでボタンを押して閉じた際、Disposed イベントが走るわけですが、破棄した後のコントロールを操作しようとしてしまい InvalidOperationException が発生しています。
Form1_Disposed イベントハンドラの処理内、冒頭で IsHandleCreated プロパティの確認をしていれば、例外エラーを防ぐことができます。
Public Class Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Dim frm As New Form1 frm.ShowDialog() frm.Dispose() End Sub Private Sub Form1_Disposed(sender As Object, e As EventArgs) Handles Me.Disposed Try System.Threading.Thread.Sleep(100) Button1.Invoke(Sub() Me.Button1.Text = "hello") Catch ex As Exception Console.WriteLine(ex.ToString()) End Try End Sub End Class
実行結果
System.InvalidOperationException: ウィンドウ ハンドルが作成される前、コントロールで Invoke または BeginInvoke を呼び出せません。