You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(wpf-interop): replace speculative surface with stub + workaround
The page previously described a hypothetical Reactor.Interop.Wpf
surface — bootstrap, host control, reference table, screenshots — that
hasn't shipped yet. Readers had to wade through ~190 lines of
roadmap-tense prose to learn the actually-actionable bit:
DesktopWindowXamlSource works today.
Demote to tier=stub. Keep just two paragraphs: a "coming soon" status
line and the WinAppSDK-primitive workaround, with Next Steps pointing
at WinForms interop as the shape reference.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
directly — the same WinAppSDK primitive [WinForms Interop](winforms-interop.md)
23
-
wraps in `XamlIslandControl`. This page describes the proposed surface
24
-
so existing WPF apps can plan around it, and the data-flow / threading
25
-
patterns that already work today.
26
-
27
-

28
-
29
-
## Proposed surface
30
-
31
-
The WPF host control will mirror `Reactor.Interop.WinForms`. Two types:
32
-
a single-shot bootstrap that brings up WinAppSDK alongside WPF's
33
-
dispatcher, and a `FrameworkElement` that wraps `DesktopWindowXamlSource`
34
-
and mounts a Reactor `Component` by type:
35
-
36
-
```csharp snippet="wpf-interop/bootstrap"
37
-
```
38
-
39
-
The snippet is roadmap-shaped — the types `WpfXamlIslandBootstrap` and
40
-
`WpfXamlIslandControl` are not yet shipped. The shape parallels
41
-
`XamlIslandBootstrap.Run()` from
42
-
[Reactor.Interop.WinForms](winforms-interop.md), with WPF's
43
-
`System.Windows.Application` driving the message loop in place of
44
-
`System.Windows.Forms.Application.Run()`.
45
-
46
-
## Reference
47
-
48
-
| Surface | Status | Notes |
49
-
|---|---|---|
50
-
| `Reactor.Interop.Wpf.WpfXamlIslandBootstrap.Run(Action)` | Roadmap | Initializes WinAppSDK + WinUI dispatcher; calls back into WPF startup. |
| `Microsoft.UI.Xaml.Hosting.DesktopWindowXamlSource` | Ships (WinAppSDK) | The underlying primitive both hosts wrap. Today's WPF apps embed this directly. |
53
-
| `Microsoft.UI.Reactor.Hosting.ReactorHostControl` | Ships | The WinUI `UserControl` that owns the Reactor element tree. Any WPF host control would mount this inside the island. |
54
-
| `UseObservable<T>(INotifyPropertyChanged)` | Ships | Bridges a WPF MVVM view-model into Reactor's re-render. |
55
-
56
-
The Reactor side is the same surface a WinForms or WinUI host uses —
57
-
nothing about the component, hooks, or modifiers changes inside the
58
-
island. The interop wrapper is purely a host-side adapter.
59
-
60
-
## The component a host mounts
61
-
62
-
`WpfXamlIslandControl.ComponentType` will accept any concrete
63
-
`Component` subclass with a parameterless constructor — same shape as
64
-
`XamlIslandControl.ComponentType` on the WinForms side. The component
65
-
itself has no idea it's hosted in WPF:
66
-
67
-
```csharp snippet="wpf-interop/host-element"
68
-
```
69
-
70
-

