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
CancellationToken
to 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 when executing your command successively, the first command handler that returns will set
IsRunning
to false
, although there may still handlers executing?
- Do you know that
CancellationTokenSource
implements IDisposable
and must be disposed? Generally a class that references unmanaged resources and therefore implements IDisposable
needs lifetime management to be disposed properly, otherwise your code introduces a potential memory leak or resource starvation?
- The current implementation is not reusable (e.g. doesn't allow a command parameter).
- The command can have a property like your
IsRunning
to return the internal busy state, but this property must have a private
(or non-public) set method. For data binding, the client should define dedicated property, that can be set locally inside the actual command handler definition. It's common to return a new ICommand
instance from a read-only computed property like:
public ICommand SaveCommand => new AsyncRelayCommand()
. In such a scenario the e.g. Button
would have a different instance than e.g. the
ProgressBar
and therefore the IsRunning
property would be a meaningless binding source for the ProgressBar
. A good API must be flexible and should not force the developer to adapt a specific programming style. The bottom line: the client code must be responsible to track the execution of his command handler in order to get the most reliable/stable results.
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 cancelable 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(CancellationToken cancellationToken);
Task ExecuteAsync(object parameter);
Task ExecuteAsync(object parameter, CancellationToken cancellationToken);
void InvalidateCommand();
}
AsyncRelayCommand.cs
public class AsyncRelayCommand : IAsyncRelayCommand
{
public bool IsExecuting => this.executionCount > 0;
protected readonly Func<Task> ExecuteAsyncNoParam;
protected readonly Action ExecuteNoParam;
protected readonly Func<bool> CanExecuteNoParam;
private readonly Func<object, Task> executeAsync;
private readonly Action<object> execute;
private readonly Predicate<object> canExecute;
private EventHandler canExecuteChangedDelegate;
private int executionCount;
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);
}
}
#region Constructors
public AsyncRelayCommand(Action<object> execute)
: this(execute, param => true)
{
}
public AsyncRelayCommand(Action executeNoParam)
: this(executeNoParam, () => true)
{
}
public AsyncRelayCommand(Func<object, Task> executeAsync)
: this(executeAsync, param => true)
{
}
public AsyncRelayCommand(Func<Task> executeAsyncNoParam)
: this(executeAsyncNoParam, () => true)
{
}
public AsyncRelayCommand(Action executeNoParam, Func<bool> canExecuteNoParam)
{
this.ExecuteNoParam = executeNoParam ?? throw new ArgumentNullException(nameof(executeNoParam));
this.CanExecuteNoParam = canExecuteNoParam ?? (() => true);
}
public AsyncRelayCommand(Action<object> execute, Predicate<object> canExecute)
{
this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
this.canExecute = canExecute ?? (param => true); ;
}
public AsyncRelayCommand(Func<Task> executeAsyncNoParam, Func<bool> canExecuteNoParam)
{
this.ExecuteAsyncNoParam = executeAsyncNoParam ?? throw new ArgumentNullException(nameof(executeAsyncNoParam));
this.CanExecuteNoParam = canExecuteNoParam ?? (() => true);
}
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() => CanExecute(null);
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() => await ExecuteAsync(null, CancellationToken.None);
public async Task ExecuteAsync(CancellationToken cancellationToken) => await ExecuteAsync(null, cancellationToken);
public async Task ExecuteAsync(object parameter) => await ExecuteAsync(parameter, CancellationToken.None);
public async Task ExecuteAsync(object parameter, CancellationToken cancellationToken)
{
try
{
Interlocked.Increment(ref this.executionCount);
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)
{
this.ExecuteNoParam.Invoke();
return;
}
this.execute?.Invoke(parameter);
}
finally
{
Interlocked.Decrement(ref this.executionCount);
}
}
public void InvalidateCommand() => OnCanExecuteChanged();
protected virtual void OnCanExecuteChanged() => this.CanExecuteChangedDelegate?.Invoke(this, EventArgs.Empty);
}