Search code examples
c#wpfasynchronousasync-awaiticommand

Async implementation of ICommand reasonable?


I've got an async implementation of the ICommand interface that of course has to implement the method void Execute(object parameter)

The actual implementation is then async like the following:

public async void Execute(object parameter)
{
    await ExecuteAsync((T)parameter);
}

The ExecuteAsync method is then defined as follows:

private readonly Func<T, Task> execute;
private bool isExecuting;

public async Task ExecuteAsync(T parameter)
{
    try
    {
        isExecuting = true;
        InvokeCanExecuteChanged();
        await execute(parameter);
    }
    finally
    {
        isExecuting = false;
        InvokeCanExecuteChanged();
    }
}

Now I do know that except for EventHandlers void should be avoided as return type for async methods. However ICommand is more or less a wrapper to clearly separate the view model from the actual event handlers. So am I fine with this implementation or will I run into problems?

Especially I'd like to know if I can safely use the execute command and rely on the Task to finish before the handler or will the handler complete regardless of the Task's status?


Solution

  • The biggest issue around async + void is that unlike an ordinary void any following code on the call site will be executed, before the code within the async void method has actually finished. You have to be 100% aware of this.

    This behavior is why async void is uncommon on an API level compared to async Task. In fact it's why i wrote a compiler error for any usage of async void within our company - Not all developers are aware of this and it can introduce potential bugs if you expect the content of that void method to finish before the call site code continues.

    Therefore your command will be fine if it provides a public async Task ExecuteAsync version of your command.

    See for yourself with this sample:

    public class SampleCommand<T> : ICommand where T : class 
    {
        /// <inheritdoc />
        public SampleCommand(Func<T, Task> execute)
        {
            this.execute = execute;
        }
    
        /// <inheritdoc />
        public bool CanExecute(object parameter)
        {
            return !isExecuting;
        }
    
        /// <inheritdoc />
        public async void Execute(object parameter)
        {
            await ExecuteAsync(parameter as T);
        }
    
        /// <inheritdoc />
        public event EventHandler CanExecuteChanged;
    
        private readonly Func<T, Task> execute;
        private bool isExecuting;
    
        public async Task ExecuteAsync(T parameter)
        {
            try
            {
                isExecuting = true;
                InvokeCanExecuteChanged();
                await execute(parameter);
            }
            finally
            {
                isExecuting = false;
                InvokeCanExecuteChanged();
            }
        }
    
        private void InvokeCanExecuteChanged()
        {
            CanExecuteChanged?.Invoke(this, EventArgs.Empty);
        }
    }
    
    public class SampleViewModel
    {
        public SampleCommand<object> TaskedCommand { get; set; }
    
        public SampleViewModel()
        {
            TaskedCommand = new SampleCommand<object>(TaskExecutionAsync);
    
            RunSomeMoreInitialization();
            RunSomeMoreInitialization2();
        }
    
        private async void RunSomeMoreInitialization()
        {
            /*
             * wpf usually calls it this way 
             * if a user calls this he might not be aware of the different behavior of this void method 
             */
            TaskedCommand.Execute(null);
            await Task.Delay(250);
            Debug.WriteLine("more code executed");
    
            /* 
             * this will print 
             * 
             * more code executed
             * command invoked
             */
        }
    
        private async void RunSomeMoreInitialization2()
        {
            // user might manually call it this way.
            await TaskedCommand.ExecuteAsync(null);
            await Task.Delay(250);
            Debug.WriteLine("more code executed");
            /* 
             * this will print 
             * 
             * command invoked
             * more code executed
             */
        }
    
        private Task TaskExecutionAsync(object o)
        {
            Task.Delay(500);
            Debug.WriteLine("command invoked");
    
            return Task.CompletedTask;
        }
    }