71
-
72
-
This is the screenshot of `WpfHostDemo` running in a stock Reactor
73
-
window — exactly what `WpfXamlIslandControl` will render once the host
74
-
ships. Build the component first, then bring up the WPF shell around
75
-
it when the API lands.
76
-
77
-
## Bridging a WPF MVVM view-model
78
-
79
-
The most common WPF migration story today is "I have a working
80
-
view-model implementing `INotifyPropertyChanged` and I want the Reactor
81
-
island to observe it." [`UseObservable`](hooks.md) does exactly that —
82
-
it subscribes the current component's re-render to the source's
83
-
`PropertyChanged` event:
16
+
**Status: coming soon.** A first-class WPF host control
17
+
(`Reactor.Interop.Wpf`) is on the roadmap but not yet shipped. The only
18
+
host wrapper in the box today is `Reactor.Interop.WinForms`.
84
19
85
-
```csharp snippet="wpf-interop/data-flow"
86
-
```
20
+
## Workaround for today
87
21
88
-
Passing the view-model through `Props` rather than capturing a
89
-
singleton in the closure makes the component independent of the host's
90
-
service-locator setup — the same component renders against a real
91
-
view-model from the WPF side and a fake one from a unit test that
92
-
drives `Component<UserPanelProps>` directly. The `OnPropertyChanged`
93
-
fire from a setter on the WPF side enqueues a render; the next render
94
-
sees the new value.
95
-
96
-
Two-way binding flows the other direction through plain event handlers.
97
-
The snippet's `TextField` writes back to `vm.Name`, which raises
98
-
`PropertyChanged`, which re-enters `UseObservable`, which schedules a
99
-
no-op re-render (the value has already been written; equality
100
-
short-circuits the slot update).
101
-
102
-
## Threading: WPF Dispatcher vs WinUI DispatcherQueue
103
-
104
-
The single most load-bearing fact about WPF + WinUI cohabitation:
105
-
WPF's `System.Windows.Threading.Dispatcher` and WinUI's
106
-
`Microsoft.UI.Dispatching.DispatcherQueue` are **distinct objects**.
107
-
They run on the same UI thread, but they are separate queues with
108
-
separate continuations. A property write that originates inside a WPF
109
-
event handler — a `Click`, a `Loaded`, a `Binding` update — is on
110
-
WPF's dispatcher; reaching a Reactor hook setter from that handler
111
-
means the setter sees an `Environment.CurrentManagedThreadId` that
112
-
matches the WinUI dispatcher's captured thread, so the fast path
113
-
runs and nothing has to be marshalled.
114
-
115
-
The case to think about is the *other* direction: a Reactor effect
116
-
that touches a WPF `DependencyProperty` directly. Inside the effect,
117
-
you're on the WinUI dispatcher; the WPF property setter expects WPF's.
118
-
Reach for `Dispatcher.Invoke` on the WPF side:
119
-
120
-
```csharp snippet="wpf-interop/threading"
121
-
```
122
-
123
-
The snippet uses an off-thread background timer to update Reactor
124
-
state — that path is fully automatic; `setTicks` detects the
125
-
off-dispatcher call and posts back onto the captured WinUI dispatcher
126
-
without any wrapper code. The hand-marshalling concern only applies
127
-
when you reach *out of* Reactor into WPF objects from inside an effect
128
-
or callback.
129
-
130
-
## Accessibility
131
-
132
-
Inside the WinUI subtree, every accessibility modifier from a pure
133
-
Reactor app keeps working — `AutomationName`, `HeadingLevel`,
134
-
`Landmark`, [`UseFocusTrap`](accessibility.md), and `UseAnnounce` all
135
-
operate against the WinUI automation peer attached to
136
-
`DesktopWindowXamlSource`:
137
-
138
-
```csharp snippet="wpf-interop/accessibility"
139
-
```
140
-
141
-
The interop boundary itself shows up as a sibling automation tree —
142
-
the screen reader walks WPF's tree via its window peer and the
143
-
WinUI tree via the island's peer. Build the WinUI-side semantics with
144
-
Reactor modifiers and don't try to reach across the boundary from
145
-
either side; the two automation trees are explicitly designed to
146
-
coexist without cross-tree parent/child links.
147
-
148
-
## Tips
149
-
150
-
**Use `Reactor.Interop.WinForms` as the shape reference.** Until the
151
-
WPF host ships, the WinForms host control demonstrates every
152
-
designer / `ComponentType` / `ContentFactory` decision the WPF host
153
-
will mirror. Reading [WinForms Interop](winforms-interop.md) first is
154
-
the fastest way to be productive against the proposed surface.
155
-
156
-
**Keep the Reactor component dispatcher-agnostic.** Don't reach for
157
-
`System.Windows.Threading.Dispatcher.CurrentDispatcher` from inside a
158
-
hook body — the captured `DispatcherQueue` is the only dispatcher the
0 commit comments