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
آخر نشاط في المصدر
٢٦ أغسطس ٢٠٢٦ في ٢٠:٥٨
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٦٧٤
التفرعات
٥٨

خيارات التثبيت

يُحدَّد 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