Derived from: docs/specs/039-property-and-event-scrub.md
Scope reminder: spec 039 is an audit. This task list converts every gap it identified into ship-ready work — new fluent extensions, missing events, missing properties, naming fixes, doc-comment corrections, named-style helpers, samples, guides, agent-kit skills, and tests. Tasks are sized to be paused/resumed; complete top-to-bottom within a phase. Cross-phase ordering matters (don't ship docs for fluents that aren't generated yet; don't update samples before the underlying API exists).
Conventions:
- All element records live under
src/Reactor/Core/Element.cs; factories insrc/Reactor/Elements/Dsl.cs; fluent modifiers insrc/Reactor/Elements/ElementExtensions.cs. New event fluents go insrc/Reactor/Elements/ElementExtensions.Events.cs; named-style fluents insrc/Reactor/Elements/ElementExtensions.NamedStyles.cs; new factories (e.g.Card, type-rampTitle/Subtitle/...) insrc/Reactor/Elements/Factories.NamedStyles.cs. - Spec section anchors are referenced in task bodies (e.g.
(spec §3.1)). - Public API additions need XML doc comments (no
CS1591). - Code must compile under
Reactor.slnxwarnings-as-errors. - New unit tests live under
tests/Reactor.Tests/. Public-API surface tests live undertests/Reactor.SelfTests/if that's the established pattern for surface scrubs (verify with a grep before adding the first one). - Samples for newly-exposed controls go under
samples/ReactorGallery/ControlPages/. - Agent-kit references live under
plugins/reactor/skills/reactor-dsl/references/andplugins/reactor/skills/reactor-recipes/references/; the human guide underdocs/guide/.
A task is "done" only when:
- Code compiles under
Reactor.slnxwarnings-as-errors. - Public API surface has XML doc comments (no
CS1591). - New unit tests cover the happy path and every documented failure mode.
- Accessibility analyzers (
REACTOR_A11Y_001..003) remain clean. - Doc + sample + agent-kit skill updates land in the same PR as the API change so the surface is discoverable the moment it ships.
- Confirm in the spec header that question answers are final and won't
change scope mid-implementation: (Q1) source-generated fluents preferred
over hand-written; (Q2)
.OnX(null)clears handler; (Q3) add fluent.AccentButton()/.SubtleButton(); (Q4)CalendarViewgets a distinct multi-select shape; (Q5) add fluent.NavigateUri(...)onHyperlinkButton. - If Q1 lands as "source-gen", add a one-paragraph note to the spec
pointing at the generator file path so future readers find it; if Q1
flips to "hand-written", strike the source-gen scaffolding task (0.4).
Resolution: hand-written. 0.4 struck. Spec header also documents
the C# property-vs-extension delegate-invocation clash discovered
during implementation — fluents drop the leading
On(e.g..Click(handler)sets propertyOnClick).
- Create empty
src/Reactor/Elements/ElementExtensions.Events.cs(namespace + emptypublic static partial class ElementExtensions). - Create empty
src/Reactor/Elements/ElementExtensions.NamedStyles.cs(same shape). - Create empty
src/Reactor/Elements/Factories.NamedStyles.cs(namespace +public static partial class Factories). - Verify the solution still builds with these empty partial files.
- Generate a CSV under
tests/Reactor.SelfTests/Fixtures/event-fluent-inventory.csvwith columnsElement, PropertyName, DelegateType, Source(spec §). Drive it from a one-off script (commit the script too undertools/api-scrub/) so the inventory regenerates as records evolve. Script:tools/api-scrub/Build-EventFluentInventory.ps1. - Cross-check the CSV count against the spec's "~60" estimate; reconcile any discrepancy by spot-checking spec §1–§9 and updating the spec if the count drifted. 68 rows; §1–§9 ≈ 60, remainder is §12 niche + §13 specialized — matches the "~60 across §1–§9" estimate.
-
Add a new projectStruck per Q1 resolution above (hand-written chosen).src/Reactor.Generators/Reactor.Generators.csproj -
Implement the generator…Struck. -
Generator must respect Q2…Struck. -
Snapshot-test the generator output…Struck.
This phase closes the spec's single biggest gap. Every callback property in the inventory from 0.3 gets a fluent extension. If 0.4 generator landed, this phase is mechanical verification; otherwise each item below is a hand-written one-line extension.
Naming note: all extensions below drop the leading On to avoid the
C# property-vs-extension clash (see Phase 0.1). Property OnClick is
reached via .Click(handler), OnTextChanged via .TextChanged(handler),
etc. Property names are unchanged.
-
Button.Click(Action? handler)(spec §2.1 — canonical example). -
HyperlinkButton.Click(Action? handler)(spec §2.2). -
RepeatButton.Click(Action? handler)(spec §2.3). -
ToggleButton.IsCheckedChanged(Action<bool>? handler)(spec §2.4). -
SplitButton.Click,ToggleSplitButton.IsCheckedChanged(spec §2.5). (DropDownButtonhas no click handler — fires its flyout instead.)
-
TextField.Changed(Action<string>? handler),TextField.SelectionChanged(...)(spec §3.1). -
PasswordBox.PasswordChanged(...)(spec §3.2). -
NumberBox.ValueChanged(Action<double>? handler)(spec §3.3). -
AutoSuggestBox.TextChanged,.QuerySubmitted,.SuggestionChosen(spec §3.4 —SuggestionChosenwas previously unreachable from factory; flag fixed in PR description). -
CheckBox.IsCheckedChanged(Action<bool>? handler)and.CheckedStateChanged(Action<bool?>? handler)(spec §3.5). -
RadioButton.IsCheckedChanged,RadioButtons.SelectedIndexChanged(spec §3.6). -
ComboBox.SelectedIndexChanged(Action<int>? handler)(spec §3.7). -
Slider.ValueChanged(Action<double>? handler)(spec §3.8). -
ToggleSwitch.IsOnChanged(Action<bool>? handler)(spec §3.9). -
RatingControl.ValueChanged(Action<double>? handler)(spec §3.10). -
ColorPicker.ColorChanged(Action<Windows.UI.Color>? handler)(spec §3.11). -
RichEditBox.TextChanged(...)(spec §1.4).
-
CalendarDatePicker.DateChanged,DatePicker.DateChanged,TimePicker.TimeChanged(spec §4). -
CalendarView.SelectedDatesChanged(...)— modelled in Phase 3.1; fluent landed alongside the event.
-
InfoBar.ActionButtonClick,.Closed(spec §5). -
Expander.IsExpandedChanged,SplitView.PaneOpenChanged(spec §6). -
NavigationView.SelectedTagChanged,.BackRequested(spec §7.2). -
TitleBar.BackRequested,.PaneToggleRequested(spec §7.3). -
TabView.SelectedIndexChanged,.TabCloseRequested,.AddTabButtonClick(spec §7.4). -
BreadcrumbBar.ItemClicked,Pivot.SelectedIndexChanged(spec §7.5).
-
ListView,GridView,TreeView,FlipView,ListBox,ItemsView<T>,TemplatedListView<T>,TemplatedGridView<T>,TemplatedFlipView<T>—.SelectedIndexChanged/.ItemClick/.ItemInvoked/.Expandingper spec §8. -
ContentDialog.Closed(Action<ContentDialogResult>? handler)(spec §9). -
Flyout.Opened,.Closed;TeachingTip.ActionButtonClick,.Closed;Popup.Closed(spec §9). -
WebView2.NavigationStarting(Action<Uri>?),.NavigationCompleted(Action<Uri>?)(spec §10). -
SelectorBar.SelectedIndexChanged,PipsPager.SelectedPageIndexChanged,RefreshContainer.RefreshRequested(spec §12).
- Add one unit test per fluent that confirms passing
nullclears any previously-set handler and that re-applying restores it. Test filetests/Reactor.Tests/Elements/EventFluentNullClearTests.cs— 63 facts, one per fluent.
-
.AccentButton()onButtonElementand any subtype declared in the record hierarchy (DropDownButtonElement,SplitButtonElement,ToggleSplitButtonElement). UsesAccentButtonStylestatic resource. -
.SubtleButton()likewise (SubtleButtonStyle). - Both helpers work via the WinUI
ThemeResourceplumbing (style is resolved at mount time throughApplication.Current.Resources, which re-evaluates on light/dark/contrast switches). - Unit test: applying
.AccentButton()then.SubtleButton()keeps the last-write-wins behavior;.ApplyStyle("...")after either still overrides. (NamedStyleFluentTests.)
-
.TextLink()onHyperlinkButtonElementandButtonElementusingTextBlockButtonStyle. - Sample: replace one inline
"Learn more".ApplyStyle(...)call insamples/ReactorGallery/with.TextLink()and confirm visual parity. (Landed in Phase 8.1 —Learn morebutton onBasicInput/ButtonPage.)
- On
TextFieldElement:.NumericInput(),.EmailInput(),.UrlInput(),.PhoneInput(),.SearchInput(). Map to the exactInputScopeNameValuemembers enumerated in spec §17.3. - Generic escape hatch:
.InputScope(InputScopeNameValue scope)onTextFieldElement. - Scoped to
TextFieldElementonly —PasswordBoxElementwould need a separate review of the WinUI behavior around password-input scopes (aPasswordinput scope can disable the soft keyboard's password suggestion UI). Not in this PR. - Unit test: each fluent attaches a setter on
TextFieldElement.Setters. (NamedStyleFluentTests.)
-
.Informational(),.Success(),.Warning(),.Error()onInfoBarElement, mapping 1:1 toInfoBarSeveritymembers. - Unit test: each fluent flips the severity; last-write-wins.
(
NamedStyleFluentTests.)
- Add
Card(Element child)inFactories.NamedStyles.csreturning a presetBorderElementwith: corner radius 8, padding 16,CardBackgroundFillColorDefaultBrush,CardStrokeColorDefaultBrush1px stroke — all sourced via the existingThemeRef/Core.Themeplumbing, not literals. -
Helper— not needed;ResolveCardBackground()/ResolveCardStroke()Background(ThemeRef)/WithBorder(ThemeRef)already route through one place (BrushHelper). - Confirm chaining:
Card(child).Padding(24).CornerRadius(16)override-wins. (NamedStyleFluentTests.) - Light/Dark/HighContrast visual smoke under
tests/Reactor.AppTests/(mount fixture, snapshot dimensions and resolved brush keys — not pixels). Deferred — requires the app test harness; tracked for Phase 11.4.
- Add
Title,Subtitle,Body,BodyStrong,BodyLargefactories alongsideHeading/SubHeading/CaptioninFactories.NamedStyles.cs, each returning a presetTextBlockElementwith the WinUI 3 type ramp values (TitleTextBlockStyle,SubtitleTextBlockStyle,BodyTextBlockStyle,BodyStrongTextBlockStyle,BodyLargeTextBlockStyle). - Do NOT also add fluents for these — spec §17.6 explicitly warns against the two-ways trap. Confirmed: factories only.
- Unit test: each factory attaches a mount action that applies the
corresponding style. (
NamedStyleFluentTests.)
- Decide the shape of the multi-selection callback. Spec Q4 OK's "a
distinct API for CalendarView". Chose:
Action<IReadOnlyList<DateTimeOffset>>? OnSelectedDatesChanged— snapshot of the full selection (not delta) so component state binds without diffing. - Add a
SelectedDatesinit property (IReadOnlyList<DateTimeOffset>?) so initial selection is set declaratively, not via.Set(). - Wire the event in the
CalendarViewreconciler hook. Subscription is unconditional; the handler reads the latest element via the element-tag, andSyncSelectedDatesusesChangeEchoSuppressorto keep declarative reconciliation from echoing as user events. - Add
.SelectedDatesChanged(...)/.SelectedDates(...)fluents (deferred from 1.3). - Unit test: null-clear (
EventFluentNullClearTests). Reconciler- level "programmatic selection changes raise the callback exactly once per atomic change; no event during initial mount" deferred to AppTests harness (tracked in Phase 11.7).
- Model the three events on
FrameElementas Reactor-shaped substitutes (matching the rest of the codebase, which doesn't surface raw WinUI EventArgs in user-facing callbacks):Action<Type>? OnNavigated,Action<Type>? OnNavigating,Action<Type, Exception>? OnNavigationFailed. Users who needNavigationMode/NavigationParameter/ cancellation can wire the raw event via.Set(...). - Fluent extensions:
.Navigated(...),.Navigating(...),.NavigationFailed(...). - Sample exercising
Frame.Navigatedfor a navigation log undersamples/ReactorGallery/ControlPages/Navigation/. (Landed in Phase 8.3 —FrameNavigationPage+ three trivial Page-derived targets inFrameSamplePages.cs. All three fluents.Navigating/.Navigated/.NavigationFailedare wired into a 20-entry rolling log.)
- Add
OnViewChanged(Action<ScrollViewerViewChangedEventArgs>?)toScrollViewElement. Used the literal spec-shaped signature (Action<EventArgs>?, no sender) —OnSizeChangeddoes pass sender but Reactor users rarely need it; the args carryIsIntermediatewhich is the more useful debouncing hook. - Fluent
.ViewChanged(...)+ null-clear unit test. - Pooled-control wiring: subscribe-once via
PoolableWireFlags.ScrollViewerViewChanged; the handler reads the live element viaGetElementTagso a later record-with that attachesOnViewChangedpicks up without re-wiring. - AppTest: "triggering a scroll fires the callback with monotonic offsets" — deferred to Phase 11.7 (requires a WinUI runtime).
- Add
OnOpenedtoPopupElement; already hasOnClosed. Fluent.Opened(handler)+ null-clear test. Trampoline reads live element via the wrapper StackPanel's tag, matching the existingOnClosedpattern.
- Add
OnWebMessageReceived(Action<string>?)andOnCoreWebView2Initialized(Action?)toWebView2Element. Fluents.WebMessageReceived(...)/.CoreWebView2Initialized(...)+ null-clear tests. Threading contract documented in XML doc comments (both fire on UI thread). -
OnWebMessageReceivedhandler falls back fromTryGetWebMessageAsString()toWebMessageAsJsonso callers always receive a string payload even when the page sends a structured message viapostMessage(...).
- Wire the three events from the inner
MediaPlayerto the element record. The MediaPlayer raises these on a worker thread; new helperDispatchToElement<TElement>marshals via the element'sDispatcherQueue.TryEnqueueand resolves the live element viaGetElementTag(handler is a no-op if the element is unmounted between fire and dispatch). Documented in XML doc comments. -
OnMediaFailedreceives the error message string (args.ErrorMessage ?? args.Error.ToString()) — keeps the callback signature simple while still preserving the most-useful failure detail. - Fluents
.MediaOpened(...)/.MediaEnded(...)/.MediaFailed(...)+ null-clear tests. Integration test ("fires-on-real-media") deferred to Phase 11.7 (AppTests harness).
Each item below is [deferred] to a follow-up spec (tracked in
docs/specs/040-specialized-control-scrub.md). Rationale: each surface
needs its own event-shape design pass (e.g. MenuFlyout.Opening exposes
a cancellable args object Reactor doesn't yet have a pattern for; MapControl
isn't packaged in this Reactor build at all). Bundling these into 039
would balloon the PR past review-budget without proportional user value.
- [deferred]
MenuFlyout.Opening/MenuFlyout.Closing— cancellable args (OpeningexposesCancelEventArgs-shaped data); needs a Reactor-shaped cancellation pattern that doesn't exist elsewhere in the codebase yet. Tracked in spec 040. - [deferred]
CommandBar.IsOpenChanged—CommandBaris itself niche in the current sample/app surface (no current consumer); add alongside any futureCommandBarwork. Tracked in spec 040. - [deferred]
SemanticZoom.ViewChangeStarted/SemanticZoom.ViewChangeCompleted—SemanticZoomis not currently modelled as a Reactor element record; modelling the control is a prerequisite. Tracked in spec 040. - [deferred]
AnnotatedScrollBar.DetailLabelRequested—AnnotatedScrollBaris a WinUI 3 1.5+ primitive not yet exposed as a Reactor element. Tracked in spec 040. - [deferred]
MapControl.ViewChanged—MapControllives in the Windows Maps SDK which is not a dependency of this Reactor build; out of scope for 039 entirely.
-
.Orientation(Orientation),.TickFrequency(double),.TickPlacement(TickPlacement),.SnapsTo(SliderSnapsTo),.ThumbToolTip(bool enabled)(spec §3.8).
-
.NumberFormatter(INumberFormatter2),.AcceptsExpression(bool = true),.ValidationMode(NumberBoxValidationMode),.Description(string)(spec §3.3).
-
.AlphaEnabled(bool = true),.MoreButtonVisible(bool = true),.ColorSpectrumVisible(bool = true),.ColorSliderVisible(bool),.ColorChannelTextInputVisible(bool),.HexInputVisible(bool)(spec §3.11). One fluent per init property — all six.
-
.TabWidthMode(TabViewWidthMode),.CloseButtonOverlayMode(TabViewCloseButtonOverlayMode),.CanDragTabs(bool = true),.CanReorderTabs(bool = true),.AllowDropTabs(bool = true),.TabStripHeader(Element),.TabStripFooter(Element)(spec §7.4).
-
.AutoSuggestBox(AutoSuggestBoxElement),.PaneFooter(Element),.PaneCustomContent(Element),.OpenPaneLength(double),.CompactModeThresholdWidth(double),.ExpandedModeThresholdWidth(double)(spec §7.2).
-
.BackButtonVisible(bool),.BackButtonEnabled(bool),.PaneToggleButtonVisible(bool),.Content(Element),.RightHeader(Element),.Icon(IconSource | string)(spec §7.3).
-
.MaxLength(int),.IsSpellCheckEnabled(bool = true),.CharacterCasing(CharacterCasing),.TextAlignment(TextAlignment),.Description(string)(spec §3.1).InputScopelives in Phase 2.3.
- Implement
.NavigateUri(Uri)fluent onHyperlinkButtonElement— Q5 decided to add the API, not just rewrite the doc. - Update the XML doc on
Button(Command)/HyperlinkButton(Command)that promised.NavigateUri(...). The promise is now real; just verify the wording matches actual behavior.
These are properties the spec called out by name as high-traffic gaps that aren't covered by Phase 4 fluents. Some are net-new init properties on the element record (with corresponding fluents); some are pure fluents over existing init properties.
-
TextBlockElement:LineHeight,MaxLines,CharacterSpacing,TextDecorationsas init properties + fluents (spec §1.1). -
RichTextBlockElement:MaxLines,LineHeight,TextAlignment,TextTrimming,CharacterSpacing(spec §1.3). -
RichEditBoxElement:IsSpellCheckEnabled,MaxLength,TextWrapping,AcceptsReturn,SelectionHighlightColorinit + fluents (spec §1.4).
-
ToggleButton: nullableIsChecked+IsThreeStateso a single record covers two/three-state, matching WinUI (spec §2.4). Verify the existingThreeStateCheckBoxprecedent — does it split or unify? Match whichever pattern the rest of Reactor uses. Implemented as: addedIsThreeState/CheckedState/OnCheckedStateChangedonToggleButtonElement+ThreeStateToggleButton(...)factory, matching the existingCheckBoxElement/ThreeStateCheckBoxprecedent.
-
PasswordBoxElement:MaxLength,Header,PasswordRevealMode,PasswordChar(spec §3.2). -
AutoSuggestBoxElement:Header,QueryIcon,IsSuggestionListOpen(spec §3.4). -
ComboBoxElement:MaxDropDownHeight,Description,OnDropDownOpened,OnDropDownClosed(spec §3.7). -
RatingControlElement:PlaceholderValue,InitialSetValue; promoteCaptionto a fluent (spec §3.10). -
ColorPickerElement:ColorSpectrumShape,MinHue/MaxHue,MinSaturation/MaxSaturation,MinValue/MaxValue(spec §3.11).
-
CalendarDatePicker:DateFormat,IsTodayHighlighted,IsCalendarOpen,IsGroupLabelVisible(spec §4.1). -
DatePicker:DayFormat,MonthFormat,YearFormat,Orientation(spec §4.1). -
CalendarView:MinDate,MaxDate,FirstDayOfWeek,NumberOfWeeksInView,DisplayMode(spec §4.1;SelectedDates*handled in Phase 3.1).
-
InfoBarElement:IconSource,Content(Element child) (spec §5). -
WrapGridElement: attached-prop fluents.WrapGridColumnSpan(int)/.WrapGridRowSpan(int)(spec §6). -
ExpanderElement:HeaderTemplate(Element-typed slot),ContentTransitions(spec §6). -
SplitViewElement:PaneBackground(Brush or theme-resource key),LightDismissOverlayMode(spec §6).
-
ListView/GridView:ItemContainerStyle,IncrementalLoadingTrigger. (deferred:GroupStyle— needs a Reactor-shaped wrapper; tracked separately.) -
ContentDialogElement:IsPrimaryButtonEnabled,IsSecondaryButtonEnabled,OnOpened,OnOpening(spec §9). LandedIsPrimaryButtonEnabled,IsSecondaryButtonEnabled, andOnOpened(with.Opened()fluent). WinUI 3ContentDialogdoesn't expose anOpeningevent —OnOpeningdeferred upstream. -
FlyoutElement:ShowMode,AreOpenCloseAnimationsEnabled,OverlayInputPassThroughElement(spec §9). -
TeachingTipElement:IconSource,HeroContent,PlacementMargin,PreferredPlacement(spec §9).
-
ImageElement:OnImageOpened,OnImageFailed,NineGrid(spec §10). -
WebView2Element: surfaceSourceas a non-init-only init property (re-init via record-with) (spec §10). Already covered:Sourceis the record's positional parameter andUpdateWebView2reconciles it across renders. -
PathElement:StrokeStartLineCap,StrokeEndLineCap,StrokeLineJoin,StrokeMiterLimit,StrokeDashCap,StrokeDashOffset,FillRule(spec §11).FillRulelives onPathGeometry, notShapes.Path, so the reconciler propagates it throughp.Data as PathGeometry. -
PipsPagerElement:WrapMode,MaxVisiblePips,PreviousButtonVisibility,NextButtonVisibility(spec §12). -
RefreshContainerElement:PullDirection(spec §12). -
ParallaxViewElement:Source,VerticalSourceStartOffset/EndOffset(spec §12).
- Decide the multi-selection event shape for
ListView/GridView/ListBox/TreeView. Spec §8 calls this out as a "universal gap" and a follow-up. If a typedIReadOnlyList<int>(orIReadOnlyList<T>for typed peers) callback is acceptable, add it; otherwise punt to a tracked deferral with a checkbox[deferred]. Landed snapshot-styleOnSelectionChanged: -ListView/GridView/ListBoxreceiveAction<IReadOnlyList<int>>?-TemplatedListView<T>/TemplatedGridView<T>/ItemsView<T>receiveAction<IReadOnlyList<T>>?(typed peers materialize items from indices) -TreeView(deferred): hierarchical selection model needs separate design Snapshot semantics match the CalendarViewOnSelectedDatesChangeddecision from Phase 3.1.
- Rename
RichTextfactory +RichTextBlockElementreferences so factory + record + WinUI name align. KeepRichTextas an[Obsolete]forwarding alias for one release per the §16.4 (B) pattern. (Record was alreadyRichTextBlockElement; rename was factory-only: bothRichText(string)andRichText(RichTextParagraph[])now live asRichTextBlock(...)with thin[Obsolete]forwarders.) - Update the ~ten sample / doc / test call sites to the new name.
- Add a CHANGELOG entry under the breaking-change-deferred section.
(Created new
### Breaking changes (deferred)section under## [Unreleased]inCHANGELOG.md.)
- Add XML doc comments to
VStack/HStack/Heading/SubHeading/Caption/Flex/FlexRow/FlexColumn/LazyVStack<T>/LazyHStack<T>/TemplatedListView<T>explaining why the name diverges from WinUI. Also covered the sibling templated peersGridView<T>andFlipView<T>and the secondFlex(direction, ...)overload. (<summary>+<remarks>per factory inDsl.cs.)
- Add an XML doc remark on
StackElement.Spacingcalling out the default-8 vs WinUI's default-0. Doc-only — default unchanged.
- Confirm spec §16.5 decision (don't rename) is reflected in code by
adding a one-line XML doc on the
TextFieldfactory pointing readers to §3.1 / §16 in the spec for naming rationale. - Same for
MaskedTextField(spec §13 / §16.3).
- Decision: keep
ScrollView. NoScrollViewerfactory alias. Reactor'sScrollViewElementreconciles to WinUI's legacyScrollViewer, but WinUI 3 also ships the newerMicrosoft.UI.Xaml.Controls.ScrollView— the shorter, modern name. Reactor'sScrollViewis already consistent with that preferred WinUI name. Originally added an[Obsolete]ScrollViewerforwarding alias for discoverability, but the PR #314 review (Copilot) flagged that the alias is not purely additive: underusing static Microsoft.UI.Reactor.Factories;alongsideusing Microsoft.UI.Xaml.Controls;, the bare nameScrollViewerthen shadows the WinUI type, so any existing code calling its attached properties (ScrollViewer.SetVerticalScrollMode(...)etc.) has to fully-qualify withglobal::Microsoft.UI.Xaml.Controls.ScrollViewer.The discoverability win didn't justify the imposed disambiguation cost on existing consumers — alias removed.
- Decision: keep
Progress/ProgressIndeterminate, add[Obsolete]ProgressBar/ProgressBar()aliases. Reactor names by intent (Progress) rather than by rendering shape (ProgressBarincludes the visual primitive in the name). Aliases let WinUI muscle-memory callers discover the Reactor spelling without breaking code.
This phase is intentionally scoped — the spec defers full coverage to spec 040, but small follow-on items belong here.
- Create
docs/specs/040-specialized-control-scrub.mdas a draft with the same three-checks-per-control structure as 039, applied toAutoSuggestElement<T>,DataGridElement<T>,MaskedTextFieldElement,PropertyGridElement,VirtualListElement. (Draft scaffold only — Status: Draft — does not pre-audit the controls.)
- Inventory specialized controls' callback properties using the same
script from 0.3 (extend it to cover
src/Reactor/Controls/**). (The script already globssrc/Reactor/Controls/**. Re-ran and regeneratedtests/Reactor.SelfTests/Fixtures/event-fluent-inventory.csv. Also broadened the delegate-type regex to capture one level of nested generics soAction<IReadOnlySet<RowKey>>?onDataGridElement<T>is now picked up.) - Land fluent extensions for any callback that does not already have
one. Phase 0.4 (source generator) was struck (hand-written chosen),
so this is purely a hand-written sweep.
-
AutoSuggestElement<T>.OnSelected— covered (Phase 1). -MaskedTextFieldElement.OnChanged— covered (Phase 1). -PropertyGridElement.OnRootChanged— covered (Phase 1). -VirtualListElement.OnVisibleRangeChanged— covered (Phase 1). -DataGridElement<T>.OnSelectionChanged— new fluent.SelectionChanged(...)added; null-clear test added.
Samples document the new surface in-tree and double as smoke tests.
- In
samples/ReactorGallery/ControlPages/BasicInput/, add a sample page or update an existing one to useButton("Save").Click(...)/.AccentButton()/.SubtleButton()/.TextLink(). (Phase 1 renamed the fluent to drop the leadingOn—.Click, not.OnClick. The underlying property remainsOnClick.) - In
BasicInput/TextField, demo.NumericInput()/.EmailInput()/.UrlInput()next to aDescription("...")fluent. - In
StatusAndInfo/InfoBar, demo all four severity fluents. - In
Text, add a "Type ramp" page usingTitle/Subtitle/Body/BodyStrong/BodyLarge. (New pageText/TypeRampPage.csregistered inControlRegistry/PageRouter.) - In
Layout, add aCard(child)example with three nested children (icon, heading, body) so the theme-aware resolution is visible. (New pageLayout/CardPage.csregistered.) - In
DateAndTime/CalendarView, demo.SelectedDatesChanged(...)+ multi-select. (Fluent drops the leadingOn; property remainsOnSelectedDatesChanged.)
-
samples/apps/chat: replace any.Set(c => c.OnClick = ...)with the new.Click(...)fluent. Grep first; no-op if none found. (No-op — grep showed zero matches. The chat app's.Set(...)calls are all theme-resource templating (.Set("ButtonBackground", ...)) or style-only WinUI knobs (Padding,TextWrapping, etc.), none of them callback properties.) -
samples/apps/demo-script-tool: same sweep; replace.Set()escape-hatches that are now expressible with fluents. (No-op — only.Set("ButtonForeground...", ThemeRef(...))hits.) -
samples/apps/regedit/wordpuzzle/validation-showcase/a11y-showcase: same sweep. (All four: no-op. Property-initOnClosed = .../OnItemInvoked = ...patterns in regedit are already inwith { ... }record-construction blocks, not inside.Set(...)escape-hatches, so the fluent swap doesn't apply.)
- Under
samples/ReactorGallery/ControlPages/Navigation/, demonstrateFrame.Navigated/.Navigating/.NavigationFailedwith a log panel showing each event. (Fluents drop the leadingOn; the underlying properties remainOnNavigatedetc.) Landed asNavigation/FrameNavigationPage.cs+Navigation/FrameSamplePages.cs.
-
docs/guide/getting-started.md: replace any.Set(c => c.OnClick = ...)examples with the new fluent. (No-op — the guide already uses theButton("…", onClick)factory shape throughout; grep confirmed zero.Set(c => c.OnX = ...)and zerowith { OnX = ... }patterns.) -
docs/guide/forms.md: cover.NumericInput()/.EmailInput()/.MaxLengthetc. as the canonical TextField configuration story. (New "Configuring TextField" section with worked example + lookup table covering Phase 2.3 named-input shapes + Phase 4.7 properties.) -
docs/guide/styling.md: add a section on named-style fluents (AccentButton,SubtleButton,TextLink, severity helpers). (New "Named-Style Fluents" section sits before the existing "Lightweight Styling" block.) -
docs/guide/layout.md: add aCard(child)example and the type-ramp factories. (Two new sections: "Card" and "Type-Ramp Factories", landed before "ScrollView and Border".) -
docs/guide/navigation.md: coverFrameevents,NavigationView.OnSelectedTagChangedfluent. (New sections "Frame Events" and "NavigationView.SelectedTagChanged" land just before "Navigation Diagnostics".) -
docs/guide/collections.md: cover any new collection events from Phase 5.8. (New "Multi-Select with SelectionChanged" section covers ListView / GridView / ListBox + typed peers; calls out theIReadOnlyList<int>vsIReadOnlyList<T>signature split and the intentional TreeView deferral.)
- If
docs/reference/contains an auto-generated API surface, regenerate. (No auto-gen system found underdocs/reference/— the only machine-derived API surface isskills/reactor.api.txtand its mirror atplugins/reactor/skills/reactor-dsl/references/reactor.api.txt, both regenerated on every Reactor build. No manual action needed here; agent-kit regen is Phase 10.1's scope.) -
docs/guide/xaml-developers.md: explicitly contrastOnClickattribute (XAML) →.OnClick(handler)(Reactor fluent) so XAML developers find the bridge. (New "Events: XAML Attributes Become Fluents" section with the canonical mapping table forClick,TextChanged,SelectionChanged,SelectedIndexChanged,IsCheckedChanged+ the property-vs-fluent naming note.)
- In
docs/specs/039-property-and-event-scrub.md, append a "Status: Implemented" header pointing at this task list and the merge PR(s). (Header replaced; backlinks to this task list and the per-phase commit history viagit log feat/039-property-event-scrub --grep='Phase '.)
- Add entries to
CHANGELOG.mdgrouped: "New fluent extensions" (one bullet listing the ~60), "Named-style helpers", "New events exposed", "Naming aliases (RichText→RichTextBlock)". (Single nested bullet block under a new### Addedheading in[Unreleased]covering all four buckets — Naming aliases stayed in the existingBreaking changes (deferred)block where Phase 6.1 originally landed them.)
The agent-kit is how AI coding assistants discover Reactor APIs. Every new fluent must surface here or it doesn't exist for agents.
- Regenerate
plugins/reactor/skills/reactor-dsl/references/reactor.api.txtfrom the new public surface so allOn..., named-style, and new init-property fluents are listed. (Auto-regenerated bytools/Reactor.SignaturesGenon every Reactor build; current copy is up-to-date —dotnet build src/Reactor/Reactor.csprojproduced no diff. Note: only theplugins/reactor/skills/reactor-dsl/references/copy is updated by the build; the legacyskills/reactor.api.txtmirror is not auto-regenerated — Phase 12 should reconcile.) - Update
plugins/reactor/skills/reactor-dsl/SKILL.mdif it documents naming conventions for fluents — clarify that callback properties have matching.OnX()extensions. (New "Fluent naming convention (callbacks)" section explains theOnXproperty →.X(...)fluent drop, the C# clash rationale, and the null-clear semantic.)
- Update existing recipes to use the new fluents where they currently
use
.Set(c => c.OnX = ...): -themed-card.cs— now uses theCard(child)factory + type-rampSubtitle/Captionfor headings. -form-with-validation.cs— adds an Age field demonstrating.NumericInput()+.MaxLength(3), applies.EmailInput()to the email field, header note covers the.Changed(...)fluent and its replace-vs-append semantic. -list-add-delete.cs— converted from manual VStack to templatedListView<T>(items, key, builder).ItemClick(...); per-row keying is now factory-supplied. -sidebar-nav.cs— header note + inline comment explain the.SelectedTagChanged(...)fluent and why.WithNavigationand.SelectedTagChangedare mutually exclusive on the same element. - Add new recipe
plugins/reactor/skills/reactor-recipes/references/named-styles.csshowingAccentButton/SubtleButton/TextLink/severity fluents side-by-side. - Add new recipe
plugins/reactor/skills/reactor-recipes/references/calendar-multiselect.csdemonstratingOnSelectedDatesChanged. - Update
plugins/reactor/skills/reactor-recipes/references/index.mdto reference the new recipes. - Mirror the changes in the legacy
skills/recipes/folder if it's still consumed (grep references; deprecate if not). (Legacy folder IS still consumed —src/Reactor/Reactor.csprojlines 99–102 packskills/*.mdandskills/recipes/*.{md,cs}toagentkit/skills/in the NuGet. All four updated recipes + both new recipes copied across; legacyindex.mdupdated. Phase 12 should reconcile the duplicate-layout source-of-truth question.)
- If
reactor-designreferences the typography ramp or card pattern, update it to point atTitle/Subtitle/Body/Card(...)instead of.Set()recipes. (Typography table extended with the WinUI 3 ramp factoriesTitle/Subtitle/Body/BodyStrong/BodyLargewith note disambiguating from the olderHeading/SubHeadingReactor presets; the "Avoid Deep Nesting" example gains aCard(...)one-liner. Mirror update applied to legacyskills/design.md.)
- Audit the SKILL.md for any
.Set(c => c.OnClick = ...)usage; swap for fluent. (No-op: grep showed zero.Set(c => c.On*)orwith { On* = ... }patterns inplugins/reactor/skills/reactor-getting-started/SKILL.md.)
- Audit for
TextField/NumberBox/PasswordBoxexamples; swap for.NumericInput()/.EmailInput()/.MaxLength()etc. (Available modifiers table extended forTextFieldandPasswordBox; new "Named-input shapes" callout with a worked.EmailInput()+.MaxLength()+.Validate()chain.)
- Audit for newly-fluent events (
.OnClick,.OnSelectedTagChanged,.OnRefreshRequested); swap from the property-init pattern. (No-op for all three: grep showed zero.Set(c => c.On*),with { On* = ... }, or property-init callback patterns in any ofreactor-commanding,reactor-navigation,reactor-inputSKILL.md files. The reactor-navigation skill talks aboutUseNavigationLifecyclehook callbacks, not raw NavigationView events.)
- Update if it lists factories — add type-ramp factories and
Card. (RootSKILL.mdcheatsheet: type-rampTitle/Subtitle/Body/BodyStrong/BodyLargeline added next to the existingHeading/SubHeading, plus aCard(child)line above the manualBorder(child).Background(Theme.CardBackground)...example. Recipe roll-call now listscanvas-positioning,named-styles,calendar-multiselect.skills/dsl-reference.mdtypography table gains the five WinUI ramp factories; layout-containers table gains aCard(child)row.) - Update
skills/forms.md/skills/input.md/skills/navigation.mdmirrors of the items in 10.5 / 10.6. (skills/forms.mdavailable- input-types table extended forTextFieldandPasswordBoxto match thereactor-formsupdate.skills/input.md/skills/navigation.md/skills/commanding.md: no-op — same grep-clean result as 10.6.)
Most phases above include their own targeted tests; this phase covers the cross-cutting test surface.
- Add a self-test under
tests/Reactor.SelfTests/that walks everyElementrecord reflectively and asserts: (a) everyAction/Action<T>property has a matching public extension method whose name equals the property name and whose first parameter is the element type; (b) the extension exists inElementExtensions.Events.csor is source-generated. This guards against future records adding a callback without a fluent. (Landed as xUnitPublicApiSurfaceGuardTests.EveryCallbackPropertyHasMatchingFluentundertests/Reactor.Tests/Elements/— Reactor.SelfTests is a Host-launcher MSTest harness, so the reflective surface test lives with the rest of the element-record unit suites. Two intentional property-name exceptions documented inline:VirtualListElement.Ref(ref-capture clashes with the generic.Ref<T>modifier) andXamlHostElement.Updater(constructor-time interop hook, not an event). All otherOnXcallbacks resolve to a matching fluent.)
- Self-test that asserts factories with names diverging from their
WinUI target (the §0.3 list) carry an XML doc remark explaining the
deviation. Use the existing doc-comment extraction tooling (grep
under
tests/Reactor.SelfTests/for// docprecedent). (Landed asNamingAlignmentGuardTests.DivergentFactoriesHaveRemarksundertests/Reactor.Tests/Elements/— loadsReactor.xml(emitted next toReactor.dllbyGenerateDocumentationFile) and asserts each ofVStack/HStack/Heading/SubHeading/Caption/Flex/FlexRow/FlexColumn/LazyVStack/LazyHStack/ListView<T>has at least one overload with a non-empty<remarks>. The genericListView<T>overload-only match captures the typed-vs-untyped divergence without flagging the WinUI-spelling untypedListView.)
- Combined with 1.6 — confirm every new
OnX(...)fluent treatsnullas a clear-handler operation. (AuditedEventFluentNullClearTests.csagainst the CSV inventory; added 8 missing facts:ItemsView.ItemInvoked,ItemsView.SelectionChanged,TemplatedGridView.ItemClick/SelectedIndexChanged/SelectionChanged,TemplatedFlipView.SelectedIndexChanged,TemplatedListView.SelectionChanged, andPropertyGrid.RootChanged. Total now 92 facts.)
- Switch theme between Light / Dark / HighContrast and confirm the
Cardbackground, stroke, and.AccentButton()brush re-resolve. (Unit-level smoke landed asCardThemeResolutionSmokeTestsundertests/Reactor.Tests/Elements/— assertsCard(child)wires theCardBackgroundFillColorDefaultBrush/CardStrokeColorDefaultBrushkeys intoThemeBindings, cross-checks againstTheme.CardBackground/Theme.CardStroke, and asserts.AccentButton()stores an OnMount action whose captured closure referencesAccentButtonStyle. The full Light/Dark/HighContrast flip-and-verify-brush test requires a theme-flip primitive onReactor.AppTests, which doesn't exist today — deferred as Reactor.AppTests follow-up.) - Deferred — Reactor.AppTests follow-up: real theme-flip test that
mounts a Card under each of Light / Dark / HighContrast and asserts
the resolved
Background/BorderBrushSolidColorBrush.Colorvalues differ across the three themes. Blocked on adding a theme-flip primitive to the AppTests harness.
- One snapshot per element with callbacks; update on schema change. (No-op: source-gen scaffolding struck per Phase 0.4 / Q1; the fluents are hand-written so the reflective surface guard from 11.1 already provides the equivalent drift detection.)
-
tests/Reactor.AppTests/— for every sample touched in Phase 8, mount under Light/Dark/NightSky and confirm no exceptions and the bound callbacks fire. (Pragmatic landing asGallerySampleConstructionSmokeTestsundertests/Reactor.Tests/Elements/— one fact per Phase-8 page asserting the page's factory + fluent chain constructs without exception and that its bound callbacks reach the expected element property. Pages areinternaltypes in a WinExe, so importing them whole-cloth would be heavyweight; the fixture replicates the spec-039-flagged factory and fluent surface each page exercises. Full Light/Dark/NightSky mount-and-fire test deferred — needs Reactor.AppTests harness.) - Deferred — Reactor.AppTests follow-up: real page-mount tests that load each Phase-8 page under Light / Dark / NightSky and simulate the bound callbacks firing. Blocked on a page-mount primitive (and a theme-flip primitive) in the AppTests harness.
-
AutoSuggestBox.OnSuggestionChosen(spec §3.4) — assert reachable via factory and fluent. (Landed asSpecFlaggedBugRegressionTests.AutoSuggestBox_*— factory path covered via the record positional constructor (the 4th positional parameter), fluent path covered via.SuggestionChosen(h), plus a fluent-overrides-constructor check.) -
HyperlinkButton.NavigateUri(spec §14 #5) — assert the fluent exists and the doc comment promise is fulfilled. (Named regression checkpoint underSpecFlaggedBugRegressionTests; canonical value-set tests already live inPhase4InitFluentTests.) -
Card(child)smoke — assert resolved brushes match the theme dict. (Named regression checkpoint underSpecFlaggedBugRegressionTests; detailed wiring lives inCardThemeResolutionSmokeTestsfrom 11.4.)
-
dotnet build Reactor.slnx -warnaserrorclean on a fresh clone. (Verified after rebase ontoorigin/main— picks up the "make solution warning-free" upstream fix (#311) which cleared a pre-existingNU1903Nerdbank.MessagePack vulnerability warning that was independent of the 039 surface.) -
REACTOR_A11Y_001..003clean across all touched files. (Implied by-warnaserrorclean — the analyzers run during build.) -
CS1591clean — every new public surface has XML doc comments. (Implied by-warnaserrorclean.)
- Run
tests/startup_perf/baseline; assert no regression vs main beyond the existing noise floor. Result (5-run median, ARM64 Release, foregrounded, AC): Reactor TTFP 238.1 ms / TTI 244.0 ms / PeakWS 118.1 MB. Calibrated pre-039 baseline (same machine, May 6): TTFP 230.2 ms / TTI 234.6 ms / PeakWS 113.1 MB. ~3% absolute drift, well within the run-to-run noise floor (WinUI3 baseline moved 178 → 147 ms TTFP in the opposite direction over the same window, confirming system-level variance dominates). Output:tests/startup_perf/out_039_after/. - Run
tests/stress_perf/once (foregrounded — per the memory [[reference_stress_perf_window_throttling]] — and on AC power per [[reference_stress_perf_drr_battery]]) to confirm no regression. Result (Reactor + ReactorOptimized @ 50% / 100%, ARM64 Release, foregrounded, AC,-SkipETW): Reactor reconcile times are essentially identical to the May-10 baseline (Reactor 50%: 46.3 ms → 46.7 ms; Reactor 100%: 56.9 ms → 56.8 ms). The ~9% FPS delta tracks framework-overhead noise outside Reactor's hot path — consistent with the API-additive nature of 039 (no reconciler / diff / mount-path code touched). Output:tests/stress_perf/baselines/full-matrix-2026-05-16-071025/.
- Lists every spec-§ entry resolved with a status checkbox.
- Lists every spec-§ entry deferred with a tracked follow-up.
- Lists the two
[Obsolete]aliases (RichText, potentiallyScrollView/Progress) and their planned removal release. - Confirms agent-kit + skills + samples landed in the same PR.
- One paragraph for the README highlights, one section in
CHANGELOG.md, one update indocs/guide/readme.md.
Cross-cutting tracker — check these off as a coarse-grained progress signal that maps to the spec's §14 ordering:
- §14 #1 — Fluent for every callback (Phase 1 — extensions drop the
leading
Onper the C# clash discovered in Phase 0.1). - §14 #2 — High-traffic init→fluent promotion (Phase 4).
- §14 #3 — Missing events modelled (Phase 3; 3.7 niche events
[deferred]to spec 040 with per-item rationale). - §14 #4 — Common-property gaps (Phase 5; GroupStyle and TreeView multi-select deferred — see Phase 5.6 / 5.8 notes).
- §14 #5 —
HyperlinkButton(Command)doc comment +.NavigateUri()(Phase 4.8). - §14 #6 — Type-ramp factories (Phase 2.6).
- §14 #7 — Spec 040 scheduled for specialized controls (Phase 7.1).
- §14 #8 —
RichText→RichTextBlockrename (Phase 6.1). - §17 — Named-style fluents and
Cardfactory (Phase 2). - Samples updated (Phase 8).
- Docs updated (Phase 9).
- Agent-kit updated (Phase 10).
- Tests landed (Phase 11).
- Ship gates green (Phase 12).