當 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 的文档
- 如果不具备窗口的知识,这里有篇博文讲的很好