Here is an implementation of AsyncCommand
that I created for this NuGet Package: AsyncAwaitBestPractices.MVVM.
This implementation was inspired by @John Thiriet's blog post, "Going Async With AsyncCommand".
using System;
using System.Threading.Tasks;
using System.Windows.Input;
namespace AsyncAwaitBestPractices.MVVM
{
///
/// An implmentation of IAsyncCommand. Allows Commands to safely be used asynchronously with Task.
///
public sealed class AsyncCommand : IAsyncCommand
{
#region Constant Fields
readonly Func _execute;
readonly Func _canExecute;
readonly Action _onException;
readonly bool _continueOnCapturedContext;
readonly WeakEventManager _weakEventManager = new WeakEventManager();
#endregion
#region Constructors
///
/// Initializes a new instance of the class.
///
/// The Function executed when Execute or ExecuteAysnc is called. This does not check canExecute before executing and will execute even if canExecute is false
/// The Function that verifies whether or not AsyncCommand should execute.
/// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown
/// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread
public AsyncCommand(Func execute,
Func canExecute = null,
Action onException = null,
bool continueOnCapturedContext = true)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute), $"{nameof(execute)} cannot be null");
_canExecute = canExecute ?? (_ => true);
_onException = onException;
_continueOnCapturedContext = continueOnCapturedContext;
}
#endregion
#region Events
///
/// Occurs when changes occur that affect whether or not the command should execute
///
public event EventHandler CanExecuteChanged
{
add => _weakEventManager.AddEventHandler(value);
remove => _weakEventManager.RemoveEventHandler(value);
}
#endregion
#region Methods
///
/// Determines whether the command can execute in its current state
///
/// true , if this command can be executed; otherwise, false .
/// Data used by the command. If the command does not require data to be passed, this object can be set to null.
public bool CanExecute(object parameter) => _canExecute(parameter);
///
/// Raises the CanExecuteChanged event.
///
public void RaiseCanExecuteChanged() => _weakEventManager.HandleEvent(this, EventArgs.Empty, nameof(CanExecuteChanged));
///
/// Executes the Command as a Task
///
/// The executed Task
/// Data used by the command. If the command does not require data to be passed, this object can be set to null.
public Task ExecuteAsync(T parameter) => _execute(parameter);
void ICommand.Execute(object parameter)
{
if (parameter is T validParameter)
ExecuteAsync(validParameter).SafeFireAndForget(_continueOnCapturedContext, _onException);
else if (parameter is null && !typeof(T).IsValueType)
ExecuteAsync((T)parameter).SafeFireAndForget(_continueOnCapturedContext, _onException);
else
throw new InvalidCommandParameterException(typeof(T), parameter.GetType());
}
#endregion
}
///
/// An implmentation of IAsyncCommand. Allows Commands to safely be used asynchronously with Task.
///
public sealed class AsyncCommand : IAsyncCommand
{
#region Constant Fields
readonly Func _execute;
readonly Func _canExecute;
readonly Action _onException;
readonly bool _continueOnCapturedContext;
readonly WeakEventManager _weakEventManager = new WeakEventManager();
#endregion
#region Constructors
///
/// Initializes a new instance of the class.
///
/// The Function executed when Execute or ExecuteAysnc is called. This does not check canExecute before executing and will execute even if canExecute is false
/// The Function that verifies whether or not AsyncCommand should execute.
/// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown
/// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread
public AsyncCommand(Func execute,
Func canExecute = null,
Action onException = null,
bool continueOnCapturedContext = true)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute), $"{nameof(execute)} cannot be null");
_canExecute = canExecute ?? (_ => true);
_onException = onException;
_continueOnCapturedContext = continueOnCapturedContext;
}
#endregion
#region Events
///
/// Occurs when changes occur that affect whether or not the command should execute
///
public event EventHandler CanExecuteChanged
{
add => _weakEventManager.AddEventHandler(value);
remove => _weakEventManager.RemoveEventHandler(value);
}
#endregion
#region Methods
///
/// Determines whether the command can execute in its current state
///
/// true , if this command can be executed; otherwise, false .
/// Data used by the command. If the command does not require data to be passed, this object can be set to null.
public bool CanExecute(object parameter) => _canExecute(parameter);
///
/// Raises the CanExecuteChanged event.
///
public void RaiseCanExecuteChanged() => _weakEventManager.HandleEvent(this, EventArgs.Empty, nameof(CanExecuteChanged));
///
/// Executes the Command as a Task
///
/// The executed Task
public Task ExecuteAsync() => _execute();
void ICommand.Execute(object parameter) => _execute().SafeFireAndForget(_continueOnCapturedContext, _onException);
#endregion
}
///
/// Extension methods for System.Threading.Tasks.Task
///
public static class TaskExtensions
{
///
/// Safely execute the Task without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/.
///
/// Task.
/// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread
/// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown
#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void
public static async void SafeFireAndForget(this System.Threading.Tasks.Task task, bool continueOnCapturedContext = true, System.Action onException = null)
#pragma warning restore RECS0165 // Asynchronous methods should return a Task instead of void
{
try
{
await task.ConfigureAwait(continueOnCapturedContext);
}
catch (System.Exception ex) when (onException != null)
{
onException?.Invoke(ex);
}
}
}
///
/// Weak event manager that allows for garbage collection when the EventHandler is still subscribed
///
public class WeakEventManager
{
readonly Dictionary> _eventHandlers = new Dictionary>();
///
/// Adds the event handler
///
/// Handler
/// Event name
public void AddEventHandler(Delegate handler, [CallerMemberName] string eventName = "")
{
if (IsNullOrWhiteSpace(eventName))
throw new ArgumentNullException(nameof(eventName));
if (handler is null)
throw new ArgumentNullException(nameof(handler));
EventManagerService.AddEventHandler(eventName, handler.Target, handler.GetMethodInfo(), _eventHandlers);
}
///
/// Removes the event handler.
///
/// Handler
/// Event name
public void RemoveEventHandler(Delegate handler, [CallerMemberName] string eventName = "")
{
if (IsNullOrWhiteSpace(eventName))
throw new ArgumentNullException(nameof(eventName));
if (handler is null)
throw new ArgumentNullException(nameof(handler));
EventManagerService.RemoveEventHandler(eventName, handler.Target, handler.GetMethodInfo(), _eventHandlers);
}
///
/// Executes the event
///
/// Sender
/// Event arguments
/// Event name
public void HandleEvent(object sender, object eventArgs, string eventName) => EventManagerService.HandleEvent(eventName, sender, eventArgs, _eventHandlers);
}
///
/// An Async implmentation of ICommand
///
public interface IAsyncCommand : System.Windows.Input.ICommand
{
///
/// Executes the Command as a Task
///
/// The executed Task
/// Data used by the command. If the command does not require data to be passed, this object can be set to null.
System.Threading.Tasks.Task ExecuteAsync(T parameter);
}
///
/// An Async implmentation of ICommand
///
public interface IAsyncCommand : System.Windows.Input.ICommand
{
///
/// Executes the Command as a Task
///
/// The executed Task
System.Threading.Tasks.Task ExecuteAsync();
}
}