15

Although I have understood async programming with c# somehow, still don't get why async with void is not the better solution, then when I want to improve my Xamarin Forms code I found many MVVM framework use AsyncCommand to avoid void with async "unlike events" like following:

public class AsyncCommand : Command {
    public AsyncCommand(Func<Task> execute) : base(() => execute()) { }
    public AsyncCommand(Func<object, Task> execute) : base((arg) => execute(arg)) { }
}

But I don't know why async if command itself not async and what about use an async command with action and run a task like that:

public AsyncCommand(Action execute) : this(() => Task.Run(execute))
public AsyncCommand(Action<object> execute) : this((arg) => Task.Run(() => execute(arg)))
2
  • Note that ICommandSource is operating on an ICommand where ICommand only defines the synchronous ICommand.Execute. Unless you execute the AsyncCommand´ from your own code the ICommandSource` e.g. a Button won't await anything. it synchronously calls ICommand.Execute. This has the same effect as assigning an async lambda to an Action delegate: the lambda is also not awaited. In both cases the await chain is broken. Therefore from a Button perspective it doesn't matter if your command handler is async void or async Task. Commented Jun 18, 2023 at 22:51
  • Unless Microsoft introduces an official IAsyncCommandSource and refactors their ICommandSource implementations to support IAsyncCommandSource (for example so that the Button calls await this.Command.ExecuteAsync) there is only a benefit for custom ICommandSource implementations and for custom code that can explicitly call and await IAsyncCommand.ExecuteAsync. Commented Jun 18, 2023 at 22:51

4 Answers 4

16

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
{
    /// <summary>
    /// An implmentation of IAsyncCommand. Allows Commands to safely be used asynchronously with Task.
    /// </summary>
    public sealed class AsyncCommand<T> : IAsyncCommand<T>
    {
        #region Constant Fields
        readonly Func<T, Task> _execute;
        readonly Func<object, bool> _canExecute;
        readonly Action<Exception> _onException;
        readonly bool _continueOnCapturedContext;
        readonly WeakEventManager _weakEventManager = new WeakEventManager();
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="T:TaskExtensions.MVVM.AsyncCommand`1"/> class.
        /// </summary>
        /// <param name="execute">The Function executed when Execute or ExecuteAysnc is called. This does not check canExecute before executing and will execute even if canExecute is false</param>
        /// <param name="canExecute">The Function that verifies whether or not AsyncCommand should execute.</param>
        /// <param name="onException">If an exception is thrown in the Task, <c>onException</c> will execute. If onException is null, the exception will be re-thrown</param>
        /// <param name="continueOnCapturedContext">If set to <c>true</c> continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to <c>false</c> continue on a different context; this will allow the Synchronization Context to continue on a different thread</param>
        public AsyncCommand(Func<T, Task> execute,
                            Func<object, bool> canExecute = null,
                            Action<Exception> 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
        /// <summary>
        /// Occurs when changes occur that affect whether or not the command should execute
        /// </summary>
        public event EventHandler CanExecuteChanged
        {
            add => _weakEventManager.AddEventHandler(value);
            remove => _weakEventManager.RemoveEventHandler(value);
        }
        #endregion

        #region Methods
        /// <summary>
        /// Determines whether the command can execute in its current state
        /// </summary>
        /// <returns><c>true</c>, if this command can be executed; otherwise, <c>false</c>.</returns>
        /// <param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param>
        public bool CanExecute(object parameter) => _canExecute(parameter);

        /// <summary>
        /// Raises the CanExecuteChanged event.
        /// </summary>
        public void RaiseCanExecuteChanged() => _weakEventManager.HandleEvent(this, EventArgs.Empty, nameof(CanExecuteChanged));

        /// <summary>
        /// Executes the Command as a Task
        /// </summary>
        /// <returns>The executed Task</returns>
        /// <param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param>
        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
    }

    /// <summary>
    /// An implmentation of IAsyncCommand. Allows Commands to safely be used asynchronously with Task.
    /// </summary>
    public sealed class AsyncCommand : IAsyncCommand
    {
        #region Constant Fields
        readonly Func<Task> _execute;
        readonly Func<object, bool> _canExecute;
        readonly Action<Exception> _onException;
        readonly bool _continueOnCapturedContext;
        readonly WeakEventManager _weakEventManager = new WeakEventManager();
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="T:TaskExtensions.MVVM.AsyncCommand`1"/> class.
        /// </summary>
        /// <param name="execute">The Function executed when Execute or ExecuteAysnc is called. This does not check canExecute before executing and will execute even if canExecute is false</param>
        /// <param name="canExecute">The Function that verifies whether or not AsyncCommand should execute.</param>
        /// <param name="onException">If an exception is thrown in the Task, <c>onException</c> will execute. If onException is null, the exception will be re-thrown</param>
        /// <param name="continueOnCapturedContext">If set to <c>true</c> continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to <c>false</c> continue on a different context; this will allow the Synchronization Context to continue on a different thread</param>
        public AsyncCommand(Func<Task> execute,
                            Func<object, bool> canExecute = null,
                            Action<Exception> 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
        /// <summary>
        /// Occurs when changes occur that affect whether or not the command should execute
        /// </summary>
        public event EventHandler CanExecuteChanged
        {
            add => _weakEventManager.AddEventHandler(value);
            remove => _weakEventManager.RemoveEventHandler(value);
        }
        #endregion

        #region Methods
        /// <summary>
        /// Determines whether the command can execute in its current state
        /// </summary>
        /// <returns><c>true</c>, if this command can be executed; otherwise, <c>false</c>.</returns>
        /// <param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param>
        public bool CanExecute(object parameter) => _canExecute(parameter);

        /// <summary>
        /// Raises the CanExecuteChanged event.
        /// </summary>
        public void RaiseCanExecuteChanged() => _weakEventManager.HandleEvent(this, EventArgs.Empty, nameof(CanExecuteChanged));

        /// <summary>
        /// Executes the Command as a Task
        /// </summary>
        /// <returns>The executed Task</returns>
        public Task ExecuteAsync() => _execute();

        void ICommand.Execute(object parameter) => _execute().SafeFireAndForget(_continueOnCapturedContext, _onException);
        #endregion
    }

    /// <summary>
    /// Extension methods for System.Threading.Tasks.Task
    /// </summary>
    public static class TaskExtensions
    {
        /// <summary>
        /// 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/.
        /// </summary>
        /// <param name="task">Task.</param>
        /// <param name="continueOnCapturedContext">If set to <c>true</c> continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to <c>false</c> continue on a different context; this will allow the Synchronization Context to continue on a different thread</param>
        /// <param name="onException">If an exception is thrown in the Task, <c>onException</c> will execute. If onException is null, the exception will be re-thrown</param>
        #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<System.Exception> 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);
            }
        }
    }

    /// <summary>
    /// Weak event manager that allows for garbage collection when the EventHandler is still subscribed
    /// </summary>
    public class WeakEventManager
    {
        readonly Dictionary<string, List<Subscription>> _eventHandlers = new Dictionary<string, List<Subscription>>();

        /// <summary>
        /// Adds the event handler
        /// </summary>
        /// <param name="handler">Handler</param>
        /// <param name="eventName">Event name</param>
        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);
        }

        /// <summary>
        /// Removes the event handler.
        /// </summary>
        /// <param name="handler">Handler</param>
        /// <param name="eventName">Event name</param>
        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);
        }

        /// <summary>
        /// Executes the event
        /// </summary>
        /// <param name="sender">Sender</param>
        /// <param name="eventArgs">Event arguments</param>
        /// <param name="eventName">Event name</param>
        public void HandleEvent(object sender, object eventArgs, string eventName) => EventManagerService.HandleEvent(eventName, sender, eventArgs, _eventHandlers);
    }

    /// <summary>
    /// An Async implmentation of ICommand
    /// </summary>
    public interface IAsyncCommand<T> : System.Windows.Input.ICommand
    {
        /// <summary>
        /// Executes the Command as a Task
        /// </summary>
        /// <returns>The executed Task</returns>
        /// <param name="parameter">Data used by the command. If the command does not require data to be passed, this object can be set to null.</param>
        System.Threading.Tasks.Task ExecuteAsync(T parameter);
    }

    /// <summary>
    /// An Async implmentation of ICommand
    /// </summary>
    public interface IAsyncCommand : System.Windows.Input.ICommand
    {
        /// <summary>
        /// Executes the Command as a Task
        /// </summary>
        /// <returns>The executed Task</returns>
        System.Threading.Tasks.Task ExecuteAsync();
    }
}
Sign up to request clarification or add additional context in comments.

