How to implement Async Command

前端 未结 4 1792
轻奢々
轻奢々 2021-02-19 16:02

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

4条回答
  •  半阙折子戏
    2021-02-19 17:02

    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();
        }
    }
    

提交回复
热议问题