こんにちは!現場の最前線でバリバリとシステムを組んでいると、「本体のプログラムを1行も書き換えずに、新しい機能や新しい帳票レイアウトを後から自由に追加できるようにしてほしい!」という、なかなか燃える(そして頭を悩ませる)要件に出会うことがありますよね。
「マクロの記録」や「ボタンをポチポチ配置するだけのWindows Forms」から一歩進んで、今回はVB.NETによるリフレクションとAppDomainを活用したプラグイン機構の自社開発という、エンタープライズ領域の骨太なテーマに挑戦してみましょう。
ここをクリアすれば、あなたのVB.NETのスキルは間違いなく一段上のステージに到達します。難しそうに見えますが、一つひとつの部品の意味を丁寧に紐解けば必ず理解できます。優しい先輩と一緒に、しっかりマスターしていきましょう!
—
1. なぜ「プラグイン機構」が必要なのか?
私たちが普段作るWindows Formsアプリケーションは、コンパイル(ビルド)すると1つの固まったexeファイル(または周辺のDLL)になります。新しい帳票を追加するたびに、本体のプログラムを修正して、全ユーザーに再インストールしてもらう……なんてやっていられませんよね。
そこで登場するのがプラグインアーキテクチャです。
[ コアアプリケーション (本体) ]
│
├── 共通の「契約書」 (Interface) を持っている
│
▼
[ 外部DLL (プラグイン1: 請求書) ] ── 動的にロード!
[ 外部DLL (プラグイン2: 納品書) ] ── 動的にロード!
本体と外部の機能(DLL)の間で「こういうメソッドの名前でデータを渡してね」という共通の契約(インターフェイス)だけを取り決めておき、本体は実行時にフォルダの中を覗き見(リフレクション)して、勝手に新しい機能を取り込んで動かす、という仕組みを作ります。
—
2. 全体設計の3つのステップ
今回の実装は、以下の3つのステップで進めます。
1. 共通インターフェイスの定義(Contract):本体とプラグインの共通言語となるルールブック。
2. プラグインの実装(Plugin):実際に動く外部機能(今回は帳票出力のイメージ)。
3. コアアプリケーション側の動的ロード(Host):DLLを見つけて読み込み、実行する主役。
—
3. 実装コードで理解するプラグインの仕組み
それでは、実際のVB.NETコードを見ていきましょう。そのままコピー&ペーストして、Visual Studioのプロジェクトで試せる構成にしています。
ステップ1:共通インターフェイスの定義(共通プロジェクト)
まずは、本体もプラグインも「これに従おうね」と約束するインターフェイスを作ります。
‘ 【プロジェクト名: PluginContract】
Namespace PluginContract
‘ すべてのプラグインが実装すべき契約(インターフェイス)
Public Interface IReportPlugin
‘ プラグインの表示名
ReadOnly Property PluginName As String
‘ 実行メソッド(データを渡して処理させる)
Sub ExecuteReport(ByVal targetData As String)
End Interface
End Namespace
ステップ2:プラグインの実装(外部DLLプロジェクト)
次に、先ほどの共通インターフェイスを参照設定した別のプロジェクト(DLL)を作成し、具体的な処理を書きます。これが「後から追加される帳票機能」になります。
‘ 【プロジェクト名: MonthlyInvoicePlugin (DLLとしてビルド)】
Imports PluginContract
Public Class MonthlyInvoicePlugin
Implements IReportPlugin
‘ プラグイン名の定義
Public ReadOnly Property PluginName As String Implements IReportPlugin.PluginName
Get
Return “月次請求書出力プラグイン v1.0″
End Get
End Property
‘ 実際の処理
Public Sub ExecuteReport(ByVal targetData As String) Implements IReportPlugin.ExecuteReport
‘ ここに実際の帳票作成ロジック(Excel出力やPDF生成など)を書く
MessageBox.Show($”[外部DLLより実行] 対象データ ‘{targetData}’ の請求書を発行しました!”,
PluginName,
MessageBoxButtons.OK,
MessageBoxIcon.Information)
End Sub
End Class
ステップ3:コアアプリケーションからの動的ロード(本体プロジェクト)
いよいよ本丸です。Windows Formsのボタンをクリックしたときに、特定のフォルダ(例: `Plugins`フォルダ)にあるDLLを自動で探し出し、メモリにロードして実行するコードです。
‘ 【プロジェクト名: CoreHostApp (Windows Forms)】
Imports System.IO
Imports System.Reflection
Imports PluginContract
Public Class MainForm
Private Sub btnLoadPlugins_Click(sender As Object, e As EventArgs) Handles btnLoadPlugins.Click
‘ プラグインを格納するフォルダのパス
Dim pluginDir As String = Path.Combine(Application.StartupPath, “Plugins”)
If Not Directory.Exists(pluginDir) Then
MessageBox.Show(“Pluginsフォルダが見つかりません。”, “エラー”, MessageBoxButtons.OK, MessageBoxIcon.Error)
Return
End If
‘ フォルダ内のすべてのDLLファイルを取得
Dim dllFiles As String() = Directory.GetFiles(pluginDir, “.dll”)
cmbPlugins.Items.Clear()
For Each dllPath As String In dllFiles
Try
‘ 1. アセンブリ(DLL)を動的に読み込む
Dim asm As Assembly = Assembly.LoadFrom(dllPath)
‘ 2. アセンブリの中から、IReportPluginを実装している型(クラス)を探す
For Each t As Type in asm.GetTypes()
If t.GetInterface(GetType(IReportPlugin).FullName) IsNot Nothing Then
‘ 3. インスタンスを動的に生成
Dim pluginInstance As IReportPlugin = CType(Activator.CreateInstance(t), IReportPlugin)
‘ 4. コンボボックスなどに名前を登録(後で呼び出せるようにオブジェクトも一緒に持たせる)
cmbPlugins.Items.Add(New PluginItem(pluginInstance))
End If
Next
Catch ex As Exception
MessageBox.Show($”DLLの読み込みに失敗しました: {Path.GetFileName(dllPath)}{vbCrLf}{ex.Message}”,
“警告”, MessageBoxButtons.OK, MessageBoxIcon.Warning)
End Try
Next
MessageBox.Show(“プラグインのロードが完了しました!”, “成功”, MessageBoxButtons.OK, MessageBoxIcon.Information)
End Sub
‘ コンボボックスに綺麗にプラグイン名を表示するためのヘルパー構造体
Private Class PluginItem
Public Property Plugin As IReportPlugin
Public Sub New(ByVal p As IReportPlugin)
Me.Plugin = p
End Sub
Public Overrides Function ToString() As String
Return Plugin.PluginName
End Function
End Class
‘ ユーザーが選択したプラグインを実行するボタン
Private Sub btnExecute_Click(sender As Object, e As EventArgs) Handles btnExecute.Click
Dim selectedItem As PluginItem = TryCast(cmbPlugins.SelectedItem, PluginItem)
If selectedItem IsNot Nothing Then
‘ 共通のインターフェイス経由でメソッドを呼び出す!
selectedItem.Plugin.ExecuteReport(“2023年10月度”)
Else
MessageBox.Show(“プラグインが選択されていません。”, “確認”, MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
End If
End Sub
End Class
—
4. 現場で直面しがちな「罠」と回避の知見
このプラグイン機構を実装する際、シニアエンジニアでもハマる有名な罠がいくつかあります。あらかじめ知っておけば怖くありません。
① 「DLLをアンロードできない」問題とAppDomainの活用
通常の `Assembly.LoadFrom` で読み込んだDLLは、一度メモリにロードされると、プログラムを終了するまでOSから解放(アンロード)されません。
つまり、開発中に「プラグインのバグを直したから、DLLを上書きしてアプリを再起動せずにテストしたい!」と思っても、ファイルがロックされて上書きできない現象が起きます。
【解決のアプローチ】
これを完全に解決するには、通常のAppDomainではなく、別ドメイン(AppDomain)を切ってその中でアセンブリを読み込み、不要になったらドメインごとアンロードする高度な設計が必要になります(※.NET Frameworkと.NET Core/5+では仕様が異なるため、モダンな.NET環境では `AssemblyLoadContext` を活用するのが近年のトレンドです)。まずは上記の基本コードで「動的ロードの快感」を味わい、その後に「アンロードの必要性」が出てきたらコンテキスト分離を調べましょう。
② バージョン不整合エラー(MissingMethodExceptionなど)
コア側のインターフェイス(`PluginContract`)を変更したのに、古いプラグインDLLをそのまま同じフォルダに残していると、キャスト時に予期せぬエラーが発生します。
プラグインを配布する際は、必ずインターフェイスのバージョン管理(アセンブリバージョン)を意識するか、起動時にインターフェイスのハッシュ値を検証するなどの防衛的プログラミングを心がけましょう。
—
おわりに
お疲れ様でした!今回は、VB.NETを用いたリフレクションによる動的アセンブリロードの核心部分を解説しました。
「本体をいじらずに、外部から機能を差し込む」という発想は、Windows Formsアプリケーションの寿命を劇的に伸ばし、チーム開発における分業を圧倒的にスムーズにします。
「ここをこう変えたらどうなるだろう?」という好奇心を大切に、ぜひご自身の開発環境でも試してみてください。この知見があなたのエンジニアとしての武器となり、より柔軟で美しいシステムを生み出す力になることを確信しています。
一緒にVB.NETの可能性を限界まで引き出していきましょう!
