* WinUI plugin enhancements and mvvm toolkit skill * Split mvvm-toolkit skill for slimming
6.7 KiB
RelayCommand cookbook
Recipes for RelayCommand / AsyncRelayCommand and the [RelayCommand]
generator. Defaults to the generator-attribute style; manual constructor
patterns are listed at the bottom for advanced cases.
Sync command
[RelayCommand]
private void IncrementCounter() => Counter++;
<Button Command="{x:Bind ViewModel.IncrementCounterCommand}" Content="+1"/>
Sync command with parameter
[RelayCommand]
private void RemoveItem(Item item) => Items.Remove(item);
<Button Command="{x:Bind ViewModel.RemoveItemCommand}"
CommandParameter="{x:Bind Item}" Content="Remove"/>
The generator picks IRelayCommand<Item> based on the parameter type.
Sync command with CanExecute
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SubmitCommand))]
private string? text;
[RelayCommand(CanExecute = nameof(CanSubmit))]
private void Submit() => service.Submit(Text!);
private bool CanSubmit() => !string.IsNullOrWhiteSpace(Text);
[NotifyCanExecuteChangedFor] raises CanExecuteChanged automatically
whenever Text changes — without it, the button stays disabled even after
the user types.
Async command
[RelayCommand]
private async Task LoadAsync()
{
Items.Clear();
foreach (var item in await service.GetItemsAsync())
Items.Add(item);
}
Bind the UI to LoadCommand.IsRunning to show a spinner:
<ProgressRing IsActive="{x:Bind ViewModel.LoadCommand.IsRunning, Mode=OneWay}"/>
Async command with cancellation
[RelayCommand(IncludeCancelCommand = true)]
private async Task DownloadAsync(CancellationToken token)
{
try
{
await using var stream = await http.GetStreamAsync(url, token);
// ...
}
catch (OperationCanceledException)
{
// Expected — user cancelled.
}
}
<Button Command="{x:Bind ViewModel.DownloadCommand}" Content="Download"/>
<Button Command="{x:Bind ViewModel.DownloadCancelCommand}" Content="Cancel"/>
DownloadCancelCommand.CanExecute is automatically wired to
DownloadCommand.IsRunning.
Async command with concurrency
[RelayCommand(AllowConcurrentExecutions = true)]
private async Task PingAsync(string host)
{
await pingService.PingAsync(host);
}
Default (AllowConcurrentExecutions = false) reports the command as
disabled while a previous execution is pending. Set to true for
fire-and-forget patterns where overlapping invocations are safe.
Async command that surfaces errors to UI
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task SyncAsync(CancellationToken token)
{
await syncService.SyncAsync(token);
}
<TextBlock Text="{x:Bind ViewModel.SyncCommand.ExecutionTask.Exception, Mode=OneWay}"/>
Without FlowExceptionsToTaskScheduler = true, an uncaught exception in
SyncAsync will crash the app (mirroring sync commands). With it, the
exception is surfaced through ExecutionTask and bubbles to
TaskScheduler.UnobservedTaskException.
Showing async command status
<StackPanel>
<ProgressRing IsActive="{x:Bind ViewModel.SyncCommand.IsRunning, Mode=OneWay}"/>
<TextBlock Text="{x:Bind ViewModel.SyncCommand.ExecutionTask.Status, Mode=OneWay}"/>
</StackPanel>
Useful properties on IAsyncRelayCommand:
| Property | Type | Purpose |
|---|---|---|
ExecutionTask |
Task? |
The currently running (or last completed) task |
IsRunning |
bool |
true while a task is in flight |
CanBeCanceled |
bool |
true if the wrapped method takes a CancellationToken |
IsCancellationRequested |
bool |
true after Cancel() was called for the in-flight task |
Methods:
| Method | Purpose |
|---|---|
Cancel() |
Signals the active CancellationToken |
NotifyCanExecuteChanged() |
Re-evaluates CanExecute and raises CanExecuteChanged |
Forwarding attributes to the generated command property
[RelayCommand]
[property: JsonIgnore]
[property: Description("Saves the current document")]
private Task SaveAsync() => repo.SaveAsync(Text!);
The generator emits SaveCommand with [JsonIgnore] and [Description]
applied — useful when the VM is serialized.
Manual RelayCommand / AsyncRelayCommand
Reach for the manual constructors when you need:
- A command composed from multiple methods or dynamically rebuilt
- A
CanExecutepredicate built from external observables - An ICommand instance held in a field (rare; the generator's lazy property is enough for almost every case)
public sealed class CounterViewModel : ObservableObject
{
public CounterViewModel()
{
IncrementCommand = new RelayCommand(() => Counter++);
DecrementCommand = new RelayCommand(() => Counter--, () => Counter > 0);
}
[ObservableProperty]
private int counter;
public IRelayCommand IncrementCommand { get; }
public IRelayCommand DecrementCommand { get; }
}
public sealed class DownloadViewModel : ObservableObject
{
public DownloadViewModel()
{
DownloadCommand = new AsyncRelayCommand(DownloadAsync, () => CanDownload);
}
[ObservableProperty]
private bool canDownload = true;
public IAsyncRelayCommand DownloadCommand { get; }
private async Task DownloadAsync()
{
CanDownload = false;
try { await http.DownloadAsync(); }
finally { CanDownload = true; }
}
}
Trigger CanExecute re-evaluation manually with
SomeCommand.NotifyCanExecuteChanged().
Task.WhenAll from a single command
[RelayCommand]
private async Task SyncAllAsync(CancellationToken token)
{
var tasks = providers.Select(p => p.SyncAsync(token));
await Task.WhenAll(tasks);
}
If you want individual progress tracking per provider, expose one command per provider instead.
Common mistakes
async voidinstead ofasync Task. The generator only wrapsTask-returning methods asIAsyncRelayCommand.async voidbecomes a syncRelayCommandand exceptions are unobserved.- Forgetting
[NotifyCanExecuteChangedFor]. The button stays disabled even thoughCanX()would now returntrue. - Calling
Cancel()on a non-cancellable command. Only commands whose wrapped method accepts aCancellationTokenhonorCancel(). - Catching
OperationCanceledExceptionand rethrowing as a different type. Loses cancellation semantics;ExecutionTask.IsCanceledwill befalse. LetOperationCanceledExceptionpropagate (or return). - Awaiting
IAsyncRelayCommand.ExecuteAsync()from inside another[RelayCommand]. Prefer calling the underlying method directly to avoid double-wrapping the cancellation/concurrency semantics.