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视图模型的UseObservable、UseObservableTree与UseObservableProperty。 - 线程与调度 —— Reactor 的 Hook setter 如何跨调度器自动编组。
- 面向 XAML 开发者 —— 面向从 WPF/XAML 页面迁移到 Reactor 声明式外壳的 迁移手册。