LoginSignup
7
8

More than 3 years have passed since last update.

【.NET】進捗ダイアログ画面で動的DLL(差し替え可能)の進捗状況を表示する

Last updated at Posted at 2018-08-04

はじめに

前回「【.NET】進捗ダイアログ画面で動的DLLの進捗状況を表示する」を作成しましたが、メイン画面(EXE)を閉じないでクラスライブラリー(動的DLL)を差し替えようとすると下記のエラーが発生します。
使用中のフォルダー.png

今回はこの問題を解消して、メイン画面(EXE)を閉じないでもクラスライブラリー(動的DLL)を差し替え出来るようにします。

原因

.NET(マネージド)では、Win32の時のFreelibrary関数(DLLを呼び出しプロセスの アドレス空間から切り離す)のようなものはなく、一旦DLLをロードしたらプロセス(Default AppDomain)を終了するまでメモリに残り続けます。

通常のDefault AppDomainにロードされている場合において
appdomain.jpg

AppDimainの仕組み

AppDimainとは、単一プロセス内で分離された複数の実行領域(domain)を提供します。
具体的には、単一プロセスの中に型やセキュリティを管理する単位としてアプリケーション・ドメイン(AppDomain)という器を作成し、その中で処理を実行させます。

AppDomainは、CreateDomainを使用してユーザードメイン(User AppDimain)を複数作成することができます。各AppDomainは小型のプロセスのようなもので、同じプロセス内にあるドメイン間通信は本物のプロセス間通信より高速ですが、Remoting境界を越えた往来が必要となるため、パフォーマンスは多少の影響が出ます。

対応

ユーザードメインを生成し、そこにアセンブリをロードして実行します。終了時にユーザードメインをアンロードすることで解放が可能になります。
参照:動的DLLの解放について

// ドメインを生成
AppDomain ad = AppDomain.CreateDomain("TEST");
// インスタンス生成
Object instance = ad.CreateInstanceAndUnwrap("ClassLibrary1", "ClassLibrary1.Class1");
// DLLのメソッド実行
instance.test();
// ドメインを開放
AppDomain.Unload(ad);

言葉通りなら上記でも良さそうですが、実際にはこれではDLLを解放することが出来ません。

型情報の参照

普通にユーザードメイン(User AppDimain)を生成して遅延バインディングをしてしまうと、.NETのメタデータ(≒ 型情報)などのアセンブリがメインのAppDomainにロードされることになり、動的なアンロードができなくなります。
これを避けるには、ドメイン間通信で参照(アドレス)だけ渡したり値をシリアライズして渡したりする必要があります。

具体的にはプロキシークラスを使う方法とインターフェースクラスを使う方法があります。
ネット上にあるサンプルの多くは、インターフェースクラスを作成してます。インターフェイスを使うと静的リンク時と同様の記述でプロパティやメソッドを利用出来るので分かりやすいです。

プロキシクラスではパラメータがSystem.Stringでシリアライズ可能なら、他から購入したライブラリなどをアンロードしたいときに利用できる可能性があります。一方、インターフェイスではロードしたいアセンブリのソースに手を加える必要があります。

プログラム仕様

取込バッチ処理のクラスライブラリー(DLL)は動的な参照とします。
進捗状況をイベントで受け取りプログレスバーに進捗状況を表示することにしました。

今回は進捗ダイアログ画面をクラスライブラリー(DLL)に分離し、メイン画面からプロキシークラスを使用して進捗ダイアログ画面(DLL)をAppDimainを作成して実行します。

理由として、最初は取込バッチ処理のクラスライブラリー(DLL)をAppDimainを作成して実行したのですが、実行はできたもののAppDimainをアンロードしても解放できなかったのです。どうもイベント(OnUpdate)を関連付けた際にメインのAppDomainに参照が残ってしまうのが原因のようです。

ソースコード

進捗ダイアログ画面

frmDoWork.png

バックグラウンド処理にはBackgroundWorkerコンポーネントを使用しています。
前回頂いたコメントを反映して「ProgressChangedEventArgs」を使用しています。

今回は別のスレッドからForm上のコントロールにアクセスしたことにより「有効ではないスレッド間の操作」の例外エラーとなったため、コントロールに対してはInvoke((Action)delegate (){}) で囲んでいます。

