WPFクライアントでプラグインシステムを実装する場合、通常はコンテナまたはプロセスベースで実現できます。外部プラグインに対して例外の分離が必要な場合は、子プロセスを使用してプラグインをロードするしかありません。これにより、プラグインで例外が発生してもメインプロセスに影響を与えません。WPF要素はプロセス間で転送できませんが、ウィンドウハンドル(HWND)は可能です。そのため、WPF要素をHWNDにラップし、プロセス間通信を介してプラグインをクライアントに転送することで、プラグインのロードを実現します。
1. HwndSourceを使用してWPFをWin32ウィンドウに埋め込む
HwndSourceは、WPFを埋め込むことができるWin32ウィンドウを生成します。HwndSource.RootVisualを使用してWPF要素を追加します。
private static IntPtr ViewToHwnd(FrameworkElement element)
{
var p = new HwndSourceParameters()
{
ParentWindow = new IntPtr(-3), // message only
WindowStyle = 1073741824
};
var hwndSource= new HwndSource(p)
{
RootVisual = element,
SizeToContent = SizeToContent.Manual,
};
hwndSource.CompositionTarget.BackgroundColor = Colors.White;
return hwndSource.Handle;
}
2. HwndHostを使用してWin32ウィンドウをWPF要素に変換する
Win32ウィンドウはWPFページに直接埋め込むことができないため、.NETは変換用のHwndHostクラスを提供しています。HwndHostは抽象クラスで、BuildWindowCoreメソッドを実装することでWin32ウィンドウをWPF要素に変換できます。
class ViewHost : HwndHost
{
private readonly IntPtr _handle;
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SetParent(HandleRef hWnd, HandleRef hWndParent);
public ViewHost(IntPtr handle) => _handle = handle;
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
SetParent(new HandleRef(null, _handle), hwndParent);
return new HandleRef(this, _handle);
}
protected override void DestroyWindowCore(HandleRef hwnd)
{
}
}
3. プラグインのエントリメソッドを規定する
プラグインのインターフェースを返す方法は複数あります。ここでは、各プラグインのDLLにPluginStartupクラスが存在し、PluginStartup.CreateView()でプラグインのインターフェースを返すものとします。
namespace Plugin1
{
public class PluginStartup
{
public FrameworkElement CreateView() => new UserControl1();
}
}
4. プラグインプロセスを起動し、匿名パイプでプロセス間通信を行う
プロセス間通信にはさまざまな方法があります。高機能が必要な場合はgRPC、シンプルなものならパイプで十分です。
- クライアントはプラグインDLLのパスを指定してプラグインをロードします。プラグインをロードする際、子プロセスを起動し、パイプ通信を介してプラグインをラップしたWin32ウィンドウハンドルを転送します。
private FrameworkElement LoadPlugin(string pluginDll)
{
using (var pipeServer = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable))
{
var startInfo = new ProcessStartInfo()
{
FileName = "PluginProcess.exe",
UseShellExecute = false,
CreateNoWindow = true,
Arguments = $"{pluginDll} {pipeServer.GetClientHandleAsString()}"
};
var process = new Process { StartInfo = startInfo };
process.Start();
_pluginProcessList.Add(process);
pipeServer.DisposeLocalCopyOfClientHandle();
using (var reader = new StreamReader(pipeServer))
{
var handle = new IntPtr(int.Parse(reader.ReadLine()));
return new ViewHost(handle);
}
}
}
- コンソールアプリケーションでプラグインDLLをロードし、プラグインのインターフェースをWin32ウィンドウに変換して、ハンドルをパイプで転送します。
[STAThread]
[LoaderOptimization(LoaderOptimization.MultiDomain)]
static void Main(string[] args)
{
if (args.Length != 2) return
var dllPath = args[0];
var serverHandle = args[1];
var dll = Assembly.LoadFile(dllPath);
var startupType = dll.GetType($"{dll.GetName().Name}.PluginStartup");
var startup = Activator.CreateInstance(startupType);
var view =(FrameworkElement) startupType.GetMethod("CreateView").Invoke(startup, null);
using (var pipeCline = new AnonymousPipeClientStream(PipeDirection.Out, serverHandle))
{
using (var writer = new StreamWriter(pipeCline))
{
writer.AutoFlush = true;
var handle = ViewToHwnd(view);
writer.WriteLine(handle.ToInt32());
}
}
Dispatcher.Run();
}
5. 効果

参考資料と注釈
- サンプルソース
- Win32とWPFの混在開発では、必然的に空域問題が発生します。
- 例外分離が不要な場合は、MEFやPrismを使用すれば十分なプラグイン機能を実現できます。
- System.AddInでも同様の機能を提供できますが、.NET Framework 4.8までしかサポートしていません。
- System.AddInベースのマルチプロセスプラグインフレームワークはこちら
- WPFとWin32のドキュメント
- ウィンドウに関する知識がない場合、こちらのブログ記事が参考になります