Skill v1.0.0
currentTrusted Publisher100/100version: "1.0.0" name: reactor-wrapping description: "Turn ANY WinUI control — a vendor/third-party control, your own custom Control, or a Windows Community Toolkit one — into a first-class declarative Reactor element with the [GenerateReactorWrapper] source generator, zero hand-written wrapper code. Covers auto-inference (props, content, two-way pairs, panel/items children, events), the [Wrap*] annotation cheat-sheet, imperative-lifecycle controls, diagnostics, and when to fall back to a hand-written ControlDescriptor."
Reactor — Wrapping Arbitrary Controls (source generator)
Use this skill when the app needs a control Reactor has no built-in factory for — a Windows Community Toolkit control, a third-party vendor control, or the app's own custom Control subclass. The Reactor.Wrappers.Generator source generator turns the control into a first-class declarative element (init-props + ControlDescriptor + Pattern-A registration + a parameterized factory named after the control) from a single annotated partial record. No hand-written wrapper, handler, or registration code.
Canonical worked example:samples/apps/wct-controls/(declarations inWctControls.cs, usage inProgram.cs). Design spec:docs/specs/058-control-wrapper-generator.md.For controls the generator still can't express, drop to the hand-writtenControlDescriptor/IElementHandlerpath indocs/guide/extending-reactor-controls.md.
The whole feature in one example
using Microsoft.UI.Reactor.Wrappers; // the [Wrap*] authoring attributesnamespace MyApp;// 1. Declare an empty partial record, annotated with the control to wrap.[GenerateReactorWrapper(typeof(CommunityToolkit.WinUI.Controls.SettingsCard))]public partial record SettingsCardElement;
// 2. Consume the generated factory via `using static` (named after the control).using static MyApp.SettingsCardElement;SettingsCard(header: "Wi-Fi",description: wifiOn ? "Connected" : "Disconnected",content: ToggleSwitch(isOn: wifiOn, onIsOnChanged: setWifiOn)) // a Reactor child INSIDE a WCT control
The generator fills the rest of that partial: one init-property per surfaced control property, the single-content/children slot, On{Event} callbacks, the ControlDescriptor, the trim-rooting static cctor registration, and the SettingsCard(...) factory. Setters is always emitted as an escape hatch (.Set(c => c.Foo = …)) for anything not surfaced.
You never call `ReactorApp.RegisterControlAssembly` yourself — the generator emits it in the element's static constructor, so a control library's Themes/Generic.xaml resolves before first realization (avoids the 0xC000027B crash, spec 058 §9a).
Requirements & setup
- Reference
Microsoft.UI.Reactor(carries the generator inanalyzers/dotnet/cs/
and the authoring attributes in Reactor.Wrappers.Abstractions.dll).
- Reference the control's own package (e.g. the WCT
CommunityToolkit.WinUI.Controls.* packages).
- The target must be a non-static `FrameworkElement` with a **public
parameterless constructor** (else REACTORGEN001).
- Keep the record
partial(the generator fills the other half) and in a
namespace you control.
What is auto-inferred (zero config)
The generator walks members from the most-derived type up to but excluding Control/FrameworkElement (Reactor already models that layout plumbing via generic modifiers like .Margin(), .Width()):
| Control shape | Becomes | |
|---|---|---|
Settable value prop (string/object→text, bool/int/double/enum, structs like Thickness/Color, Brush/FontFamily, bool?/DateTimeOffset? tri-state) | a nullable / Optional<T> one-way init-prop (skipped when unset) | |
Value prop P paired with a {P}Changed event | a two-way controlled Optional<T> prop + On{P}Changed callback, with echo suppression | |
The control's [ContentProperty] (a UIElement child, or object named exactly Content) | the single `content:` child slot | |
A public Children of UIElementCollection (a Panel) | a params Element[] children slot | |
A public Items whose type is/implements IList<object> (an ItemsControl) | a params object[] items slot | |
A fire-and-forget RoutedEventHandler / TypedEventHandler<,> / EventHandler<A> event | an On{Event} callback (typed events auto-surface the whole args as Action<A>) |
Two-way auto-pairing keys strictly on the `{P}Changed` name. Controls whose change event breaks that convention (ToggleSwitch.IsOn↔Toggled, Segmented.SelectedIndex↔SelectionChanged) stay one-way until you add [WrapControlled].
Annotation cheat-sheet (opt-in, when inference isn't enough)
All live in Microsoft.UI.Reactor.Wrappers. All are repeatable unless noted.
| Attribute | Use it to… | |
|---|---|---|
[GenerateReactorWrapper(typeof(T), Exclude/Include/AutoDiscover)] | wrap T; prune noisy inherited props (Exclude = new[]{ "CommandParameter" }) or switch to explicit opt-in (AutoDiscover = false, Include = new[]{ … }). | |
[WrapControlled("P", ChangedEvent = "…")] | make P two-way when its event isn't {P}Changed. Use Events = new[]{ "Checked","Unchecked" } for multi-event values (read back after any fire). Deferred = true for deferred/coercing string boxes (PasswordBox.Password, AutoSuggestBox.Text). A control has one controlled slot — two [WrapControlled] on one control warns REACTORGEN012. | |
[WrapOneWay("P")] | opt a prop out of two-way auto-pairing (display-only despite a {P}Changed, e.g. ProgressBar.Value). | |
[WrapAlias("FriendlyName", "ControlProperty")] | surface a control prop under a nicer name ("Min"→"Minimum"). Aliasing the content property turns it into a string value prop (no child slot). | |
[WrapContent("Prop")] | override which property is the content: child slot. | |
[WrapConvert("Prop")] | surface a struct prop (CornerRadius, Thickness) as an ergonomic scalar via its single-arg constructor (double? → new CornerRadius(v)). | |
[WrapEvent("Foo", Arg = "ErrorMessage")] | project event args into a typed On{Event}(args.Arg) callback; Args = new[]{ … } for multi-param. | |
[WrapLifecycle(nameof(Start), OnUnmounted = nameof(Stop))] | run a static void M(TControl) on mount/unmount for imperative controls (CameraPreview.StartAsync) — no call-site boilerplate. | |
[WrapPanelChildren(PerChild = …)] | wire per-child attached props (Grid.SetRow, Canvas.SetLeft) onto a panel; AfterAll for two-pass (RelativePanel). | |
[WrapManual("P")] + static partial Customize(d) | hand-chain one entry the generator can't infer while it still generates the rest. | |
[WrapElementSlot("Prop", ControlProperty = "…")] | auto-wire a secondary single-element slot that writes a dedicated control property (TabView.TabStripHeader, SettingsCard.HeaderIcon) — mounts via ctx.MountChild, updates via state-preserving ctx.ReconcileChild. Use ControlProperty= when the element-facing name differs from the control property. The control property must be public-settable and object/UIElement-typed. For the primary child use [WrapContent]; for a slot that shares a control prop with a string fallback (Expander.Header) stay manual. | |
[WrapPolymorphic(nameof(Resolve))] / [WrapDecorator(nameof(Create))] | controls whose concrete type is chosen at runtime, or that need a fully custom mount/update/unmount lifecycle (emit an IDecoratorElementHandler, not a descriptor). |
Imperative control (lifecycle) — paste-ready
// CameraPreview must be StartAsync'd after mount and Stop'd on unmount. Declared// ONCE here, so every call site is a plain declarative `CameraPreview(...)`.[GenerateReactorWrapper(typeof(CommunityToolkit.WinUI.Controls.CameraPreview))][WrapEvent("PreviewFailed", Arg = "Error")][WrapLifecycle(nameof(StartPreview), OnUnmounted = nameof(StopPreview))]public partial record CameraPreviewElement{private static async void StartPreview(CommunityToolkit.WinUI.Controls.CameraPreview cp){try { await cp.StartAsync(cp.CameraHelper); } catch { /* surfaced via PreviewFailed */ }}private static void StopPreview(CommunityToolkit.WinUI.Controls.CameraPreview cp){try { cp.Stop(); } catch { /* already torn down */ }}}
Non-conventional two-way event
// SelectedIndex changes through SelectionChanged, not SelectedIndexChanged.[GenerateReactorWrapper(typeof(CommunityToolkit.WinUI.Controls.Segmented))][WrapControlled("SelectedIndex", ChangedEvent = "SelectionChanged")]public partial record SegmentedElement;// → Segmented(selectedIndex: idx, onSelectedIndexChanged: setIdx, items: …)
Hand-tuning a single generated member
Write the member yourself in your half of the partial; the generator skips any member you declare. Example — treat Header as templated Reactor content instead of plain text:
public partial record SettingsCardElement{public Element? Header { get; init; }}
Diagnostics
REACTORGEN001 (target isn't a non-static FrameworkElement with a public parameterless ctor) · …002 (bad Include/Exclude name) · …003/…004 (WrapControlled property / change-event) · …005 (WrapAlias control property) · …006 (WrapOneWay) · …007 (WrapContent) · …008 (WrapConvert) · …009/…010 (WrapEvent target / arg) · …011 (WrapLifecycle method) · …012 (two controlled props share one control) · …013 (WrapElementSlot control property missing/not-settable — set ControlProperty=) · …014 (WrapElementSlot control property not UIElement-assignable) · …015 (WrapElementSlot slot name not a valid identifier). Run mur check <path> — it surfaces these with skill pointers.
When to drop to a hand-written descriptor
The generator targets the common case. Reach for the manual ControlDescriptor / IElementHandler path (docs/guide/extending-reactor-controls.md, registered via ControlRegistry.RegisterHandler) only when a control's contract is something the attributes above can't yet express — and prefer adding a reusable [Wrap*] capability over a one-off handler when the shape is general.