はじめに
.NET Core 3.0以降から、Windows Formがプレビュー版で対応しましたので、下記2つを.NET Core版に移植しました。
① 【.NET】進捗ダイアログ画面で動的DLLの進捗状況を表示する
② 【.NET】進捗ダイアログ画面で動的DLL(差し替え可能)の進捗状況を表示する
①については、変更箇所が少なかったため(Assembly.Load→AssemblyLoadContextに変更)、記事に追加する形で修正しました。
②については、変更箇所がそれなりにあったので今回記事にしました。
.NET Coreにてメイン画面(EXE)を閉じないでもクラスライブラリー(動的DLL)を差し替え可能なことを実現します。
画像は前回の記事の使いまわしです。.NET Framework版と.NET Core版で実現することは変わらないため、ただ日付だけ古いですがご了承ください。
環境
- Windows 10 Home
- Visual Studo 16.7.0 Preview 1.0
- .NET Core 3.1
AppDimainから変更
.NET Framework版では、新規のAppDimainという単一プロセス内で分離された複数の実行領域(domain)を使用して実現させていました。
.NET Core ではAppDimainが1プロセスで1つしかサポートされていないため、AppDimainが使えません。
その代わりAssemblyLoadContextという新しいクラスが用意されているので、これを使用して実現します。
System.Runtime.Loader.AssemblyLoadContext について
AssemblyLoadContextクラスを使用しても、考え方としてはAppDimainの時と変わりません。
進捗ダイアログ画面から取込バッチ処理のクラスライブラリー(DLL)を動的にロードし、処理終了後にアンロードした上で取込バッチ処理(動的DLL)を差し替えようとすると下記のエラーが発生します。
これは、.NETのメタデータ(≒ 型情報)などのアセンブリが進捗ダイアログ画面側に残った状態になってしまうからです。
対応方法
AppDimainの時と同様に進捗ダイアログ画面をクラスライブラリー(DLL)に分離し、メイン画面からAssemblyLoadContextインスタンスを作成して進捗ダイアログ画面(DLL)をロードして実行します。
また、取込バッチ処理(動的DLL)も同じAssemblyLoadContextインスタンスにロードされて実行されます。
進捗ダイアログ画面を閉じると、AssemblyLoadContextインスタンスがアンロードされます。
ソースコード
進捗ダイアログ画面
バックグラウンド処理にはBackgroundWorkerコンポーネントを使用しています。
別のスレッドからForm上のコントロールにアクセスしたことにより「有効ではないスレッド間の操作」の例外エラーとなったため、コントロールに対してはInvoke((Action)delegate (){}) で囲んでいます。
前回との違いとして、プロパティにAssemblyLoadContext用のコンテキストを追加しています。
using System;
using System.ComponentModel;
using System.Windows.Forms;
using System.Reflection;
using System.IO;
namespace DoWork
{
public partial class frmDoWork : Form
{
// 取込処理名
public string ExecuteName { get; set; }
// 取込処理の引数
public string Arguments { get; set; }
// コンテキスト
public AssemblyLoadContext Context { get; set; }
private Type _myType = null;
private Object _instance = null;
// コンストラクタ
public frmDoWork()
{
InitializeComponent();
}
// 画面初回表示時
private void frmDoWork_Shown(object sender, EventArgs e)
{
// アセンブリ名を使ってクラス ライブラリーを動的に読み込み
string baseName = Path.GetFileNameWithoutExtension(ExecuteName);
var myDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var assemblyPath = Path.Combine(myDirectory, ExecuteName);
Assembly assembly = Context.LoadFromAssemblyPath(assemblyPath);
_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);
// 閉じるボタンを無効にする
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.Split(',') });
// 結果を設定する
e.Result = result;
}
// 途中経過イベント処理
private void bgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
// ProgressBarの値を変更する
Invoke((Action)delegate ()
{
prbDowork.Value = e.ProgressPercentage;
// タイトルのテキストを変更する
lblTitle.Text = (e.ProgressPercentage).ToString() + " %";
});
}
// 取込処理が終わったときに呼び出される
private void bgWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Invoke((Action)delegate ()
{
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, ProgressChangedEventArgs e)
{
// ProgressChangedイベントハンドラを呼び出し
bgWorker.ReportProgress(e.ProgressPercentage);
}
// 実行処理
public void Execute(string executeName, string args)
{
this.ExecuteName = executeName;
this.Arguments = args;
this.ShowDialog();
}
}
}
メイン画面
実行ボタンで進捗ダイアログ画面を表示する。
.NET Core でアセンブリのアンローダビリティを使用およびデバッグする方法
アセンブリをロードするときに、そのファイルが見つからない場合、FileNotFoundException例外(System.IO名前空間)が発生してしまう。
この問題を避けるために、JITコンパイラが動くのを、アセンブリがダウンロードされた後まで遅延させなければならない。そのためにJIT 最適化によるインライン展開を抑止する方法として「[MethodImpl(MethodImplOptions.NoInlining)]」をメソッドの上に付加している。
private void btnExecute_Click(object sender, EventArgs e)
{
// メッセージボックスを表示する
DialogResult result = MessageBox.Show("実行します。よろしいですか ? ",
"処理実行",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button2);
if (result == DialogResult.Yes)
{
// 処理実行
string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
string assemblyPath = Path.Combine(path, "DoWork.dll");
ExecuteAndUnload(assemblyPath, out WeakReference alcWeakRef);
// アンロードされるまで待つ
int counter = 0;
for (counter = 0; alcWeakRef.IsAlive && (counter < 10); counter++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
if (counter < 10)
{
System.Diagnostics.Debug.WriteLine("アンロード成功");
}
else
{
System.Diagnostics.Debug.WriteLine("アンロード失敗");
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void ExecuteAndUnload(string assemblyPath, out WeakReference alcWeakRef)
{
// アセンブリをロードするAssemblyLoadContextを作成
var alc = new MyAssemblyLoadContext();
// アセンブリをロード
Assembly a = alc.LoadFromAssemblyPath(assemblyPath);
// 外からアンロードを検知するために弱参照を設定
alcWeakRef = new WeakReference(alc, trackResurrection: true);
// リフレクションで関数コール
var type = a.GetType("DoWork.frmDoWork");
var instance = Activator.CreateInstance(type);
var method = type.GetMethod("Execute");
method.Invoke(instance, new object[] { "SUB00001", "", alc});
// アンロード実施
alc.Unload();
}
}
AssemblyLoadContext のカスタム
frmMain.cs内に追加しています。
単純にアセンブリをロードしたいだけなら、AssemblyLoadContext.Default を使用すればいいのだが、下記の理由で Default を使うとアンロードができなくなるためカスタムする必要がある。
AssemblyLoadContextのUnloadメソッドはコールされた時点では開始されるだけで完了はしません。、
アンロードが完了されるのは、以下の条件を満たしたときとなります。
- コール スタックに、AssemblyLoadContext にロードされたアセンブリ内のメソッドを含むスレッドがなくなった。
- AssemblyLoadContext にロードされたアセンブリ内の型、それらの型のインスタンス、およびアセンブリ自体が参照されなくなった。
参照:.NET Core でアセンブリのアンローダビリティを使用およびデバッグする方法
AssemblyLoadContextのコンストラクタ引数にある'isCollectible'は'true'にする必要があります。これはパフォーマンスの観点からデフォルトでは'false'になっています。
また、'false'のまま'Unload'をコールすると例外が発生してしまうためです。
public class MyAssemblyLoadContext : AssemblyLoadContext
{
public MyAssemblyLoadContext() : base(isCollectible: true)
{
}
}
取込バッチ処理
クラスライブラリー(DLL)で、進捗状況のイベントを発生させ進捗値を渡します。
処理内容はサンプルなので適当です。
using System;
namespace SUB00001
{
public class Program : MarshalByRefObject
{
// 更新されると起きるイベント
public event ProgressChangedEventHandler 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)
{
// 更新イベントを起こす
ProgressChangedEventArgs e = new ProgressChangedEventArgs(value, null);
Update?.Invoke(this, e);
}
}
}
DLL差し替え結果
- デスクトップに取込バッチ処理クラスライブラリー(DLL)の成功版をコピーする。
- メイン画面を起動
- 取込バッチ処理クラスライブラリー(DLL)の失敗版を実行する(進捗画面は閉じる)。
- メイン画面を起動したまま、DLLを成功版に差し替える。
- 取込バッチ処理クラスライブラリー(DLL)の成功版を実行する。
失敗
SubProccess70()の戻り値を true -> false に変更。
DLL差し替え
成功
最後に
.NET Core版の移植に時間がかかるかと思ったんですが、案外あっさりとできました。
とはいっても先人たちが、記事を書いてくれたお陰様です。感謝!
今の時代にあんまり用途がない気がしますが、何かの役には立つことがあるかと思います。