C#で別スレッドからコントロールを操作する (Dispatcher.Invoke)

時間のかかる処理をする場合、メインのスレッドで実行してしまうとボタンクリックなどの画面操作が出来なくなってしまいます。そんな時は処理を別スレッドで行いメインスレッドとは非同期で動作させるのが望ましいです。

そんな非同期の処理についてC#ではasync/awaitという便利な構文があります。

詳細はこちら↓

C#でasync/awaitを使った非同期処理

 

ですが、スレッドを分けた場合、他のスレッドで動作するコントロールを直接操作する事は出来ません。

処理の進捗具合をTextBoxに表示させる場合など、表示に利用したいコントロールが別スレッドである事を意識してコーディングする必要があります。 

ダメな例

以下はWPFをつかって処理の進捗をTextBoxへ表示しようとしている例。

<Window x:Class="WpfTest1.MainWindow" Title="MainWindow" Height="150" Width="250">
    <Grid Margin="10">
        <StackPanel>
            <TextBlock x:Name="CtrlText" Text="進捗:"/>
        </StackPanel>
    </Grid>
</Window>
    public partial class MainWindow : Window
    {
        // 時間のかかる処理をするメソッド
        private async void Test()
        {
            await Task.Run(() =>
            {
                for (int i = 1; i <= 100; ++i)
                {
                    // 
                    // ここで重たい処理をする
                    // 
                    // 
                    CtrlText.Text = "進捗:" + i.ToString() + "%";
                }
            });
        }


        public MainWindow()
        {
            InitializeComponent();
            Test();
        }
    }

6行目でawaitが定義され、Task化された8行目~15行目は非同期で実行されます。

14行目で進捗を表示するためにTextBoxのTextプロパティを書き換えようとしていますが・・・

 

実行してみると、14行目でSystem.InvalidOperationExceptionの例外が発生してしまいます。

「このオブジェクトは別のスレッドに所有されているため、呼び出しスレッドはこのオブジェクトにアクセスできません。」という内容

 

これは、awaitにより別スレッドで実行されている処理からはメインスレッドが所有するTextBoxコントロールにはアクセスできない為です。 

正しい例

    public partial class MainWindow : Window
    {
        // 時間のかかる処理をするメソッド
        private async void Test()
        {
            await Task.Run(() =>
            {
                for (int i = 1; i <= 100; ++i)
                {
                    // 
                    // ここで重たい処理をする
                    // 
                    // 
                    this.Dispatcher.Invoke((Action)(() =>
                    {
                        CtrlText.Text = "進捗:" + i.ToString() + "%";
                    }));
                }
            });
        }


        public MainWindow()
        {
            InitializeComponent();
            Test();
        }
    }

14行目でthis.Dispatcher.Invokeメソッドを呼び出しています。

Invokeメソッドへ渡す引数はDelegate(Actionというパラメータなし戻り値なしのメソッド)です。

14行目~17行目ではラムダ式(=>)を使って TextBoxのTextプロパティを書き換えるメソッドを作成しています。

 

Dispatcherキューを管理するクラスです。

this は MainWindow オブジェクト(つまりTextBox管理しているウィンドウ)ですね。

 

 

つまり、TextBoxを所有しているスレッドのキュー管理(this.Dispatcher)に対して、このデリゲートを実行してねっと依頼しているイメージになります。