When a WPF client needs to implement a plug-in system, it can generally be implemented based on a container or process. If you need to isolate exceptions from external plug-ins, you can only use child processes to load the plug-in, so that if the plug-in throws exceptions, it will not affect the main process. WPF elements cannot be transferred across processes, but window handles (HWND) can, so you can wrap the WPF elements into HWND and then transfer the plug-in to the client through inter-process communication to implement plug-in loading.
1. Embed WPF into Win32 windows using HwndSource
HwndSource generates a Win32 window that can embed WPF, and uses HwndSource.RootVisual to add a WPF element.
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. Convert Win32 windows to WPF elements using HwndHost
Win32 windows cannot be directly embedded into WPF pages, so. Net provides a HwndHost class to convert. HwndHost is an abstract class that can convert a Win32 window into a WPF element by implementing the BuildWindowCore method.
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. Contract entry method for plug-in
You can return to the plug-in interface in a variety of ways. I have agreed here that each plug-in dll has a PluginStartup class, and PluginStartup.CreateView() can return the plug-in interface.
namespace Plugin1
{
public class PluginStartup
{
public FrameworkElement CreateView() => new UserControl1();
}
}
4. Start the plug-in process and use anonymous pipes to achieve inter-process communication
There are many ways to communicate between processes. Grpc needs to be fully functional. Simply using pipes is fine.
- The client loads the plug-in by specifying the plug-in dll address. When loading the plug-in, a subprocess is started and the Win32 window handle that wraps the plug-in is transferred through pipe communication.
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);
}
}
}
- Load the plug-in dll through the console program and convert the plug-in interface into a Win32 window, and then pipe the handle.
[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 Effects

References and remarks
- 示例源码
- The mixed development of win32 and WPF will inevitably involve airspace issues.
- If exception isolation is not needed, good plug-in functionality can be achieved using mef or prism.
- System.AddIn provides similar functionality, but only supports it. net framework 4.8.
- 这里有一个基于 System.AddIn 实现的多进程插件框架
- wpf 跟 win32 的文档
- 如果不具备窗口的知识,这里有篇博文讲的很好