天天看點

WPF 通過多程序實作異常隔離的用戶端

當 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").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 效果

WPF 通過多程式實作異常隔離的用戶端

參考資料和備注

  • ​​示例源碼​​
  • win32 和 WPF 混合開發,不可避免會涉及空域問題。
  • 如果不需要異常隔離,使用 mef 或者 prism 已經可以實作良好的插件功能。
  • System.AddIn 也可以提供類似的功能,但是隻支援到 .net framework 4.8。
  • ​​這裡有一個基于 System.AddIn 實作的多程序插件架構​​
  • ​​wpf 跟 win32 的文檔​​
  • ​​如果不具備視窗的知識,這裡有篇博文講的很好​​