Skill v1.0.0
currentTrusted Publisher100/100version: "1.0.0" name: reactor-commanding description: "Reactor commanding — actions that appear in multiple surfaces (menu + toolbar), need keyboard shortcuts, or need CanExecute. Command, StandardCommand, UseCommand, CommandHost. Use when wiring shared actions or shortcuts."
Commanding in Reactor
Use Command when an action shows up in multiple surfaces (toolbar + menu + context menu), needs a keyboard shortcut, or needs CanExecute disabling. Use a bare Action for one-off button clicks with no reuse.
Controlled prop note: command examples may host controlled inputs such asTextBox(text, onChange). Factory call sites stay plain-valued, but directreads from migrated element records areOptional<T>; see
Command record
var save = new Command{Label = "Save", // requiredExecute = () => Save(), // sync// OR:ExecuteAsync = async () => await SaveAsync(), // async (wrap with UseCommand)CanExecute = hasChanges, // default trueIcon = SymbolIcon("Save"),Description = "Save the document", // tooltip + a11yAccelerator = Accelerator(VirtualKey.S, VirtualKeyModifiers.Control),AccessKey = "S", // Alt+keyDebounceMs = 0, // >0 = leading-edge debounce (needs UseCommand)};// Computed: IsEnabled = CanExecute && !IsExecuting && !IsDebouncing
Command<T> is identical but Execute/ExecuteAsync receive a typed parameter — bind the parameter at the call site with MenuItem(cmd, item).
StandardCommand factory
Pre-built commands with correct labels, icons, and accelerators:
var cut = StandardCommand.Cut(() => CutSelection());var copy = StandardCommand.Copy(() => CopySelection());var paste = StandardCommand.Paste(() => PasteFromClipboard());var undo = StandardCommand.Undo(() => Undo());var redo = StandardCommand.Redo(() => Redo());var delete = StandardCommand.Delete(() => DeleteSelected());var save = StandardCommand.Save(async () => await SaveAsync()); // async overloadvar open = StandardCommand.Open(() => OpenFile());// CanExecute parameter:var cut2 = StandardCommand.Cut(() => CutSelection(), canExecute: hasSelection);
Also available: SelectAll, Close, Share, Play, Pause, Stop, Forward, Backward.
Command-aware DSL overloads
Define once, bind anywhere:
var save = StandardCommand.Save(() => SaveFile());Button(save) // label → content, execute → click, isEnabled → isEnabledAppBarButton(save) // + icon, accelerator, accessKey, descriptionMenuItem(save) // + icon, accelerator, accessKey, descriptionMenuItem(deleteCmd, item) // parameterized: binds item as argument
Custom content (icon + label, stacked layouts) — build the element, then attach the command with the .Command() modifier. Works on Button, HyperlinkButton, RepeatButton, ToggleButton, and AppBarButton:
// Factory takes a text label only:Button(save)// .Command() binds execute + isEnabled + icon/accelerator/accessKey/description// onto any custom-content clickable, and auto-disables while !command.IsEnabled:Button(HStack(Icon(SymbolIcon("Save")), TextBlock("Save"))).Command(save)
.Command() re-applies IsEnabled on every update (so UseCommand toggling IsExecuting, or CanExecute changes, flow through) — never re-thread .IsEnabled(command.IsEnabled) by hand. It composes with .IsDisabledFocusable(): a disabled command keeps the button reachable via Tab instead of dropping it from the tab order.
Per-site overrides with with:
var delete = StandardCommand.Delete(() => DeleteSelected());MenuItem(delete) // "Delete"MenuItem(delete with { Label = "Remove permanently" })AppBarButton(delete with { Icon = SymbolIcon("Clear") })
UseCommand — async lifecycle
Only needed for commands with `ExecuteAsync`. Sync commands pass through unchanged.
class Editor : Component{public override Element Render(){var saveCmd = UseCommand(StandardCommand.Save(async () =>{await SaveAsync();}));// saveCmd.Execute is now a sync wrapper around the async// saveCmd.IsExecuting is true while the async is in-flight// saveCmd.IsEnabled auto-flips to false while executingreturn HStack(Button(saveCmd),saveCmd.IsExecuting ? ProgressRing() : Empty());}}
- Consumes 2 hook slots; re-entrance guard ignores clicks while executing.
IsExecutingresets to false even ifExecuteAsyncthrows.- Call unconditionally — don't wrap in
if. DebounceMs > 0adds leading-edge debounce: the first fire runs,
re-fires within the window are dropped, and IsEnabled is false for the duration so the button auto-disables then re-enables. Replaces the Task.Delay-to-absorb-double-clicks workaround. Needs UseCommand to persist the window — a raw new Command { DebounceMs = … } bound directly does not debounce. Works on sync Execute too (stays on the UI thread; no Task.Run hop). For async commands the disabled window is the longer of the lambda lifetime and DebounceMs.
CommandHost — keyboard-scoped accelerators
Limits Accelerator registration to a subtree:
var save = StandardCommand.Save(() => SaveFile());var undo = StandardCommand.Undo(() => UndoAction());CommandHost([save, undo],VStack(TextBlock("Ctrl+S / Ctrl+Z only fire inside this region"),TextBox(value, onChange)))
Commands without an Accelerator are ignored by CommandHost.
Sharing commands via Context
Editor-provides / toolbar-consumes:
record EditorCommands(Command Save, Command Undo, Command Redo);static readonly Context<EditorCommands?> EditorCtx = new(null);class Editor : Component{public override Element Render(){var save = UseCommand(StandardCommand.Save(async () => await SaveAsync()));var undo = StandardCommand.Undo(() => Undo());return TextBox(text, onChange).Provide(EditorCtx, new EditorCommands(save, undo, redo));}}class Toolbar : Component{public override Element Render(){var cmds = UseContext(EditorCtx);if (cmds is null) return Empty();return CommandBar(primaryCommands: [AppBarButton(cmds.Save),AppBarButton(cmds.Undo),]);}}
ICommand interop
Bridge existing MVVM/CommunityToolkit ICommand:
var cmd = CommandInterop.FromCommand(viewModel.SaveCommand,"Save",icon: SymbolIcon("Save"),accelerator: Accelerator(VirtualKey.S, VirtualKeyModifiers.Control));
Anti-patterns
- Don't create commands inside loops. Define once, bind per item with
MenuItem(cmd, item).
- Don't
UseCommandfor sync-only commands — it wastes hook slots. - Don't call
UseCommandconditionally — hooks must run in the same order
every render.
- Don't mix
ExecuteandExecuteAsyncon the same command — pick one.