はじめに
C#によるWPFアプリ開発において、ビルド後から機能を追加したい状況に遭遇したため、機能をプラグイン化してアプリ内で動的に読み込めるようにした。方法を以下に残す。
概要
インターフェースを活用し、プラグインの実装と読み込みを行う。そのためには、アプリ側でプラグインを読み込めるようにする共通インターフェースが不可欠である。
まず、アプリ側では、共通インターフェースを継承したクラスを読み込めるようにする。
次に、プラグイン側では、共通インターフェースを継承したクラスにプラグイン個別の処理を記述する。
そして、共通インターフェースを通してアプリ側でDLL化したプラグインを読み込む。
実装
例として、ドロップダウンで選択した動物の鳴き声をテキストボックスに表示する簡単なWPFアプリを実装する。プラグインとしては、動物の種類を後から追加できるようにする。
また、データバインディングの処理を簡潔にするためReactiveUIを使用する。プログラム全体でエラーハンドリングは省略する。
プロジェクト名はTestAppで統一する。
0. 準備
ディレクトリ構成を以下の通りにする。
TestApp/
|-TestApp.Desktop/ # wpfアプリのプロジェクト (TestApp.Pluginを依存関係として追加)
| `-TestApp.Desktop.csproj
|-TestApp.Plugin/ # プラグインの抽象クラスを実装するプロジェクト
| `-TestApp.Plugin.csproj
|-TestApp.Plugins/ # ここにプラグインをプロジェクト単位で追加していく
| |-Plugin1/
| | `Plugin1.csproj
| |-Plugin2/
| | `Plugin2.csproj
| `...
`-TestApp.sln
TestApp.Pluginプロジェクトに共通インターフェースを実装し、TestApp.DesktopプロジェクトにWPFアプリを実装する。さらに、TestApp.Plugins/ディレクトリ内にプラグインプロジェクトを配置していくようになる。
プラグイン関係のプロジェクトの作成は、Visual Studioのプロジェクト新規作成にて「クラスライブラリ」を選択する。
Visual Studioディレクトリ構成変更方法の備忘録
Visual Studioではソリューションファイルによりプロジェクトの構成が定義されている。そのため、Visual Studio内で特定の手順を踏んで構成を変更しなければならない。
Visual Studio内で行う手順を以下に示す。
- 場所を変えたいプロジェクトを、[右クリック]->[プロジェクトをアンロード]からアンロード。そして、アンロードしたプロジェクトをソリューションエクスプローラー上で削除
- 移動先ディレクトリを作成し、その中にアンロードしたプロジェクト関係のファイルを入れる
- ソリューションを右クリックし、[追加]->[既存のプロジェクト]から移動させたプロジェクトのcsprojファイルを選択する
- 移動完了
1. 共通インターフェースの実装
TestApp.Pluginプロジェクトに共通インターフェースIAnimalを実装する。
IAnimalインターフェースには、動物の名前であるNameフィールドと、動物の鳴き声を返すCryメソッドを定義する。
namespace TestApp.Plugin
{
public interface IAnimal
{
/// <summary>
/// 動物名
/// </summary>
string Name { get; }
/// <summary>
/// 動物の鳴き声を返すメソッド
/// </summary>
/// <returns>動物の鳴き声</returns>
string Cry();
}
}
2. WPFアプリの実装
TestApp.DesktopプロジェクトにWPFアプリを実装する。
WPFアプリはMVVMアーキテクチャを採用しているため、ViewとViewModelを実装する。
ViewはMainWindowView.xaml、ViewModelはMainWindowViewModel.csとする。
2.0. TestApp.Pluginプロジェクトを依存関係として追加
IAnimalインターフェースを読み込み側であるWPFアプリで参照できるように、TestApp.PluginプロジェクトをTestApp.Desktopに依存関係として追加する。
ソリューションエクスプローラーでTestApp.Desktopプロジェクトを[右クリック]->[ビルドの依存関係]->[プロジェクトの依存関係]から開かれたウィンドウで、TestApp.Pluginにチェックボックスを入れてOKを押す。
2.1. Viewの実装
まず、MainWindowView.xamlを実装する。
読み込んだプラグインを選択するComboboxコントロール、プラグインの処理を呼び出すButtonコントロール、Buttonが押されたら文章を表示するLabelコントロールを、StackPanelコントロールを使用して縦に並べる。
<Window x:Class="TestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestApp"
mc:Ignorable="d"
Title="MainWindow"
Height="200"
Width="400">
<StackPanel Margin="20">
<ComboBox DisplayMemberPath="Name" // `IAnimal.Name`を指す
ItemsSource="{Binding Animals}"
SelectedIndex="{Binding SelectedIndex}"/>
<Button Command="{Binding CallCommand}"
Margin="20">call</Button>
<Label HorizontalContentAlignment="Center"
Content="{Binding AnimalSoundText}" />
</StackPanel>
</Window>
2.2 ViewModelの実装
次に、MainWindowViewModel.csを実装する。
using ReactiveUI;
using System.Windows.Input;
using TestApp.Desktop.Models;
namespace TestApp.Desktop.ViewModels
{
public class MainWindowViewModel : ReactiveObject
{
private string _animalSoundText = string.Empty;
/// <summary>
/// 動物の鳴き声のテキスト
/// </summary>
public string AnimalSoundText
{
get => _animalSoundText;
set => this.RaiseAndSetIfChanged(ref _animalSoundText, value);
}
private int _selectedIndex = -1;
/// <summary>
/// ComboBoxで選択された要素のインデックス
/// </summary>
public int SelectedIndex
{
get => _selectedIndex;
set => this.RaiseAndSetIfChanged(ref _selectedIndex, value);
}
/// <summary>
/// 読み込まれたプラグインのリスト
/// </summary>
public Animals Animals { get; }
/// <summary>
/// ボタンが押されたら呼び出されるコマンド
/// </summary>
public ICommand CallCommand { get; }
public MainWindowViewModel()
{
var config = new Config();
Animals = new Animals(config.PluginDir);
CallCommand = ReactiveCommand.Create(call);
}
private void call()
{
if (SelectedIndex < 0 || SelectedIndex >= Animals.Count())
{
return;
}
AnimalSoundText = Animals[SelectedIndex].Cry();
}
}
}
2.3. Modelの実装
Modelには、DLLファイルをC#のクラスとして動的生成するPluginActivatorクラスと、プラグインを読み込んでリストとして保持するAnimalsクラスを実装する。
まず、PluginActivatorクラスを実装する。
using System.Diagnostics;
using System.IO;
using System.Reflection;
using TestApp.Desktop.Plugin;
namespace TestApp.Desktop.Models
{
public class PluginAvtivator
{
/// <summary>
/// プラグインを返すイテレータメソッド
/// </summary>
/// <param name="pluginDir">プラグインディレクトリのパス</param>
/// <returns>IAnimalを継承したクラスをイテレートして返す</returns>
public static IEnumerable<IAnimal> Load(string pluginDir)
{
foreach (var dllPath in Directory.GetFiles(pluginDir, "*.dll"))
{
Assembly asm = Assembly.LoadFrom(dllPath);
Debug.WriteLine($"{asm.GetName().Name} : assembly loaded.");
foreach (var type in asm.GetTypes()) // アセンブリに含まれるクラス等を全て取得
{
if (type == null) continue;
if (type.GetInterfaces().Contains(typeof(IAnimal))) // クラスに継承されているインターフェースを取得し、IAnimalが含まれていたら
{
var plugin = (IAnimal)Activator.CreateInstance(type)!; // クラスをインスタンス化する
if (plugin != null) yield return plugin;
}
}
}
}
}
}
次に、Animalsクラスを実装する。
using System.Collections.ObjectModel;
using TestApp.Desktop.Plugin;
namespace TestApp.Desktop.Models
{
public class Animals : ObservableCollection<IAnimal>
{
public Animals(string pluginDir)
{
load(pluginDir);
}
private void load(string pluginDir)
{
foreach (var plugin in PluginAvtivator.Load(pluginDir))
{
this.Add(plugin);
}
}
}
}
3. プラグインの実装
TestApp.Pluginsディレクトリ内に複数のプラグインを実装する。プラグインとなるクラスには2.0. TestApp.Pluginプロジェクトを依存関係として追加と同様に依存関係を追加し、IAnimalインターフェースを継承する。
例としてCatプラグインとDogプラグインを実装する。
ディレクトリ構成は以下のようになる。
TestApp.Plugins/
|-Cat/
| |-Cat.cs
| `Cat.csproj
|-Dog/
| |-Dog.cs
| `Dog.csproj
`...
3.1. 例:Catプラグインの実装
以下にCatプラグインの実装を示す。
using TestApp.Plugin;
namespace Cat
{
public class Cat : IAnimal
{
public string Name { get; } = "Cat";
private string _sound = "meow";
public Cat() { }
public string Cry()
{
return _sound;
}
}
}
3.2. 例:Dogプラグインの実装
以下にDogプラグインの実装を示す。
using TestApp.Plugin;
namespace Dog
{
public class Dog : IAnimal
{
public string Name { get; } = "Dog";
private string _sound = "woof";
public Dog() { }
public string Cry()
{
return _sound;
}
}
}
4. プラグインの読み込み
プラグインをビルドし、生成されたdllファイルをWPFアプリで読み込む。
メニューバーから[ビルド]->[Cat|Dogのビルド]を行うと、TestApp.Plugins/Cat|Dog/bin/Debug/net9.0ディレクトリにCat|Dog.dllが生成される。(Debugビルドの場合)
生成したdllファイルは任意のディレクトリに配置し、TestApp.DesktopのAnimalsクラスにディレクトリのパスを設定する。
5. 実行
実行すると以下のようなウィンドウが起動する。コンボボックスにCat、Dogが追加されており、選択した状態でcallボタンを押すと、選択した動物の鳴き声がその下に表示されることがわかる。
