はじめに
前記事「【.NET】進捗ダイアログ画面で外部プロセスの進捗状況を表示する」では、取り込みバッチ処理を外部プロセス(コンソールアプリケーション)で実現しましたが、クラスライブラリー(DLL)にしたいという要望で再度サンプルを作ることになりました。
プログラム仕様
クラスライブラリー(DLL)は静的な参照とはせず動的な参照とします。進捗状況をイベントで受け取りプログレスバーに進捗状況を表示することにしました。
前回同様、バックグラウンド処理にはBackgroundWorkerコンポーネントを使用します。
※今回はBackgroundWorkerコンポーネントを使わなくでも出来ましたが、前記事に合わせました。
ソースコード
進捗ダイアログ画面
バックグラウンド処理にはBackgroundWorkerコンポーネントを使用しています。
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で試しました。下記の部分を変更することで対応出来ます。
Assembly assembly = Assembly.Load(baseName);
.NET Core 3.1以降の場合、AssemblyLoadContextクラスを使用します。今回はアンロードについては考慮していません。
AssemblyLoadContext
var myDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var assemblyPath = Path.Combine(myDirectory, ExecuteName);
Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
メイン画面
実行ボタンで進捗ダイアログ画面を表示する
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)で、進捗状況のイベントを発生させ進捗値を渡します。
処理内容はサンプルなので適当です。
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);
}
}
}
実行結果
成功
失敗
SubProccess70()の戻り値を true -> false に変更。
最後に
今回は動的にイベントハンドラーを追加するところと、EventArgsの値を取得する方法が分かるまで苦労してしまいました。
約10年前に複数のWindowsフォームの動的DLLを使ったシステムを構築したのを思い出しました、ユーザーのところに修正したモジュールの差し替えにいった同僚が遅く帰ってきました。
聞くところによるとユーザーの仕事終わりでメニュー画面を閉じるのを待っていたそうです。DLLファイルの差し替えなんてすぐだろうと思ってたのですが、動的DLLを呼び出しているメニュー画面を閉じないとエラーで差し替え出来ないのです。このあたりは反省した覚えがあります。
動的DLLの解放について - @IT掲示板
次回は、動的DLLを差し替えできるようにしてみたいと思います。
参照
- [C#][dynamic] 動的にイベント ハンドラーを追加
- 動的にDLLファイルのクラスメソッドを呼び出す (Reflectionを用いたアセンブリの動的呼び出し) (C#プログラミング)
- C# Reflection Programmatic Event Handlers with Custom Event Args - stackoverflow
- Subscribe to an event of a loaded assembly - stackoverflow
- Type.InvokeMemberを使ってメンバの呼び出しを行う
- C# のイベント作ってみた
- C# で DLL の 動的 呼び出し & 読み込み