Backgroundworkerの使い方

最近プログラミングのネタをほとんど投稿しない、何故か?
それは最近の職務がプログラミングだから。
教師生活25年、ついについに、プロのプログラマーに・・・小学生の頃からいわゆるバイナリーでプログラムをベタ打ちしていたことを考えると大変感慨深い。
あの頃にプログラムや変数がとどのつまりは実行メモリー上にある何かだと学んだことは、現在の仕事の礎になっている。


そんな感傷はさておき、久々の趣味グラマー。
大量のファイルの処理などの重い処理をUIスレッドでやらせると、UIが凍る、アプリが反応しなくなる。
これを防ぐために、UIアプリでは処理スレッドをマルチスレッド化して、UIスレッドの処理から分離をする。
C#にはTaskというクラスがあり、このブログでも一度取り上げている。
C++のスレッドに比べれば極めて簡単にマルチスレッド化できる。
今回取り上げるBackgroundworkerはUIスレッドに特化したもので、例えば処理中に"現在処理中です、X%処理済み"のような表示をさせるものだ。
MSFTからも懇切丁寧なドキュメントが公開されている。
learn.microsoft.com
いつも通り、何でも作ってやってみよう。


まずFormを作る、環境はVisual Studio 2022、.NET8を使用する。

プロジェクトテンプレートからWindows Form Applicationを選択する。
frameworkは.net framework 2.0以上なら良いのだが、今回は最新の.NET 8を使用する。
プロジェクト名は"TestBackgroundWorker"。
テンプレートが出来上がったらスケルトンビルド(何も変更せずにビルド)してdebugモードで実行しておくのはお約束。

次にFormに必要な部品を組み込む、今回は

  • Button x 2
  • Label
  • Backgroundworker

の4つだ、ツールボックスからFormに放り込み、適当に配置する。

BackgroundworkerはTimer同様に欄外に配置される。

お次に各部品のイベントハンドラーを起こす。
各部品を選択して、プロパティウィンドウから次のイベントハンドラーを作る。

  • Button: Click
  • Backgroundworker: DoWork、ProgressChanged、RunWorkerCompleted

適当なメソッド名をつければ、Formクラスにイベントハンドラーがテンプレートされる。
最近のVisual Studioはこの辺りを秘匿しなくなったので、Form.Designer.csを見ればどのようになっているか一目瞭然だ。
筆者のはこんな感じ。

        private void InitializeComponent()
        {
            buttonStart = new Button();
            buttonCancel = new Button();
            labelProgress = new Label();
            backgroundWorkerTest = new System.ComponentModel.BackgroundWorker();
            SuspendLayout();
            // 
            // buttonStart
            // 
            buttonStart.Location = new Point(112, 219);
            buttonStart.Name = "buttonStart";
            buttonStart.Size = new Size(112, 34);
            buttonStart.TabIndex = 0;
            buttonStart.Text = "Start";
            buttonStart.UseVisualStyleBackColor = true;
            buttonStart.Click += OnBtnStartClick;
            // 
            // buttonCancel
            // 
            buttonCancel.Location = new Point(350, 219);
            buttonCancel.Name = "buttonCancel";
            buttonCancel.Size = new Size(112, 34);
            buttonCancel.TabIndex = 1;
            buttonCancel.Text = "Cancel";
            buttonCancel.UseVisualStyleBackColor = true;
            buttonCancel.Click += OnBtlCancelClick;
            // 
            // labelProgress
            // 
            labelProgress.AutoSize = true;
            labelProgress.Location = new Point(86, 43);
            labelProgress.Name = "labelProgress";
            labelProgress.Size = new Size(59, 25);
            labelProgress.TabIndex = 2;
            labelProgress.Text = "label1";
            // 
            // backgroundWorkerTest
            // 
            backgroundWorkerTest.WorkerReportsProgress = true;
            backgroundWorkerTest.WorkerSupportsCancellation = true;
            backgroundWorkerTest.DoWork += bgWkrTestDoWork;
            backgroundWorkerTest.ProgressChanged += bgWkrTestProgressChanged;
            backgroundWorkerTest.RunWorkerCompleted += bgWkrTestRunWorkerCompleted;
            // 
            // Form1
            // 
            AutoScaleDimensions = new SizeF(10F, 25F);
            AutoScaleMode = AutoScaleMode.Font;
            ClientSize = new Size(800, 450);
            Controls.Add(labelProgress);
            Controls.Add(buttonCancel);
            Controls.Add(buttonStart);
            Name = "Form1";
            Text = "Backgroundworker Test";
            ResumeLayout(false);
            PerformLayout();
        }

