reactor-wrapping
Skill839 starsChanged 6 days agoMITView on GitHub
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.
In this repository23 other files it gives its agents.
- AGENTS.mdAGENTS.md
- analyzer-dymSkill
- coverage-upliftSkill
- perf-compareSkill
- pr-lifecycleSkill
- pr-reviewSkill
- reactor-advancedSkill
- reactor-asyncSkill
- reactor-build-and-checkSkill
- reactor-chartsSkill
- reactor-commandingSkill
- reactor-designSkill
- reactor-devtoolsSkill
- reactor-dockingSkill
- reactor-dslSkill
- reactor-formsSkill
- reactor-getting-startedSkill
- reactor-inputSkill
- reactor-navigationSkill
- reactor-recipesSkill
- reactor-windowingSkill
- reactor-appSkill
- ChartingSkill
The file
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 8
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 …
- Very specific: line 68
Keep the record partial (the generator fills the other half) and in a namespace you control.
- Very specific: line 93
All live in Microsoft.UI.Reactor.Wrappers.
- Very specific: line 143
Write the member yourself in your half of the partial.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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 attributes
namespace 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 controlThe 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.
The generator emits ReactorApp.TryRegisterControlAssembly(typeof(X).Assembly)
in the element's static constructor to discover a control library's metadata
before first realization (spec 058 §9a). The Try call quietly returns false
if the provider has been trimmed; a 0xC000027B crash when the wrapped control
is first realized is a symptom that explicit registration may be needed.
For code-only Native AOT apps, implicit discovery of the toolkit provider can be
trimmed. Explicitly construct and register its generated XamlMetaDataProvider
before realization when needed; see docs/aot-support.md. The observable Items
collection's generated WinRT exposure does not root toolkit metadata.
Requirements & setup
- Reference
Microsoft.UI.Reactor(carries the generator inanalyzers/dotnet/cs/and the authoring attributes inReactor.Wrappers.Abstractions.dll). - Reference the control's own package (e.g. the WCT
CommunityToolkit.WinUI.Controls.*packages). - The target must be a non-static
FrameworkElementwith a public parameterless constructor (elseREACTORGEN001). - 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; incremental reconciliation, .WithKey(...) preserves state across reorders, and writable IList<object> sinks get an observable list on mount |
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, and terminally unmounts the child before detaching it, independently of the descriptor author's OnUnmount. When no slot element is declared, the control's constructor default is left untouched. 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.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

