【.NET】進捗ダイアログ画面で外部プロセスの進捗状況を表示する

# はじめに
取り込みバッチ処理を外部プロセス(コンソールアプリケーション)で作成して、進捗状況を進捗ダイアログ画面のプログレスバーで表示するサンプルを作ることになりました。

# プログラム仕様
外部プロセスの進捗状況を確認するには、進捗状況を何かに出力してもらう必要があります。
コンソールアプリケーションなので標準出力に進捗状況を出力して、進捗ダイアログ画面で非同期で標準出力の内容を読み取りプログレスバーに進捗状況を表示することにしました。

# ソースコード
## 進捗ダイアログ画面
![frmDoWork.png](https://qiita-image-store.s3.amazonaws.com/0/14053/a1f5eab3-921f-f622-07ad-bb32080ae65b.png)

バックグラウンド処理にはBackgroundWorkerコンポーネントを使用しています。

```csharp:frmDoWork.cs
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Forms;
using System.Text.RegularExpressions;

namespace SampleMain
{
    public partial class frmDoWork : Form
    {
        // 取込処理名
        public string ExecuteName { get;  set; }
        // 取込処理の引数
        public string Arguments { get; set; }

        // プロセス処理用
        private Process _proc = null;

        // コンストラクタ
        public frmDoWork()
        {
            InitializeComponent();
        }

        // 画面ロード時
        private void frmDoWork_Load(object sender, EventArgs e)
        {
            // 取込処理の準備
            _proc = new Process();
            // プロセスの実行名をセット
            _proc.StartInfo.FileName = ExecuteName;
            // プロセスの引数をセット
            _proc.StartInfo.Arguments = Arguments;
            // ウィンドウを表示しないようにする
            _proc.StartInfo.CreateNoWindow = true;

            //入力できるようにする
            _proc.StartInfo.UseShellExecute = false;
            _proc.StartInfo.RedirectStandardInput = true;

            //非同期で出力を読み取れるようにする
            _proc.StartInfo.RedirectStandardOutput = true;
            _proc.OutputDataReceived += proc_OutputDataReceived;
         }

        // 画面初回表示時
        private void frmDoWork_Shown(object sender, EventArgs e)
        {
            // 閉じるボタンを無効にする
            btnClose.Enabled = false;

            // ProgressChangedイベントが発生するようにする
            bgWorker.WorkerReportsProgress = true;
            // 処理を開始する
            bgWorker.RunWorkerAsync();
        }

        // 画面を閉じる
        private void btnClose_Click(object sender, EventArgs e)
        {
            Close();
        }

        // 行が出力されるたびに呼び出される
        private void proc_OutputDataReceived(object sender, DataReceivedEventArgs e)
        {
            if (e.Data == null) return;

            Regex regex = new Regex(@"進捗 (\d.)%");
            Match match = regex.Match(e.Data);
            if(match.Groups.Count > 1)
            {
                int percentage = int.Parse(match.Groups[1].Value);

                // ProgressChangedイベントハンドラを呼び出し
                bgWorker.ReportProgress(percentage);
            }
        }

        // 取込処理
        private void bgWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker bgWorker = (BackgroundWorker)sender;

            // 処理を開始する
            _proc.Start();
            // 非同期で出力の読み取りを開始する
            _proc.BeginOutputReadLine();
            _proc.WaitForExit();

            // 結果を設定する
            e.Result = _proc.ExitCode;
        }

        // 途中経過イベント処理
        private void bgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            // ProgressBarの値を変更する
            prbDowork.Value = e.ProgressPercentage;
            // タイトルのテキストを変更する
            lblTitle.Text = (e.ProgressPercentage).ToString() + " %";
        }

        // 取込処理が終わったときに呼び出される
        private void bgWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                // エラーが発生したとき
                lblTitle.Text = "エラー:" + e.Error.Message;
            }
            else
            {
                // ProgressBarの結果を取得する
                int result = (int)e.Result;

                if(result == -1)
                {
                    // エラーで中断したとき
                    lblTitle.Text = "処理を中断しました。";
                }
                else
                {
                    // 正常に終了したとき
                    prbDowork.Value = prbDowork.Maximum;
                    lblTitle.Text = "完了しました。";
                }
            }

            // 閉じるボタンを有効に戻す
            btnClose.Enabled = true;
        }
    }
}
```

## メイン画面
実行ボタンで進捗ダイアログ画面を表示する

```csharp:frmMain.cs
private void btnExecute_Click(object sender, EventArgs e)
{
    // メッセージボックスを表示する
    DialogResult result = MessageBox.Show("実行します。よろしいですか ? ",
                                          "処理実行",
                                          MessageBoxButtons.YesNo,
                                          MessageBoxIcon.Question,
                                          MessageBoxDefaultButton.Button2);
    if (result == DialogResult.Yes)
    {
        // 処理実行
        frmDoWork frm = new frmDoWork();
        // 実行ファイル名 サンプルなので固定
        frm.ExecuteName = "SUB00001.exe";
        // 進捗画面表示
        frm.ShowDialog();
    }
}
```

## 取込バッチ処理
外部プロセス(コンソールアプリケーション)で、進捗状況をコンソールに出力します。
処理内容はサンプルなので適当です。

```csharp:Program.cs

using System;

namespace SUB00001
{
    class Program
    {
        static int Main(string[] args)
        {
            if (!SubProccess10()) return -1;
            if (!SubProccess30()) return -1;
            if (!SubProccess50()) return -1;
            if (!SubProccess70()) return -1;
            if (!SubProccess90()) return -1;

            // 完了
            SetProgress(100);

            return 0;
        }

        // 処理10%
        static bool SubProccess10()
        {
            System.Threading.Thread.Sleep(200);

            SetProgress(10);

            return true;
        }

        // 処理30%
        static bool SubProccess30()
        {
            System.Threading.Thread.Sleep(200);

            SetProgress(30);

            return true;
        }

        // 処理50%
        static bool SubProccess50()
        {
            System.Threading.Thread.Sleep(200);

            SetProgress(50);

            return true;
        }

        // 処理70%
        static bool SubProccess70()
        {
            System.Threading.Thread.Sleep(200);

            SetProgress(70);

            return true;
        }

        // 処理90%
        static bool SubProccess90()
        {
            System.Threading.Thread.Sleep(200);

            SetProgress(90);

            return true;
        }

        // 進捗状況を標準出力に出力する
        static void SetProgress(int value)
        {
            Console.WriteLine(string.Format("進捗 {0}%", value));
        }
    }
}
```

# 実行結果
## 成功
![import_progress.gif](https://qiita-image-store.s3.amazonaws.com/0/14053/28e8289f-ea93-ee66-1609-dd8569590c9f.gif)

## 失敗
SubProccess70()の戻り値を true -> false に変更。

![import_progress2.gif](https://qiita-image-store.s3.amazonaws.com/0/14053/88b59eb8-6125-21d6-5738-0bd4891bef10.gif)

# 最後に
最初に進捗状況のファイル出力して、ファイル監視(FileSystemWatcher)クラスの変更イベントでプログレスバーの更新しようとしたのですが、プロセス処理が終わってから変更イベントが動作するので、途中経過が表示できませんでした。
どうしたものかとネットで「c# 外部プログラム 非同期」で検索してみると、標準出力を非同期で内容を読み取れることが分かり、突破口は開けました。

プログレスバーの更新にBackgroundWorkerコンポーネントを使わないで、Invokeメソッドで対応しようとしたんですがなんか出来なくて諦め、今後キャンセルも考慮してそのままBackgroundWorkerコンポーネントを使うことにしました。

スレッド関連の理解がまだ足りてないようです。

# 参照
* [時間のかかる処理の進行状況を表示する](https://dobon.net/vb/dotnet/programing/displayprogress.html)
* [外部プロセスの標準出力を非同期で取得する](https://takachan.hatenablog.com/entry/2017/07/08/151756)
* [DOSコマンドを実行し出力データを取得する](https://dobon.net/vb/dotnet/process/standardoutput.html)
* [非同期で標準出力(コンソールアプリケーション)の内容をプログラムで受け取る (C#プログラミング)](https://www.ipentec.com/document/csharp-get-standard-output-async)
* [コンソール・アプリケーションで進行状況を表示するには？](http://www.atmarkit.co.jp/fdotnet/dotnettips/576consoleprogress/consoleprogress.html)