AsyncRelayCommand¶
The AsyncRelayCommand<TParam> is an asynchronous implementation of the IRelayCommand<TParam> interface. It allows you to execute long-running operations safely without blocking the UI, while automatically preventing overlapping executions.
Overview¶
AsyncRelayCommand<TParam> is designed for scenarios where command execution involves I/O, network calls, or other async operations. While a command is executing, CanExecute automatically returns false, ensuring that the user cannot accidentally trigger multiple simultaneous executions.
Features¶
- Async/await support: Execute
Task-returning methods - Overlapping prevention: Automatically disables while executing
- Typed parameters: Execute with
TParamparameters - Can Execute notifications: Notify UI to re-check executability via
RaiseCanExecuteChanged() - Integrated logging: Built-in diagnostic logging
- UI thread dispatch: Automatic marshalling to UI thread
- Thread-safe execution state: Uses
Interlockedfor thread-safe flags
Basic Usage¶
Simple Async Operation¶
using TA.Utils.Core.MVVM;
using System.Threading.Tasks;
public class DataViewModel : ViewModelBase
{
private string _data = "Click to load";
private AsyncRelayCommand<string> _loadDataCommand;
public string Data
{
get => _data;
set => SetField(ref _data, value);
}
public AsyncRelayCommand<string> LoadDataCommand => _loadDataCommand ??=
new AsyncRelayCommand<string>(
execute: LoadDataAsync,
canExecute: CanLoadData,
name: "LoadData"
);
private async Task LoadDataAsync(string url)
{
Data = "Loading...";
try
{
// Simulate async operation (network call, database query, etc.)
await Task.Delay(2000);
Data = $"Loaded from {url}";
}
catch (Exception ex)
{
Data = $"Error: {ex.Message}";
}
}
private bool CanLoadData(string url)
{
return !string.IsNullOrEmpty(url);
}
}
XAML binding:
<Button Command="{Binding LoadDataCommand}"
CommandParameter="https://example.com"
Content="Load Data" />
<TextBlock Text="{Binding Data}" />
File Processing Example¶
public class FileProcessorViewModel : ViewModelBase
{
private AsyncRelayCommand<string> _processFileCommand;
private string _status = "Ready";
public string Status
{
get => _status;
set => SetField(ref _status, value);
}
public AsyncRelayCommand<string> ProcessFileCommand => _processFileCommand ??=
new AsyncRelayCommand<string>(
execute: ProcessFileAsync,
canExecute: CanProcessFile,
name: "ProcessFile"
);
private async Task ProcessFileAsync(string filePath)
{
Status = "Processing...";
try
{
// Simulate file processing
var lines = await File.ReadAllLinesAsync(filePath);
Status = $"Processed {lines.Length} lines";
}
catch (Exception ex)
{
Status = $"Failed: {ex.Message}";
}
}
private bool CanProcessFile(string filePath)
{
return !string.IsNullOrEmpty(filePath) && File.Exists(filePath);
}
}
Constructor¶
public AsyncRelayCommand(
Func<TParam, Task> execute,
Func<TParam, bool>? canExecute = null,
string? name = null,
ILog? log = null)
Parameters¶
- execute (required): A function that accepts a parameter and returns a
Task. Exceptions in this task are logged but do not propagate. - canExecute (optional): A predicate that determines executability; defaults to always returning
true. Only evaluated when not currently executing. - name (optional): Display name for diagnostics; defaults to "unnamed"
- log (optional): Logger instance; defaults to a degenerate (no-op) logger
Properties¶
Name¶
The name of the command (for diagnostic/display purposes).
Methods¶
CanExecute(object? parameter)¶
Returns false if the command is currently executing, regardless of the canExecute predicate. Returns the result of the canExecute predicate otherwise.
Execute(object? parameter)¶
Executes the command asynchronously. The method returns immediately (non-blocking). Exceptions thrown in the task are logged and do not propagate to the caller.
command.Execute("https://example.com"); // Returns immediately
// Actual async work happens in the background
RaiseCanExecuteChanged()¶
Notifies the UI that the CanExecute state may have changed. Marshalled to the UI thread.
// After an external state change that affects canExecute
_externalStateChanged = false;
command.RaiseCanExecuteChanged();
Events¶
CanExecuteChanged¶
Raised to notify the UI that CanExecute should be re-evaluated. Invoked on the UI thread.
Execution State Management¶
The command automatically manages its execution state:
- Before execution:
CanExecutereturnstrue(if predicate allows) - During execution:
CanExecutereturnsfalse(blocking further executions) - After execution completes:
CanExecutereverts to predicate-based result
This prevents overlapping executions without explicit user code.
// Conceptually, here's what happens internally:
private async Task Execute(TParam parameter)
{
// Set executing flag
Interlocked.Increment(ref _isExecuting);
try
{
await executeFunction(parameter); // User's async method
}
finally
{
// Clear executing flag
Interlocked.Decrement(ref _isExecuting);
}
}
Error Handling¶
Exceptions thrown in the execute task are caught, logged, and do not propagate. Check your logger to diagnose errors.
private async Task LoadDataAsync(string url)
{
try
{
var response = await httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
// ...
}
catch (HttpRequestException ex)
{
log.Error().Exception(ex).Message("HTTP request failed").Write();
Data = "Network error";
}
}
Logging¶
When a logger is provided, AsyncRelayCommand<TParam> logs:
- Trace level:
CanExecutequeries and execution state changes - Error level: Exceptions during task execution
var logger = new MyLoggerService();
var command = new AsyncRelayCommand<string>(
execute: LoadDataAsync,
name: "LoadData",
log: logger
);
UI Responsiveness¶
Because execution is asynchronous, the UI remains responsive:
// This does NOT block the UI
await command.Execute(param);
// The UI continues to process user input while the task runs
Cancellation¶
AsyncRelayCommand<TParam> does not have built-in cancellation support. If you need cancellation, use CancellationToken:
private CancellationTokenSource _cancellationTokenSource;
private async Task LoadDataAsync(string url)
{
using (_cancellationTokenSource = new CancellationTokenSource())
{
try
{
var data = await FetchDataAsync(url, _cancellationTokenSource.Token);
Data = data;
}
catch (OperationCanceledException)
{
Data = "Cancelled";
}
}
}
public void CancelOperation()
{
_cancellationTokenSource?.Cancel();
}
Best Practices¶
-
Always provide meaningful names for diagnostics:
-
Handle exceptions gracefully in your execute method:
-
Use guard predicates to prevent invalid operations:
-
Provide user feedback:
-
Use descriptive UI states:
See Also¶
- RelayCommand - For synchronous command execution
- ViewModelBase - Base class for view models
- UI Thread Dispatcher - Thread dispatch abstraction