8 Comments

hello Brandon.I want to use it in my application, but I can't get work with CanExecute - button is not changing to enable, when condition is set. Only it works when initialize - but changes not working. Can You post an example with view how to make it works?
@Andreas_k You’ll need to call AsyncCommand.RaiseCanExecuteChanged() each time the value of CanExecute changes. I’ve added this as an example at the bottom of the Readme for v4.0.0: github.com/brminnick/AsyncAwaitBestPractices/blob/…
And here is an example of how I’m using it in a sample app: github.com/brminnick/SimpleXamarinGraphQL/blob/…
So do I need to add that AsyncAwaitBestPractices.MVVM nuget package to my project to be able to use this code? I am missing references to EventManagerService and Subscription.
Why do you call the extension SafeFireAndForget instead of just FireAndForget? What makes it safe?
|
8

There is nothing wrong with async void on a command execute handler providing you handle your exceptions.

So what does an AsyncCommand, offer? Potentially the following

  • An error channel to pass back any unhandled exceptions

  • Not having to write async void or async lamdas

  • IsBusy framework, to stop things like double hits or what ever you can imagine

5 Comments

"Total benefit? Next to zero" are you sure about that?
I am puzzled by your statement that the total benefit is "next to zero", immediately after listing 3 possible benefits. Defining a custom class to give a central place to inject exception handling - without cluttering each call site - seems quite useful to me. Is your point that no framework built-in class would be likely to provide significant benefit? That this is a situation where it needs to be up to each programmer to decide if it is worth writing a custom class to handle their requirements?
@ToolmakerSteve yeah i should really remove the last part. There are benefits in certain areas
There is no big benefit when the ICommand is used with the ICommandSource of the WPF ljbrary. The point is that ICommand only defines a synchronous Execute method. And ICommandSource´ expects to handle an ´ICommand. This means not matter your IAsyncCommand.ExecuteAsync implementation the ICommandSource e.g. Button won't await your command. The call to ICommand.Execute is always synchronous. AsyncCommand usually calls ExecuteAsync from the ICommand.Execute method. It's the same as when assigning an async lambda to an Action delegate. The await chain is broken.
So unless you call IAsyncCommand.ExecuteAsync from your own code explicitly, there is no difference in assigning a async void command handler.
4

To anyone interested: Brandons solution above does not requery the CanExecute automatically and requires RaiseCanExecuteChanged(). To change this, you can exchange

    public event EventHandler CanExecuteChanged
    {
        add => _weakEventManager.AddEventHandler(value);
        remove => _weakEventManager.RemoveEventHandler(value);
    }

with

    public event EventHandler CanExecuteChanged {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

and remove

public void RaiseCanExecuteChanged() => _weakEventManager.HandleEvent(this, EventArgs.Empty, nameof(CanExecuteChanged));

This fixed the problem for me.

1 Comment

What is CommandManager and what does it do differently from the original implementation?
0

You should always avoid async void occurrences in your code (exceptions of event handlers).

Please refer this blog from Stephen for more detail

Async void methods have different error-handling semantics. When an exception is thrown out of an async Task or async Task method, that exception is captured and placed on the Task object. With async void methods, there is no Task object, so any exceptions thrown out of an async void method will be raised directly on the SynchronizationContext that was active when the async void method started.

enter image description here

1 Comment

as you wrote - exceptions of event handlers and ICommand\IAsyncCommand is also exceptional case, it's event handlers for mvvm in wpf

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.