Commands
The MVVM (Model View ViewModel) pattern is the de facto standard for developing UI applications using XAML. It is also widely used in Xamarin applications using the native UI approach instead of Xamarin Forms.
Whatever the UI stack we are using amongst those, there is always the concept of a Command. A command, implements the ICommand interface which has been around since .NET 3.0.
This concept and its first implementations predates the creation of async await.
In most applications we would have code like this:
public class MainViewModel : ViewModelBase
{
private bool _isBusy;
public bool IsBusy
{
get => _isBusy;
private set => Set(ref _isBusy, value);
}
public RelayCommand Submit { get; private set; }
public MainViewModel()
{
Submit = new RelayCommand(ExecuteSubmit, CanExecuteSubmit);
}
private void ExecuteSubmit()
{
IsBusy = true;
// Do something
IsBusy = false;
}
private bool CanExecuteSubmit()
{
return true;
}
}
Here I am using the MvvmLight toolkit so the class implementing ICommand is RelayCommand. Other frameworks could have other names such as DelegateCommand or ActionCommand.
Asynchronism
As we can see in the code above, running asynchronous code inside the ExecuteSubmit
method would force us to mark it as async which is a really bad idea.
If you have no idea why async void is a bad idea please refer to the previous post.
Introducing AsyncCommand
If you are just interested by a reusable piece of code to use in your project take a look at the AsyncAwaitBestPratices library that was inspired by the content of this post.
We are not forced to use a RelayCommand and we can craft our own ICommand implementation:
public interface IAsyncCommand : ICommand
{
Task ExecuteAsync();
bool CanExecute();
}
public class AsyncCommand : IAsyncCommand
{
public event EventHandler CanExecuteChanged;
private bool _isExecuting;
private readonly Func<Task> _execute;
private readonly Func<bool> _canExecute;
private readonly IErrorHandler _errorHandler;
public AsyncCommand(
Func<Task> execute,
Func<bool> canExecute = null,
IErrorHandler errorHandler = null)
{
_execute = execute;
_canExecute = canExecute;
_errorHandler = errorHandler;
}
public bool CanExecute()
{
return !_isExecuting && (_canExecute?.Invoke() ?? true);
}
public async Task ExecuteAsync()
{
if (CanExecute())
{
try
{
_isExecuting = true;
await _execute();
}
finally
{
_isExecuting = false;
}
}
RaiseCanExecuteChanged();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
#region Explicit implementations
bool ICommand.CanExecute(object parameter)
{
return CanExecute();
}
void ICommand.Execute(object parameter)
{
ExecuteAsync().FireAndForgetSafeAsync(_errorHandler);
}
#endregion
}
This implementation has some key features:
- It does not allow concurrent execution so as to pass monkey testing.
- It provides explicit implementations in order to be bindable with XAML.
- It uses the
FireAndForgetSafeAsync
extension method and theIErrorHandler
interface introduced in the previous post to deal with the async void issues when using XAML binding. - It only publicly expose the
ExecuteAsync
providing the Task for cases when the command is not invoked through binding.
This command is not the answer for all async issues but it is a simple one for most of the cases I have encountered.
Using AsyncCommand
With everything in place, we can modify the view model to use the AsyncCommand
:
public class MainViewModel : ViewModelBase
{
private bool _isBusy;
public bool IsBusy
{
get => _isBusy;
private set => Set(ref _isBusy, value);
}
public IAsyncCommand Submit { get; private set; }
public MainViewModel()
{
Submit = new AsyncCommand(ExecuteSubmitAsync, CanExecuteSubmit);
}
private async Task ExecuteSubmitAsync()
{
try
{
IsBusy = true;
var coffeeService = new CoffeeService();
await coffeeService.PrepareCoffeeAsync();
}
finally
{
IsBusy = false;
}
}
private bool CanExecuteSubmit()
{
return !IsBusy;
}
}
As you can see, there is no async void in the view model !
Invoking the command
The command is easily invoked either by:
- Binding
<Button Text="Give me coffee !" Command="{Binding Submit}" VerticalOptions="End" />
- Code
public void OnPrepareButtonClick(object sender, EventArgs e) { IErrorHandler errorHandler = null; // Get an instance somewhere ViewModel.Submit.ExecuteAsync().FireAndForgetSafeAsync(errorHandler); }
Generic version
For all commands that need a parameter we can use the following generic version:
public interface IAsyncCommand<T> : ICommand
{
Task ExecuteAsync(T parameter);
bool CanExecute(T parameter);
}
public class AsyncCommand<T> : IAsyncCommand<T>
{
public event EventHandler CanExecuteChanged;
private bool _isExecuting;
private readonly Func<T, Task> _execute;
private readonly Func<T, bool> _canExecute;
private readonly IErrorHandler _errorHandler;
public AsyncCommand(Func<T, Task> execute, Func<T, bool> canExecute = null, IErrorHandler errorHandler = null)
{
_execute = execute;
_canExecute = canExecute;
_errorHandler = errorHandler;
}
public bool CanExecute(T parameter)
{
return !_isExecuting && (_canExecute?.Invoke(parameter) ?? true);
}
public async Task ExecuteAsync(T parameter)
{
if (CanExecute(parameter))
{
try
{
_isExecuting = true;
await _execute(parameter);
}
finally
{
_isExecuting = false;
}
}
RaiseCanExecuteChanged();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
#region Explicit implementations
bool ICommand.CanExecute(object parameter)
{
return CanExecute((T)parameter);
}
void ICommand.Execute(object parameter)
{
ExecuteAsync((T)parameter).FireAndForgetSafeAsync(_errorHandler);
}
#endregion
}
The two version are pretty similar and it is tempting to only keep the latter. We could use a AsyncCommand<object>
with null parameter for replace the first one. While it technically works, it is better to keep the two of them in the sense that having no parameter is not semantically similar to taking a null parameter.
Conclusion
By writing a simple custom command that natively handles asynchronism, we are able to simplify and improve our code and our application’s stability. Embedding the FireAndForgetSafeAsync
method inside the command removes the possibility that we forget to handle exceptions.
Please do not forget that crashes are not acceptable and are entirely our fault.
The source code of this post is available on my GitHub.
As always, please feel free to read my previous posts and to comment below, I will be more than happy to answer.
Comments