WPF implements exception-isolated clients through multiple processes

WPF implements exception-isolated clients through multiple processes

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. In this way, if the plug-in throws exceptions, it will not affect the main process.

最后更新 9/22/2021 10:10 PM
鹅群中的鸭霸
预计阅读 4 分钟
分类
WPF
标签
.NET WPF plugin system modular Win32

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

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 9/13/2025

Migration from WPF to Avalonia series: Why I have to migrate WPF programs to Avalonia

In the past few years, our host computer software has been mainly developed using WPF and WinForm. These technologies are really easy to use on the Windows platform, and they have also accompanied us through the stage of small-scale trial production to today's large-scale delivery. However, with the development of business and changes in customer needs, the single Windows technology stack has gradually become a hurdle that we must overcome.

继续阅读
同分类 / 同标签 1/26/2025

WPF internationalizes with custom XML files

This article describes in detail the methods of using custom XML files to achieve internationalization in WPF programs, including installing the necessary NuGet package, dynamically obtaining language lists, dynamically switching languages, using translation strings in code and xaml interfaces, etc. It also provides source code links to help developers easily internationalize WPF applications.

继续阅读