【.NET】進捗ダイアログ画面で動的DLLの進捗状況を表示する

# はじめに
前記事「[【.NET】進捗ダイアログ画面で外部プロセスの進捗状況を表示する](https://qiita.com/yaju/items/c77762f38678961f77a1)」では、取り込みバッチ処理を外部プロセス(コンソールアプリケーション)で実現しましたが、クラスライブラリー(DLL)にしたいという要望で再度サンプルを作ることになりました。

# プログラム仕様
クラスライブラリー(DLL)は静的な参照とはせず動的な参照とします。進捗状況をイベントで受け取りプログレスバーに進捗状況を表示することにしました。
前回同様、バックグラウンド処理にはBackgroundWorkerコンポーネントを使用します。

※今回はBackgroundWorkerコンポーネントを使わなくでも出来ましたが、前記事に合わせました。

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

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

```csharp:frmDoWork
using System;
using System.ComponentModel;
using System.Windows.Forms;
using System.Reflection;
using System.IO;

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

        // 動的DLL処理用
        private Type _myType = null;
        private Object _instance = null;

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

        // 画面ロード時
        private void frmDoWork_Load(object sender, EventArgs e)
        {
            // アセンブリ名を使ってクラス ライブラリーを動的に読み込み
            string baseName = Path.GetFileNameWithoutExtension(ExecuteName);
            Assembly assembly = Assembly.Load(baseName);
             _myType = assembly.GetType(baseName + ".Program");
            _instance = Activator.CreateInstance(_myType);

            // アセンブリ内のクラスの Update イベントの EventInfo を取得
            EventInfo eventInfo = _myType.GetEvent("Update");
            var methodInfo = this.GetType().GetMethod("OnUpdate");

            Delegate handler = Delegate.CreateDelegate(eventInfo.EventHandlerType, this, methodInfo);

            // EventInfo に対してイベント ハンドラーを追加
            eventInfo.AddEventHandler(_instance, handler);
         }

        // 画面初回表示時
        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 bgWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker bgWorker = (BackgroundWorker)sender;

            // 処理を開始する
            int result = (int)_myType.InvokeMember("Main", BindingFlags.InvokeMethod, null, _instance, new object[] { Arguments });

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

        // 途中経過イベント処理
        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;
        }


        // 進捗値の更新
        public void OnUpdate(object sender, EventArgs e)
        {
            PropertyInfo prop_value = e.GetType().GetProperty("value");
            int percentage = Convert.ToInt32(prop_value.GetValue(e, null));

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

【2020/05/07追記】
.NET Core 3.1で試しました。下記の部分を変更することで対応出来ます。

```csharp
Assembly assembly = Assembly.Load(baseName);
```

.NET Core 3.1以降の場合、AssemblyLoadContextクラスを使用します。今回はアンロードについては考慮していません。
[AssemblyLoadContext](https://qiita.com/ikihiki/items/f80839db7763406bb000)

```csharp
var myDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var assemblyPath = Path.Combine(myDirectory, ExecuteName);
Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
```

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

```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.dll";
        // 進捗画面表示
        frm.ShowDialog();
    }
}
```
## 取込バッチ処理
クラスライブラリー(DLL)で、進捗状況のイベントを発生させ進捗値を渡します。
処理内容はサンプルなので適当です。

```csharp:Program.cs
using System;

namespace SUB00001
{
    public class UpdateEventArgs : EventArgs
    {
        public int value { get; set; }
    }

    class Program
    {
        // 更新されると起きるイベント
        public event EventHandler Update;

        public 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%
        private bool SubProccess10()
        {
            System.Threading.Thread.Sleep(200);

            SetProgress(10);

            return true;
        }

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

            SetProgress(30);

            return true;
        }

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

            SetProgress(50);

            return true;
        }

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

            SetProgress(70);

            return true;
        }

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

            SetProgress(90);

            return true;
        }

        // 進捗状況を標準出力に出力する
        private void SetProgress(int value)
        {
            // 更新イベントを起こす
            UpdateEventArgs e = new UpdateEventArgs();
            e.value = value;

            Update?.Invoke(this, e);
        }
    }
}
```

# 実行結果
## 成功
![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)

# 最後に
今回は動的にイベントハンドラーを追加するところと、EventArgsの値を取得する方法が分かるまで苦労してしまいました。

約10年前に複数のWindowsフォームの動的DLLを使ったシステムを構築したのを思い出しました、ユーザーのところに修正したモジュールの差し替えにいった同僚が遅く帰ってきました。
聞くところによるとユーザーの仕事終わりでメニュー画面を閉じるのを待っていたそうです。DLLファイルの差し替えなんてすぐだろうと思ってたのですが、動的DLLを呼び出しているメニュー画面を閉じないとエラーで差し替え出来ないのです。このあたりは反省した覚えがあります。
[動的ＤＬＬの解放について - @IT掲示板](https://qa.atmarkit.co.jp/q/1552)

次回は、動的DLLを差し替えできるようにしてみたいと思います。

# 参照
* [[C#][dynamic] 動的にイベント ハンドラーを追加](http://blog.shos.info/archives/2013/06/csharp_addeventhandler.html)
* [動的にDLLファイルのクラスメソッドを呼び出す (Reflectionを用いたアセンブリの動的呼び出し) (C#プログラミング)](https://www.ipentec.com/document/csharp-dynamic-load-assembly-use-reflection)
* [C# Reflection Programmatic Event Handlers with Custom Event Args - stackoverflow](https://stackoverflow.com/questions/4723319/c-sharp-reflection-programmatic-event-handlers-with-custom-event-args)
* [Subscribe to an event of a loaded assembly - stackoverflow](https://stackoverflow.com/questions/16023129/subscribe-to-an-event-of-a-loaded-assembly)
* [Type.InvokeMemberを使ってメンバの呼び出しを行う](https://smdn.jp/programming/dotnet-samplecodes/reflection/Type_InvokeMember%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%A1%E3%83%B3%E3%83%90%E3%81%AE%E5%91%BC%E3%81%B3%E5%87%BA%E3%81%97%E3%82%92%E8%A1%8C%E3%81%86.csharp.xhtml)
* [C# のイベント作ってみた](http://var.blog.jp/archives/70431864.html)
* [C# で DLL の 動的 呼び出し & 読み込み](https://garafu.blogspot.com/2014/01/c-dll.html)