When a WPF client needs to implement a plugin system, it can generally be based on containers or processes. If exception isolation for external plugins is required, only a child process can be used to load the plugin. This way, even if the plugin throws an exception, the main process will not be affected. WPF elements cannot be transmitted across processes, but window handles (HWND) can. Therefore, WPF elements can be wrapped into HWNDs and transmitted to the client via inter-process communication, thereby achieving plugin loading.
1. Using HwndSource to Embed WPF into a Win32 Window
HwndSource creates a Win32 window that can embed WPF content. Use 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. Using HwndHost to Convert a Win32 Window into a WPF Element
Win32 windows cannot be directly embedded into WPF pages. Therefore, .NET provides the HwndHost class to perform the conversion. HwndHost is an abstract class. By implementing the BuildWindowCore method, you can convert a Win32 window into a WPF element.
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. Defining the Plugin's Entry Method
There are multiple ways to return the plugin's UI. Here, I define that each plugin's DLL must have a PluginStartup class, and PluginStartup.CreateView() returns the plugin's UI.
namespace Plugin1
{
public class PluginStartup
{
public FrameworkElement CreateView() => new UserControl1();
}
}
4. Starting the Plugin Process and Using Anonymous Pipes for Inter-Process Communication
There are multiple ways to achieve inter-process communication. For full functionality, you can use gRPC; for simplicity, pipes are sufficient.
- The client loads the plugin by specifying the plugin DLL path. When loading the plugin, a child process is started, and the Win32 window handle encapsulating the plugin is transmitted via 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);
}
}
}
- The console application loads the plugin DLL, converts the plugin UI into a Win32 window, and transmits the handle through the pipe.
[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 pipeClient = new AnonymousPipeClientStream(PipeDirection.Out, serverHandle))
{
using (var writer = new StreamWriter(pipeClient))
{
writer.AutoFlush = true;
var handle = ViewToHwnd(view);
writer.WriteLine(handle.ToInt32());
}
}
Dispatcher.Run();
}
5. Effect

References and Notes
- Example Source Code
- Mixed Win32 and WPF development inevitably involves airspace issues.
- If exception isolation is not required, using MEF or Prism already provides good plugin functionality.
System.AddInalso provides similar functionality but only supports .NET Framework 4.8.- Here is a multi-process plugin framework based on System.AddIn
- WPF and Win32 Interoperation Documentation
- If you lack knowledge about windows, here is a well-written blog post