Skip to main content

reactor-commanding

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.

来源信息

仓库
microsoft/microsoft-ui-reactor
最近来源活动
2026年8月26日 20:58
检测到的 SKILL.md 语言
英语
星标
674
分支
58

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
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 as > `TextBox(text, onChange)`. Factory call sites stay plain-valued, but direct > reads from migrated element records are `Optional<T>`; see > [`migration/050-optional-t.md`](../../../../docs/guide/migration/050-optional-t.md). ## Command record ```csharp var save = new Command { Label = "Save", // required Execute = () => Save(), // sync // OR: ExecuteAsync = async () => await SaveAsync(), // async (wrap with UseCommand) CanExecute = hasChanges, // default true Icon = SymbolIcon("Save"), Description = "Save the document", // tooltip + a11y Accelerator = Accelerator(VirtualKey.S, VirtualKeyModifiers.Control), AccessKey = "S", // Alt+key DebounceMs = 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: ```csharp 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 overload var 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: ```csharp var save = StandardCommand.Save(() => SaveFile()); Button(save) // label → content, execute → click, isEnabled → isEnabled AppBarButton(save) // + icon, accelerator, accessKey, description MenuItem(save) // + icon, accelerator, accessKey, description MenuItem(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`: ```csharp // 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`: ```csharp 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. ```csharp 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 executing return HStack( Button(saveCmd), saveCmd.IsExecuting ? ProgressRing() : Empty()); } } ``` - Consumes 2 hook slots; re-entrance guard ignores clicks while executing. - `IsExecuting` resets to false even if `ExecuteAsync` throws. - Call unconditionally — don't wrap in `if`. - `DebounceMs > 0` adds **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: ```csharp 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: ```csharp 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`: ```csharp 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 `UseCommand` for sync-only commands — it wastes hook slots. - Don't call `UseCommand` conditionally — hooks must run in the same order every render. - Don't mix `Execute` and `ExecuteAsync` on the same command — pick one.
在 GitHub 查看