Skip to content

Commit 2c08803

Browse files
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>
1 parent 378f8eb commit 2c08803

2 files changed

Lines changed: 57 additions & 499 deletions

File tree

docs/_pipeline/templates/wpf-interop.md.dt

Lines changed: 31 additions & 174 deletions
Original file line numberDiff line numberDiff line change
@@ -4,188 +4,45 @@ app: wpf-interop
44
order: 20.5
55
audience: advanced
66
goal: |
7-
Status of WPF interop in Reactor: what ships today, what's on the
8-
roadmap, and how to host a Reactor component tree from a WPF app
9-
through the WinAppSDK primitives that already exist. Cover the
10-
hypothetical Reactor.Interop.Wpf surface so readers can plan around
11-
it, plus the data-flow / threading patterns that already work today.
12-
tier: solid
7+
Status page: a first-class WPF host control is not yet shipped. Point
8+
readers at the WinAppSDK primitive (`DesktopWindowXamlSource`) that
9+
unblocks WPF→Reactor hosting today, and at the WinForms interop docs
10+
as the shape reference for the eventual surface.
11+
tier: stub
1312
---
1413

1514
# WPF Interop
1615

17-
**Status:** A first-class WPF host control (`Reactor.Interop.Wpf`) is
18-
not yet shipped. The package `Reactor.Interop.WinForms` is the only
19-
host wrapper in the box today; the WPF equivalent is on the roadmap.
20-
Until it ships, host Reactor content from WPF by embedding
21-
[`DesktopWindowXamlSource`](https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.hosting.desktopwindowxamlsource)
22-
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-
![WPF and WinForms hosts both stand on DesktopWindowXamlSource and mount a ReactorHostControl that owns the Reactor element tree. The WPF host control on the left is on the roadmap; the WinForms one on the right ships today.](images/wpf-interop/host-architecture.svg)
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. |
51-
| `Reactor.Interop.Wpf.WpfXamlIslandControl` | Roadmap | `FrameworkElement` wrapping `DesktopWindowXamlSource`. `ComponentType` / `ContentFactory` / `XamlContent` slots mirror [WinForms](winforms-interop.md). |
52-
| `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-
![Reactor counter component as the future WPF host would mount it](screenshot://wpf-interop/host-element)
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`.
8419

85-
```csharp snippet="wpf-interop/data-flow"
86-
```
20+
## Workaround for today
8721

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
159-
reconciler knows about. Hand-marshalling onto WPF's dispatcher belongs
160-
in `UseEffect` cleanup against WPF objects, not inside `Render()`.
161-
162-
**Bridge view-models through `UseObservable`, not `DataContext`.** WPF
163-
`DataContext` is a pull-based binding propagation mechanism; Reactor
164-
binds by closure, with re-renders driven by `INotifyPropertyChanged`
165-
through [`UseObservable`](hooks.md). Pass the view-model through `Props`
166-
so the component is testable without a WPF host.
167-
168-
**Build and screenshot the component first.** Author the Reactor side
169-
(component, hooks, modifiers) as a standalone Reactor window the same
170-
way every other doc app in this docset does. When the WPF host control
171-
ships, dropping the component into a `WpfXamlIslandControl.ComponentType`
172-
slot is a one-line change.
22+
Host a Reactor component tree from WPF by embedding
23+
[`DesktopWindowXamlSource`](https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.hosting.desktopwindowxamlsource)
24+
directly — the same WinAppSDK primitive that
25+
[WinForms Interop](winforms-interop.md) wraps in `XamlIslandControl`.
26+
Inside the island, mount a `ReactorHostControl` and assign it a
27+
`ComponentType`. The Reactor side of the boundary is identical to any
28+
other host: components, hooks, modifiers, and
29+
[`UseObservable<T>`](hooks.md) for bridging an `INotifyPropertyChanged`
30+
view-model all work unchanged.
31+
32+
The WPF `Dispatcher` and WinUI `DispatcherQueue` are distinct objects
33+
on the same UI thread, so plain property writes from WPF event handlers
34+
into Reactor setters work without marshalling — see
35+
[Threading and Dispatch](threading-and-dispatch.md) for the invariants
36+
the WinUI side enforces.
17337

17438
## Next Steps
17539

176-
- **[WinForms Interop](winforms-interop.md)** — Previous: the shipping
177-
parallel host with `XamlIslandControl`, `XamlIslandBootstrap.Run()`,
178-
and the `ComponentType` designer integration the WPF surface will
179-
mirror.
180-
- **[Windows](windows.md)** — Next: top-level windows, multi-window apps,
181-
and tray icons inside a Reactor-primary host.
40+
- **[WinForms Interop](winforms-interop.md)** — The shipping parallel
41+
host. Read this first; the WPF surface will mirror it.
18242
- **[Hooks](hooks.md)** — `UseObservable`, `UseObservableTree`, and
183-
`UseObservableProperty` for bridging WPF view-models.
184-
- **[Threading and Dispatch](threading-and-dispatch.md)** — How
185-
Reactor's hook setters auto-marshal across dispatchers and the
186-
invariants the WinUI side enforces.
43+
`UseObservableProperty` for bridging `INotifyPropertyChanged`
44+
view-models from WPF.
45+
- **[Threading and Dispatch](threading-and-dispatch.md)** — How Reactor's
46+
hook setters auto-marshal across dispatchers.
18747
- **[XAML Developers](xaml-developers.md)** — Migration cookbook for
188-
WPF XAML pages moving to Reactor's declarative shell.
189-
- **[Reactor vs XAML](reactor-vs-xaml.md)** — The architectural shift
190-
from `Binding` / `DataContext` / `DataTemplate` to closure-based
191-
binding and function components.
48+
WPF/XAML pages moving to Reactor's declarative shell.

0 commit comments

Comments
 (0)