I realized that the question is quite old, but still seems to have some relevance. I guess people feel attracted, because they expect to find a usable asynchronous ICommand implementation.
The current implementation is full of flaws, of which most are not addressed by any answer.
The implementation needs serious fixes otherwise it is dangerous and a potential bug producer:
- Why do you ignore the command parameter of
Execute(object)? - Why are you not awaiting
_action? - Why do you pass an internal
CancellationTokento the async delegate, shouldn't the caller of your API be responsible to handle cancellation (like the whole Task library does)? - Do you know that when executing your command successively, you are overriding the reference to the previous
CancellationTokenSource, which makes it impossible to cancel previous invocations? - Do you know that
CancellationTokenSourceimplementsIDisposableand must be disposed? Generally a class that references unmanaged resources and therefore implementsIDisposableneeds lifetime management to be disposed properly, otherwise your code introduces a potential memory leak or resource starvation? - The current imlementation is not reusable (e.g. doesn't allow a command parameter).
- The command can have a property to return the internal busy state, but this property must have a
privateset method. For data binding, the client should define dedicated property, that he can set on his own inside his command handler definition. It's common to return a newICommandinstance from a read-only computed property like:
public ICommand SaveCommand => new AsyncRelayCommand(). In such a scenario the e.g.Buttonwould have a different instance than e.g. theProgressBarand therefore theAsyncRelayCommand.IsExecutingwould be meaningless binding source for theProgressBar. A good API must be flexible and should not force the developer to adapt a specific programming style.
The general idea of an asynchronous command is to allow to define asynchronous command handlers. This means the real asynchronous part is the responsibility of the client of the command API. The command itself is only the invocator of the command handler (like the common C# event pattern).
For this reason the client is responsible to decide whether his asynchronous operation is cancellable or not. He must provide the CancellationToken and is responsible to manage multiple instance of them (e.g., by linking them). The command API in general should also accept a CancellationToken following the fashion of the Task API or TAP library implements.
Another advantage of avoiding any CancellationTokenSource aggregation is the elimination of the responsibility to track the lifetime of a IDisposable or even having the asynchronous command itself to implement IDisposabe
Since async void signatures introduce some serious problems regarding error handling, the original ICommand API should be "hidden" by implementing the interface member ICommand.Execute explicitly. This way we can encourage the client to invoke the clean async Task custom overload e.g. async Task ExecuteAsync(object).
A clean and very basic non-generic solution, that also accepts synchronous command handlers (to support "default" synchronous command execution) could look as followed:
IAsyncRelayCommand.cs
public interface IAsyncRelayCommand : ICommand
{
bool IsExecuting { get; }
bool CanExecute();
Task ExecuteAsync();
Task ExecuteAsync(object parameter);
void InvalidateCommand();
}
AsyncRelayCommand.cs
public class AsyncRelayCommand : IAsyncRelayCommand
{
#region Constructors
// Initialize with synchronous command handler
public AsyncRelayCommand(Action<object> execute)
: this(execute, param => true)
{
}
// Initialize with synchronous parameterless command handler
public AsyncRelayCommand(Action executeNoParam)
: this(executeNoParam, () => true)
{
}
// Initialize with asynchronous command handler
public AsyncRelayCommand(Func<object, Task> executeAsync)
: this(executeAsync, param => true)
{
}
// Initialize with asynchronous parameterless command handler
public AsyncRelayCommand(Func<Task> executeAsyncNoParam)
: this(executeAsyncNoParam, () => true)
{
}
// Initialize with synchronous parameterless command handler
public AsyncRelayCommand(Action executeNoParam, Func<bool> canExecuteNoParam)
{
this.ExecuteNoParam = executeNoParam ?? throw new ArgumentNullException(nameof(executeNoParam));
this.CanExecuteNoParam = canExecuteNoParam ?? (() => true);
}
// Initialize with synchronous command handler
public AsyncRelayCommand(Action<object> execute, Predicate<object> canExecute)
{
this.Execute = execute ?? throw new ArgumentNullException(nameof(execute));
this.CanExecute = canExecute ?? (param => true);
}
// Initialize with asynchronous parameterless command handler
public AsyncRelayCommand(Func<Task> executeAsyncNoParam, Func<bool> canExecuteNoParam)
{
this.ExecuteAsyncNoParam = executeAsyncNoParam ?? throw new ArgumentNullException(nameof(executeAsyncNoParam));
this.CanExecuteNoParam = canExecuteNoParam ?? (() => true);
}
// Initialize with asynchronous command handler
public AsyncRelayCommand(Func<object, Task> executeAsync, Predicate<object> canExecute)
{
this.ExecuteAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync));
this.CanExecute = canExecute ?? (param => true);
}
#endregion Constructors
public bool CanExecute() => this.CanExecuteNoParam?.Invoke()
?? this.CanExecute?.Invoke(null)
?? true;
public bool CanExecute(object parameter) => this.CanExecute?.Invoke(parameter)
?? this.CanExecuteNoParam?.Invoke()
?? true;
async void ICommand.Execute(object parameter)
=> await ExecuteAsync(parameter, CancellationToken.None);
public async Task ExecuteAsync(object parameter)
=> await ExecuteAsync(parameter, CancellationToken.None);
public async Task ExecuteAsync(object parameter, CancellationToken cancellationToken)
{
try
{
this.IsExecuting = true;
cancellationToken.ThrowIfCancellationRequested();
if (this.ExecuteAsync != null)
{
await this.ExecuteAsync.Invoke(parameter).ConfigureAwait(false);
return;
}
if (this.ExecuteAsyncNoParam != null)
{
await this.ExecuteAsyncNoParam.Invoke().ConfigureAwait(false);
return;
}
if (this.ExecuteNoParam != null)
{
his.ExecuteNoParam.Invoke();
return;
}
this.Execute?.Invoke(parameter);
}
finally
{
this.IsExecuting = false;
}
}
public void InvalidateCommand() => this.CanExecuteChangedDelegate?.Invoke(this, EventArgs.Empty);
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
this.CanExecuteChangedDelegate = (EventHandler) Delegate.Combine(this.CanExecuteChangedDelegate, value);
}
remove
{
CommandManager.RequerySuggested -= value;
this.CanExecuteChangedDelegate = (EventHandler) Delegate.Remove(this.CanExecuteChangedDelegate, value);
}
}
public bool IsExecuting { get; private set; }
private EventHandler CanExecuteChangedDelegate { get; set; }
private Func<bool> CanExecuteNoParam { get; set; }
private readonly Func<object, Task> ExecuteAsync { get; set; }
private readonly Action<object> Execute { get; set; }
private readonly Predicate<object> CanExecute { get; set; }
}