このページの最終更新日 2020/10/25

BackgroundWorkerを使用して中断処理を実装する

1. BackgroundWorkerを使ってみる

 非同期処理を実装する方法としては「BackgroundWorkder」や「Task」クラスを使う方法が考えられます。このページでは「BackgroundWorker」について解説します。
 BackgroundWorkerを使用したコードcode.1に示します。基本的な構成はTaskの場合と変わっていませんが、8行目~12行目でBackgroundWorkerに対する設定を色々行っている点が新たなところです。31行目で直接プログレスバーの値を変更出来ない理由はTaskで「有効ではないスレッド 間の操作」のエラーが出る事情と同じです。_workder.ReportProgress(iProg); のコードが呼び出されると、40~43行目のShowWorkProgressメソッドが呼び出されるようになります。

- code.1 -
public partial class FormMain : Form
{
    private BackgroundWorker _workder;

    public FormMain()
    {
        InitializeComponent();
	_workder = new BackgroundWorker();
	_workder.WorkerSupportsCancellation = true; //キャンセル処理を行う
	_workder.WorkerReportsProgress = true;   //進捗状況の更新を行う
	_workder.DoWork += new DoWorkEventHandler(ExecuteWork); //RunWorkerAsync()が呼び出されるとExecuteWork()を実行
	_workder.ProgressChanged += new ProgressChangedEventHandler(ShowWorkProgress); //ReportProgress()が呼び出されるとShowWorkProgress()を実行

        this.btnExe.Click += new EventHandler(BtnExeClick);
        this.btnCancel.Click += new EventHandler(BtnCancelClick);
    }

    private void BtnExeClick(object sender, EventArgs e)
    {
        _workder.RunWorkerAsync();
    }