最後にイベントハンドラーをしこしこ実装する。
前述の通りイベントハンドラーはForm.csに全てテンプレートされているので、以下のように実装する。

    public partial class Form1 : Form
    {
        static int DummyLoadTimeInMsec = 500;
        static int AmountOfWork = 20;
        public Form1()
        {
            InitializeComponent();
            labelProgress.Text = $"Not Started";
        }

        private void bgWkrTestDoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
        {
            BackgroundWorker worker = sender as BackgroundWorker;

            for (int countWork = 0; countWork < AmountOfWork; countWork++)
            {
                if (worker.CancellationPending == true)
                {
                    e.Cancel = true;
                    break;
                }
                else
                {
                    System.Threading.Thread.Sleep(DummyLoadTimeInMsec);
                    int progressPercentage = countWork * 100 / AmountOfWork;
                    worker.ReportProgress(progressPercentage);
                }
            }
        }

        private void bgWkrTestProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
        {
            labelProgress.Text = $"Progress: {e.ProgressPercentage.ToString()}% completed";
        }

        private void bgWkrTestRunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
        {
            if (e.Cancelled == true)
            {
                labelProgress.Text = $"Canceled";
            }
            else if (e.Error != null)
            {
                labelProgress.Text = $"Error: {e.Error.Message}";
            }
            else
            {
                labelProgress.Text = $"Completed";
            }
        }

        private void OnBtnStartClick(object sender, EventArgs e)
        {
            if (backgroundWorkerTest.IsBusy != true)
            {
                backgroundWorkerTest.RunWorkerAsync();
            }
        }

        private void OnBtlCancelClick(object sender, EventArgs e)
        {
            if (backgroundWorkerTest.WorkerSupportsCancellation == true)
            {
                // Cancel the asynchronous operation.
                backgroundWorkerTest.CancelAsync();
            }
        }
    }

「1文字変数名は早漏・短小のやること」「ハードコードは悪」だ。
最近はString.Formatも使わず、文字列補間式($"{}")を使っている。
これを使うと文字列書式に変数名やメソッドを直接ぶっこめるので、より直感的にコーディングができる。
ところでこの実装の肝は

  • BackgroundworkerオブジェクトのRunWorkerAsyncを実行すると、DoWorkのイベントハンドラーであるbgWkrTestDoWorkが実行される
  • DoWorkの中で、worker.ReportProgressに処理の進行状況を%で渡すと、ProgressChangedのイベントハンドラーであるbgWkrTestProgressChangedが呼ばれてUIが更新される

といったところだ。

こいつをビルド、実行してStartボタンを押せば、UIスレッドは止まらずにバックグラウンドで重い処理(ここでは500msecのスリープを20回実行)することができて、その進捗がUIスレッドに反映される。
止めたいときにはCancelボタンを押せばよろしい。


さて、何でもUIスレッドに実装するのは阿呆のやること。
DVAのように、処理とUI系は分離するのがよい。
どうしたらよいか?

まず以下のような処理クラスを作る。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;

namespace TestBackgroundWorker
{
    internal class DemandingWork
    {
        static int DummyLoadTimeInMsec = 500;
        static int AmountOfWork = 20;
        public DemandingWork() 
        { }
        public void DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
        {
            BackgroundWorker worker = sender as BackgroundWorker;

            for (int countWork = 0; countWork < AmountOfWork; countWork++)
            {
                if (worker.CancellationPending == true)
                {
                    e.Cancel = true;
                    break;
                }
                else
                {
                    // Perform a time consuming operation and report progress.
                    System.Threading.Thread.Sleep(DummyLoadTimeInMsec);
                    int progressPercentage = countWork * 100 / AmountOfWork;
                    worker.ReportProgress(progressPercentage);
                }
            }
        }
    }
}

次にこのクラスのインスタンスをFormのメンバーにする。

        private DemandingWork demandingWork;

FormのInitializeComponentで実体化する。

            demandingWork = new DemandingWork();

BackgroundworkerのDoWorkのイベントハンドラをDemandingWork.DoWorkにする。

            backgroundWorkerTest.DoWork += delegate (object sender, DoWorkEventArgs e)
            {
                demandingWork.DoWork(sender, e);
            };

一時delegateを作っているが、λ式で一発で行ってもよい(筆者はλ式が苦手な年寄、ぐおふぉんぐおふぉん)

こりだけ、詳細はこちらから。
github.com