C#, UIコントロールをUIスレッド以外からアクセスする方法

Windowsフォームアプリでよく発生する例外の1つが、UIスレッド以外からUIコントロールにアクセスしようとして発生するInvalidOperationException例外。ここではその対処法について書く。

System.InvalidOperationException: ‘有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール ‘Form1’ がアクセスされました。

例えば、下記のコードを実行すると、例外が発生する。

using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            Task.Run(() =>
            {
                label1.Text = "Hello from another thread!";
            });
        }
    }
}
例外スクリーンショット

“System.InvalidOperationException: ‘有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール ‘Form1’ がアクセスされました。'”

メインスレッドとワーカースレッド

例外を発生させないためには、UIスレッド(メインスレッド)以外のスレッド(ワーカースレッド)からUIコントロールにアクセスさせない。今、どのスレッドが実行されているのかは、スレッドウィンドウに表示される。

UIスレッド以外で実行されている場合、Invokeを使ってUIスレッドに切り替えて実行させるようにする。ここでは、具体的な実装を書いてみる。

戻り値なし: Invoke(new Action<引数の型>(メソッド名), 引数名)

戻り値なしの場合はActionを使う。

例えば、上記の例であれば下記のように書き換えれば良い。

UIコントロールへのアクセス部分をメソッド化し、InvokeRequiredがtrueの場合(つまりUIスレッド以外で実行している場合)、Invokeを使った再帰呼び出しの結果、UIスレッドで実行させることが出来る。

return; を書き忘れると例外が発生するので注意。

using System;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            Task.Run(() =>
            {
                UpdateTextBox("Hello from another thread!");
            });
        }

        private void UpdateTextBox(string message)
        {
            if (label1.InvokeRequired)
            {
                label1.Invoke(new Action<string>( UpdateTextBox), message );
                return;
            }
            label1.Text = message;
        }
    }
}

戻り値あり: Invoke(new Func<引数の型,戻り値の型>(メソッド名)[, 引数名])

戻り値ありの場合はFuncを使う。Invokeの戻り値はobject型なので、returnする前に型を返したい型にキャストしてあげる必要がある。

例えば、下記のように書く。

using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp2
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            string message = "";
            Task.Run(() =>
            {
                message = GetLabelText();
            });
        }

        private string GetLabelText()
        {
            if (label1.InvokeRequired)
            {
                object text = label1.Invoke(new Func<string>(GetLabelText));
                return (string)text;
            }
            return label1.Text;
        }
    }
}