    private void  ExecuteWork(object sender, DoWorkEventArgs e)
    {
        try
        {
            for (int iProg = 1; iProg <= 100; iProg++)
            {
                Task.Delay(100).Wait();
                if (_workder.CancellationPending) { throw new Exception("ユーザー操作により中止されました。"); }
		_workder.ReportProgress(iProg);
            } 
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
    
    private void ShowWorkProgress(object sender, ProgressChangedEventArgs e)
    {
    this.pbWork.Value = e.ProgressPercentage;
    }
    
    private void BtnCancelClick(object sender, EventArgs e)
    {
        _workder.CancelAsync();
    }
}

 まずはここまでのコードを自分で書いて、- pic.2 - と同様に実行されることを確認して頂きたく。さて、ここからはもう少しBackgroundWorkerについて丁寧にアプリを作っていきたいと思います。例えば、今の例では「実行」ボタンと「停止」ボタンが常にクリック可能な状態となっていますが、「停止」ボタンは実行中でないのならクリック不可能な状態にしておくべきですし、逆に実行中に再度実行ボタンを押される状態になっているのも問題有りなので「実行」ボタンは実行中には操作不能にすべきです。
 また、BackgroundWorkerに何かしら変数を渡したいときはどのようにすれば良いでしょうか。この辺りを次の章から解説していきます。



2. BackgroundWorkerでボタン操作の可/不可の切り替え

 まずは、「実行」ボタンと「中止」ボタンの操作の可/不可についてプログラムを作っていきましょう。フォームを起動したときには、「中止」ボタンは使用不可にしておきたいので、予めIDE上でbtnCancelコントロールのプロパティenableの値をfalseにしておきます。
 先ほどのコードから新たに書き加えるべき箇所を赤字で示しました。1つ目にやるべきことは、BackgroundWorkerが開始される直前で「実行」ボタンは操作不可、「中止」ボタンは操作可能状態にする必要があります。57~61行目でボタンの操作可/不可を切り替えるメソッドを用意し、BackgroundWorkerが開始される直前の21行目でこのメソッドを呼び出しています。
 2つ目にやるべきことはBackgroundWorkerが終了した時点で、「実行」ボタンは操作可能、「中止」ボタンは操作不能にすることです。52~55行目でBackgroundWorkerが終了したときに呼び出されるメソッドを定義しておき、13行目でこのメソッドを呼び出す設定をしています。

- code.2 -
public partial class FormMain : Form
{
    private BackgroundWorker _workder;

    public FormMain()
    {
        InitializeComponent();
        _workder = new BackgroundWorker();
        _workder.WorkerSupportsCancellation = true; //キャンセル処理を行う
        _workder.WorkerReportsProgress = true;   //進捗状況の更新を行う
        _workder.DoWork += new DoWorkEventHandler(ExecuteWork); //RunWorkerAsync()が呼び出されるとExecuteWork()を実行
        _workder.ProgressChanged += new ProgressChangedEventHandler(ShowWorkProgress); //ReportProgress()が呼び出されるとShowWorkProgress()を実行
	_workder.RunWorkerCompleted += new RunWorkerCompletedEventHandler(ExecuteWhenWorkerCompleted);//_workerが終了するとExecuteWhenWorkCompleted()を実行

        this.btnExe.Click += new EventHandler(BtnExeClick);
        this.btnCancel.Click += new EventHandler(BtnCancelClick);
    }

    private void BtnExeClick(object sender, EventArgs e)
    {
	DoEnableButton(true);
        _workder.RunWorkerAsync();
    }

    private void  ExecuteWork(object sender, DoWorkEventArgs e)
    {
        try
        {
            for (int iProg = 1; iProg <= 100; iProg++)
            {
                Task.Delay(100).Wait();
                if (_workder.CancellationPending) { throw new Exception("ユーザー操作により中止されました。"); }
                _workder.ReportProgress(iProg);
            } 
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }

    private void BtnCancelClick(object sender, EventArgs e)
    {
        _workder.CancelAsync();
    }

    private void ShowWorkProgress(object sender, ProgressChangedEventArgs e)
    {
        this.pbWork.Value = e.ProgressPercentage;
    }

	private void ExecuteWhenWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        DoEnableButton(false);
    }

    private void DoEnableButton(bool isRunning)
    {
        this.btnExe.Enabled = !isRunning;
        this.btnCancel.Enabled = isRunning;
    }
}

このコードを実行してフォームアプリケーションを起動し、「実行」ボタンをクリックすると次のように「実行」ボタンが操作不可、「中止」ボタンが操作可能になります。

「実行」ボタンは操作不可、「中止」ボタンは操作可

- pic.1 -

ここで、プログレスバーの値が最後の100まで更新されるか、もしくは「中止」ボタンをクリックするかのどちらかが行われてBackgroundWorkerの処理が終了した場合は、pic.5のようになります。また、pic.5の状態で再度「実行」ボタンをクリックすると、プログレスバーの値が再度1から更新されるようになります。

「実行」ボタンは操作可、「中止」ボタンは操作不可

- pic.2 -

3. BackgroundWorkerへ引数を渡す

 まずBackgroundWorkerオブジェクトのRunWorkerAsyncメソッドの中に渡したいオブジェクトを引数として指定します。 このように指定された変数は、DoWorkEventArgsオブジェクトの Argumentプロパティから取り出すことが出来ます。 渡したい変数が複数ある場合は、以下の22・23行目のようにTupleを使うか、あるいはクラスを使って複数の変数を1つのオブジェクトの形にして渡し、30~32行目のように受け取った値をキャストして複数の変数を取り出すことが出来ます。
 なお、渡したい変数はクラスレベルのプロパティにしてしまうことも可能ではありますが、なるべく変数のスコープは狭くするべきであるという観点からあまりお勧め出来ない方法です。

- code.3 -
public partial class FormMain : Form
{
    private BackgroundWorker _workder;

    public FormMain()
    {
        InitializeComponent();
        _workder = new BackgroundWorker();
        _workder.WorkerSupportsCancellation = true; //キャンセル処理を行う
        _workder.WorkerReportsProgress = true;   //進捗状況の更新を行う
        _workder.DoWork += new DoWorkEventHandler(ExecuteWork); //RunWorkerAsync()が呼び出されるとExecuteWork()を実行
        _workder.ProgressChanged += new ProgressChangedEventHandler(ShowWorkProgress); //ReportProgress()が呼び出されるとShowWorkProgress()を実行
        _workder.RunWorkerCompleted += new RunWorkerCompletedEventHandler(ExecuteWhenWorkerCompleted); //_workerが終了するとExecuteWhenWorkCompleted()を実行

        this.btnExe.Click += new EventHandler(BtnExeClick);
        this.btnCancel.Click += new EventHandler(BtnCancelClick);
    }

    private void BtnExeClick(object sender, EventArgs e)
    {
        DoEnableButton(true);
	Tuple<DateTime, string> argsToWorker = Tuple.Create(new DateTime(2018,6,25), "コムスコシステムズ");
        _workder.RunWorkerAsync(argsToWorker);</highlight>
    }

    private void  ExecuteWork(object sender, DoWorkEventArgs e)
    {
        try
        {
	    var argsFromMain = (Tuple<DateTime, string>) e.Argument;
            System.Diagnostics.Debug.WriteLine(argsFromMain.Item1.ToString("yyyy年MM月dd日"));
            System.Diagnostics.Debug.WriteLine(argsFromMain.Item2);</highlight>

            for (int iProg = 1; iProg <= 100; iProg++)
            {
                Task.Delay(100).Wait();
                if (_workder.CancellationPending) { throw new Exception("ユーザー操作により中止されました。"); }
                _workder.ReportProgress(iProg);
            } 
        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }

    private void BtnCancelClick(object sender, EventArgs e)
    {
        _workder.CancelAsync();
    }

    private void ShowWorkProgress(object sender, ProgressChangedEventArgs e)
    {
        this.pbWork.Value = e.ProgressPercentage;
    }

    private void ExecuteWhenWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        DoEnableButton(false);
    }

    private void DoEnableButton(bool isRunning)
    {
        this.btnExe.Enabled = !isRunning;
        this.btnCancel.Enabled = isRunning;
    }
}

プログラムを実行しIDEのデバッグログにpic.6のようにメッセージが表示されていればOKです。

BackgroundWorkerに渡された引数の中身をIDEで表示する

- pic.3 -