阅读 120

WPF 通过进程实现异常隔离的客户端

当 WPF 客户端需要实现插件系统的时候,一般可以基于容器或者进程来实现。如果需要对外部插件实现异常隔离,那么只能使用子进程来加载插件,这样插件如果抛出异常,也不会影响到主进程。WPF 元素无法跨进程传输,但是窗口句柄(HWND)可以,所以可以将 WPF 元素包装成 HWND,然后通过进程间通信将插件传输到客户端中,从而实现插件加载。

1. 使用 HwndSource 将 WPF 嵌入到 Win32 窗口

HwndSource 会生成一个可以嵌入 WPF 的 Win32 窗口,使用 HwndSource.RootVisual 添加一个 WPF 元素。

复制代码123456789101112131415C#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 元素。

复制代码12345678910111213141516171819C#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() 可以返回插件的界面。

复制代码1234567C#namespace Plugin1
{    public class PluginStartup
    {
        public FrameworkElement CreateView() => new UserControl1();
    }
}

4. 启动插件进程,使用匿名管道实现进程间通信

进程间通信有多种方式,需要功能齐全可以使用 grpc,简单的使用管道就好了。

  • 客户端通过指定插件 dll 地址来加载插件。加载插件的时候,启动一个子进程,并且通过管道通信,传输包装插件的 Win32 窗口句柄。

复制代码1234567891011121314151617181920212223C#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 窗口,然后通过管道传输句柄。

复制代码1234567891011121314151617181920212223C#[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.OutserverHandle))
    {        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 的文档

  • 如果不具备窗口的知识,这里有篇博文讲的很好

本文作者:鹅群中的鸭霸

本文链接:https://www.cnblogs.com/wengzp/p/15305896.html


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