當 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").Invo(startup, nul; using (var pipeCline = new AnonymousPipeClientStream(PipeDirection.OutserverHandle))
{
using (var writer = new StreamWriter(pipeCline))
{
writer.AutoFlush = true;
var handle = ViewToHwnd(view);
writer.WriteLine(handle.ToInt32());
}
}
Dispatcher.Run();
}

4 效果

參考資料和備註