frmDoWork
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; }

        private Type _myType = null;
        private Object _instance = null;

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

        // 画面初回表示時
        private void frmDoWork_Shown(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);
            // 閉じるボタンを無効にする
            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();
        }
    }
}

メイン画面

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

frmMain.cs
private void btnExecute_Click(object sender, EventArgs e)
{
    // メッセージボックスを表示する
    DialogResult result = MessageBox.Show("実行します。よろしいですか ? ",
                                          "処理実行",
                                          MessageBoxButtons.YesNo,
                                          MessageBoxIcon.Question,
                                          MessageBoxDefaultButton.Button2);
    if (result == DialogResult.Yes)
    {
        // 処理実行
        AppDomain appDomain = AppDomain.CreateDomain("Domain");
        Type type = typeof(Proxy);
        Proxy proxy = (Proxy)appDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, type.FullName);
        // 実行ファイル名 サンプルなので固定
        // 進捗画面表示
        proxy.Execute("SUB00001.dll", "");
        AppDomain.Unload(appDomain);
    }
}

プロキシクラス

frmMain.cs内に追加しています。

Proxy
public class Proxy : MarshalByRefObject
{
    public dynamic instance;

    // 進捗画面アセンブリのロード
    public Proxy()
    {
        string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
        Assembly asm = Assembly.LoadFile(Path.Combine(path, "DoWork.dll"));
        Type type = asm.GetType("DoWork.frmDoWork");
        instance = Activator.CreateInstance(type);
    }

    // 進捗画面表示
    public void Execute(string executeName, string args)
    {
        instance.Execute(executeName, args);
    }
}

取込バッチ処理

クラスライブラリー(DLL)で、進捗状況のイベントを発生させ進捗値を渡します。
処理内容はサンプルなので適当です。

今回は[Serializable]属性ではなく、MarshalByRefObjectを継承しています。
参照:MarshalByRefObjectとSerializableのちょっとした違い (アプリケーションドメイン, 境界越え, Assembly, AppDomain, CreateInstanceAndUnwrap)

Program.cs
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差し替え結果

  1. デスクトップに取込バッチ処理クラスライブラリー(DLL)の成功版をコピーする。
  2. メイン画面を起動
  3. 取込バッチ処理クラスライブラリー(DLL)の失敗版を実行する(進捗画面は閉じる)。
  4. メイン画面を起動したまま、DLLを成功版に差し替える。
  5. 取込バッチ処理クラスライブラリー(DLL)の成功版を実行する。

失敗

SubProccess70()の戻り値を true -> false に変更。
import_progress2.gif

DLL差し替え

コピー完了.png

成功

import_progress.gif

その他

今回これを作成するにあたり、プラグイン作成関連を調べました。System.AddInとMEFは調べるまで知らなかったです。
これから拡張機能を作成するならMEF2を使用していくのがいいでしょう。

System.AddIn

System.Addin(MAF - 管理アドインフレームワークとも呼ばれる)は、バージョン3.5以降、.NETに存在します。アプリケーションにアドインとかプラグイン機構を作りこむためのライブラリ。
System.Addinでは、使用されなくなったAppDomainを自動的にアンロードし、メモリを再利用できるように管理している。
フォルダ構造が決まっちゃってるなど、かなり面倒くさい。2008年のリリース以来変更が加えられていません。

MEF

MEFはプラグイン的な動的ローディングをサポートするためのフレームワーク。
MEFは、.NET Framework 4からは標準ライブラリに取り込まれ、System.ComponentModel.Composition名前空間以下のクラスがMEFの実体となります。
MEF2は、.NET Framework 4.5以降で使用できる。

最後に

今回「進捗ダイアログ画面で進捗状況を表示する」シリーズを作成して、知らなかった知識を得られました。
今回作成するにあたり参照した記事はどれも古いものが多く、それだけ需要がないんでしょうね。

メイン画面を終わらせれなかったり、Windows サービスや常駐プログラムのように常時稼働しているタイプのプログラムで、停止や終了をせずに参照しているプログラムを入れ替えたいという要望がどのくらいあるのか分かりませんが、何かの役には立つと思います。

参照

7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8