| name | reactor-commanding |
|---|---|
| description | Reactor's Command system — Command / Command<T> records, StandardCommand factory, command-aware DSL overloads (Button/MenuItem/AppBarButton), UseCommand hook for async lifecycle, CommandHost for keyboard-scoped accelerators, and ICommand interop. Load this when wiring menus, toolbars, keyboard shortcuts, or any action that appears in multiple surfaces. |
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.
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
};
// Computed: IsEnabled = CanExecute && !IsExecutingCommand<T> is identical but Execute/ExecuteAsync receive a typed
parameter — bind the parameter at the call site with MenuItem(cmd, item).
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 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.
Define once, bind anywhere:
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 argumentPer-site overrides with with:
var delete = StandardCommand.Delete(() => DeleteSelected());
MenuItem(delete) // "Delete"
MenuItem(delete with { Label = "Remove permanently" })
AppBarButton(delete with { Icon = SymbolIcon("Clear") })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 executing
return 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.
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"),
TextField(value, onChange)))Commands without an Accelerator are ignored by CommandHost.
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 TextField(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),
]);
}
}Bridge existing MVVM/CommunityToolkit ICommand:
var cmd = CommandInterop.FromCommand(
viewModel.SaveCommand,
"Save",
icon: SymbolIcon("Save"),
accelerator: Accelerator(VirtualKey.S, VirtualKeyModifiers.Control));- 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.