【C#】動的にDLLを読み込んでプラグイン機能を実装する

2023年2月7日

こんにちは、ノムノムです。

今回は、実際に仕事でも利用しているDLLの動的読み込みを紹介しようかと思います。
動的に読み込むことができれば機能をあとから追加したりいろいろできます。

通常機能は無料で配布して、特定の機能はプラグインとして販売するということもできますね。
ではさっそくやってみます。

今回は動的に呼び出すことを中心に記述しています。ソースの書き方はご自身のやりやすいように書くのが良いかと思います。

プラグインでベースとなるクラスを作る

プラグインのベースとなる仮想クラスを作成します。プラグインを設定する上で必須となるプロパティやメソッドなどを仮想で宣言します。
これで継承したクラスはこれらの仮想プロパティや仮想メソッドを必ず実装しなくてはいけません。

以下に例を記述します。

public abstract class BasePluginClass
{
    //プラグインの名前を取得する
    public abstract String PluginName { get; }

    //フォームの呼び出し
    public abstract void FormLoad();
}

ベースプラグインを継承したクラスを作成する

作成したBasePluginのDLLを参照追加します。

参照追加したらBasePluginを継承したクラスを作成します。

以下は例です。

public  class PluginLoadClass : BasePlugin.BasePluginClass
{
    //プラグインの名前を取得する
    public override String PluginName { get { return "Plug In 1"; } }

    //フォームの呼び出し
    public override void FormLoad()
    {
        using (Forms.Plugin1Form form = new Forms.Plugin1Form())
        {
            form.ShowDialog();
        }
    }
}

フォームロード時に読み込んでメニューに反映する

メインプロジェクトにサンプルとしてこのような画面を作成しました。
FlowLayoutPanelには何もコントロールはありません。
ここに動的に読み込んだプラグインDLLを追加していこうと思います。

BasePluginを確認しますので参照を追加してください。

フォームロード時に追加する処理を記述しました。DLLを読み込むタイミングは好きなタイミングでいいと思います。

private void Form1_Load(object sender, EventArgs e)
{
    //EXEの場所を取得する
    string app_dir = System.Windows.Forms.Application.StartupPath;

    //EXEの場所にあるDLLファイルをすべて読み込む
    foreach (string dll in System.IO.Directory.GetFiles(app_dir))
    {
        //DLLファイル以外は読まない
        if (dll.ToLower().EndsWith(".dll") == false) continue;

        //ファイル読み込み
        var asm = System.Reflection.Assembly.LoadFrom(dll);

        //DLLの中のTypeをすべて取得し、プラグインのタイプがあるかチェックする
        foreach (Type type in asm.GetTypes())
        {
            //プラグインかどうかは継承元がプラグインベースになっているか
            if (type.BaseType != typeof(BasePlugin.BasePluginClass)) continue;

            //対象のクラスのインスタンスを作成
            dynamic plugin = Activator.CreateInstance(type);

            //ボタンを作成
            Button button = new Button();
            button.AutoSize = true;

            //BasePluginClassを継承していれば必ずPluginNameがある
            button.Text = plugin.PluginName;
            button.Tag = plugin;

            //ボタンのクリックイベントをプラグインに渡す。
            button.Click += btnPlubin_Click;

            //作成したボタンをフローレイアウトパネルに渡す
            this.flowLayoutPanel1.Controls.Add(button);
        }
    }
}

private void btnPlubin_Click(object sender, EventArgs e)
{
    //ボタンのTagプロパティに格納しておいたプラグインのインスタンスを取得する
    BasePlugin.BasePluginClass plugin = (sender as Button).Tag as BasePlugin.BasePluginClass;
    //フォームロードのメソッドを呼び出す。
    plugin.FormLoad();
}

ポイントは以下です。

  • var asm = System.Reflection.Assembly.LoadFrom(dll);でDLLの読み込みを行う
  • foreach (Type type in asm.GetTypes())でDLLが持つクラスをすべて取得する。
  • if (type.BaseType != typeof(BasePlugin.BasePluginClass))でBasePluginを継承したクラスかをチェックしている
  • dynamic plugin = Activator.CreateInstance(type);で作成したインスタンスを動的型付け変数に格納している

プラグインのDLLをデバッグフォルダに入れて実行します。

これを実行するとDLLが動的に読み込まれ、ボタンが生成されてFlowLayoutPanelに追加されます。
ボタンをクリックするとプラグイン内のフォームが呼び出されるはずです。

そのままデバッグ実行してもビルドされない

デバッグ実行をしたとき動的に呼ばれるプラグインに関してはビルドされません。

その理由は、ソリューション内での依存関係がないからです。
以下の例を見てください。

BasePluginに関してはPluginSampleプロジェクトで参照しているので開始を押してもビルドされますが、Plugin1、Plugin2に関してはPluginSampleで参照していません。

その為、依存関係がないと判断されデバッグ開始時にビルドの対象外になります。
さらに参照していれば参照プロパティでコピーされますが、参照していないのでPluginに関してはコピーされません。

ビルドをする場合は、以下のようのソリューションのビルドまたはリビルドを行うことをおススメします。
そうすれば全体をビルドするので依存関係にないPlugin1、Plugin2もビルドされます。

ビルドイベントを使って使いやすくする

ビルドを掛けてDLLをコピーすることが面倒ですのでビルドイベントを利用してメインプロジェクトであるPluginSampleの出力パスにビルド時にコピーされるようにします。

プラグインのプロジェクトのプロパティを開きます。

ビルドイベントから「ビルド後の編集」を開きます。

ここでビルド後に実行するコマンドラインを設定できます。

ここでマクロを指定してコマンドラインを記入します。今回作成したのは以下です。

copy /y $(TargetDir)$(TargetName)* $(SolutionDir)PluginSample\bin\$(ConfigurationName)

解説します。

copy /y はコピーコマンドです。ファイルがあっても上書きするため/yを付けています。
$(TargetDir)$(TargetName)* は出力フォルダのファイルのdllとpdbをソースにします。
$(SolutionDir)PluginSample\bin\$(ConfigurationName) はコピー先のPluginSampleの出力先フォルダをしてします。$(ConfigurationName)はDebugやReleaseなどの構成名が入ります。

これでプラグインをビルドした時に自動的にメインプロジェクトの出力先にコピーされます。

まとめ

これをどういう風に使っているかというと、DLLをサーバーに上げておきユーザーがアプリを起動するとDLLが自動的にダウンロードされ読み込まれるという風にしています。

なにが良いかというと

  • 更新のたびに作成していたインストーラーをいちいち全員に渡して再インストールしてもらわなくてよくなった
  • 機能の追加を行った際、サーバーにDLLをアップロードするだけでユーザー全体に提供できる

これがすごく良くて、運用しながら機能開発や改修作業を行ったり、要はアジャイル開発みたいにできるのですごく便利です。

障害対応もすぐできます。

ここまで読んで頂いてありがとうございます。