Skip to content

WPF 互操作

状态:即将推出。 一等公民的 Microsoft.UI.Reactor(Reactor)WPF 宿主控件 (Reactor.Interop.Wpf)已在路线图上,但尚未交付。目前 框架提供的唯一宿主包装是 Reactor.Interop.WinForms

现阶段的变通做法

在 WPF 中托管 Reactor 组件树,可以直接嵌入 DesktopWindowXamlSource —— 也就是 WinForms 互操作XamlIslandControl 包装的同一个 WinAppSDK 原语。 WPF 通过 HwndHost 接纳外来 HWND,因此孤岛住在 一个 HwndHost 子类里:

// WPF hosts foreign HWNDs through HwndHost. DesktopWindowXamlSource owns the
// island HWND; ReactorHostControl is the WinUI element mounted inside it.
//
// ReactorHostControl has no ComponentType property — that one belongs to the
// WinForms XamlIslandControl. On the WinUI side you either hand it a
// ComponentFactory or call Mount(...) directly.
sealed class ReactorWpfIsland : HwndHost
{
    private DesktopWindowXamlSource? _source;
    private ReactorHostControl? _host;

    protected override HandleRef BuildWindowCore(HandleRef hwndParent)
    {
        _source = new DesktopWindowXamlSource();
        _source.Initialize(Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hwndParent.Handle));
        _source.ShouldConstrainPopupsToWorkArea = true;

        _host = new ReactorHostControl { ComponentFactory = () => new WpfHostedDashboard() };
        _source.Content = _host;

        var bridgeHwnd = Microsoft.UI.Win32Interop.GetWindowFromWindowId(_source.SiteBridge.WindowId);
        return new HandleRef(this, bridgeHwnd);
    }

    protected override void OnRenderSizeChanged(SizeChangedInfo info)
    {
        base.OnRenderSizeChanged(info);
        // ActualWidth/Height are WPF DIPs; MoveAndResize sizes the child HWND
        // in physical pixels. Scale by the current DPI or the island is too
        // small at anything above 100%, and wrong after a monitor DPI change.
        var scale = System.Windows.Media.VisualTreeHelper.GetDpi(this);
        _source?.SiteBridge.MoveAndResize(new RectInt32(
            0, 0,
            (int)(ActualWidth * scale.DpiScaleX),
            (int)(ActualHeight * scale.DpiScaleY)));
    }

    protected override void DestroyWindowCore(HandleRef hwnd)
    {
        // The source's Dispose does not dispose its content — release the
        // Reactor host explicitly or the reconciler and its effects leak.
        _host?.Dispose();
        _host = null;
        _source?.Dispose();
        _source = null;
    }
}

在孤岛内部,你挂载的 WinUI 元素是一个 ReactorHostControl

ReactorHostControl 没有 ComponentType 属性。 那个属性 属于 WinForms 的 XamlIslandControl 包装,而不是宿主 控件本身。在 WinUI 一侧,请设置 ComponentFactory(用于无参 组件)或调用 Mount(component) / Mount(renderFunc)

// Mount(...) is the alternative when the component needs constructor
// arguments — ComponentFactory covers the parameterless case.
static class DirectMount
{
    public static ReactorHostControl Create(string title)
    {
        var host = new ReactorHostControl();
        host.Mount(new TitledDashboard(title));
        return host;
    }
}

class TitledDashboard(string title) : Component
{
    public override Element Render() => Heading(title);
}

边界上 Reactor 那一侧与任何其他宿主完全相同: 组件、Hook、修饰符,以及用于桥接 INotifyPropertyChanged 视图模型的 UseObservable<T>,都照常工作。

class WpfHostedDashboard : Component
{
    public override Element Render()
    {
        var (count, setCount) = UseState(0);

        return VStack(12,
            Heading("Reactor inside WPF"),
            TextBlock($"Count: {count}"),
            Button("+1", () => setCount(count + 1))
        ).Padding(24);
    }
}

有两条生命周期规则由 WinForms 包装替你处理,而 HwndHost 不会:

  • DesktopWindowXamlSource.Dispose() 不会释放它的内容。 请在 DestroyWindowCore 中自行释放 ReactorHostControl,否则 协调器与每一个活的副作用都会在整个进程生命期内泄漏。
  • 如果 WPF 重建了宿主窗口,请重新创建 source;复用一个 已释放的 source 会抛异常。

WPF 的 Dispatcher 与 WinUI 的 DispatcherQueue 是同一 UI 线程上 彼此不同的对象,因此从 WPF 事件处理函数直接写入属性、 落到 Reactor setter 上无需编组即可工作 —— WinUI 一侧强制执行的 不变式见 线程与调度

键盘输入、主题化与弹出窗口约束,是 XamlIslandControl 有投入、而本范例没有覆盖的部分:WinUI 在创建第一个孤岛之前, 需要 UI 线程上有一个 DispatcherQueue 与一个 Application 实例,而且必须泵送 ContentPreTranslateMessage 按键才能到达 XAML。请把 XamlIslandBootstrap 当作那套 配置的参考 —— WPF 宿主必须把它复现出来。

后续阅读

  • WinForms 互操作 —— 已交付的平行 宿主。请先读这一篇;WPF 的面会与它对应。
  • Hook —— 用于桥接来自 WPF 的 INotifyPropertyChanged 视图模型的 UseObservableUseObservableTreeUseObservableProperty
  • 线程与调度 —— Reactor 的 Hook setter 如何跨调度器自动编组。
  • 面向 XAML 开发者 —— 面向从 WPF/XAML 页面迁移到 Reactor 声明式外壳的 迁移手册。