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

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

そんな非同期の処理について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行目~17行目でTextBoxへアクセスしようとしている部分をラムダ演算子(=>)を使ってDelegateにする。(Actionデリゲートというパラメータなし戻り値なしのメソッドにする)

そのDelegateをパラメータとして、Dispatcher.Invokeメソッドを呼び出している。

 

コントロールを所有しているスレッドのキュー管理(this.Dispatcher)に対して、このデリゲートを実行してねと依頼しているイメージ。