AsyncRelayCommand.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. // See the LICENSE file in the project root for more information.
  4. using System;
  5. using System.ComponentModel;
  6. using System.Runtime.CompilerServices;
  7. using System.Threading;
  8. using System.Threading.Tasks;
  9. using CommunityToolkit.Mvvm.ComponentModel.__Internals;
  10. using CommunityToolkit.Mvvm.Input.Internals;
  11. #pragma warning disable CS0618, CA1001
  12. namespace CommunityToolkit.Mvvm.Input;
  13. /// <summary>
  14. /// A command that mirrors the functionality of <see cref="RelayCommand"/>, with the addition of
  15. /// accepting a <see cref="Func{TResult}"/> returning a <see cref="Task"/> as the execute
  16. /// action, and providing an <see cref="ExecutionTask"/> property that notifies changes when
  17. /// <see cref="ExecuteAsync"/> is invoked and when the returned <see cref="Task"/> completes.
  18. /// </summary>
  19. public sealed partial class AsyncRelayCommand : IAsyncRelayCommand, ICancellationAwareCommand
  20. {
  21. /// <summary>
  22. /// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="ExecutionTask"/>.
  23. /// </summary>
  24. internal static readonly PropertyChangedEventArgs ExecutionTaskChangedEventArgs = new(nameof(ExecutionTask));
  25. /// <summary>
  26. /// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="CanBeCanceled"/>.
  27. /// </summary>
  28. internal static readonly PropertyChangedEventArgs CanBeCanceledChangedEventArgs = new(nameof(CanBeCanceled));
  29. /// <summary>
  30. /// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="IsCancellationRequested"/>.
  31. /// </summary>
  32. internal static readonly PropertyChangedEventArgs IsCancellationRequestedChangedEventArgs = new(nameof(IsCancellationRequested));
  33. /// <summary>
  34. /// The cached <see cref="PropertyChangedEventArgs"/> for <see cref="IsRunning"/>.
  35. /// </summary>
  36. internal static readonly PropertyChangedEventArgs IsRunningChangedEventArgs = new(nameof(IsRunning));
  37. /// <summary>
  38. /// The <see cref="Func{TResult}"/> to invoke when <see cref="Execute"/> is used.
  39. /// </summary>
  40. private readonly Func<Task>? execute;
  41. private readonly Func<object?, Task>? execute1;
  42. /// <summary>
  43. /// The cancelable <see cref="Func{T,TResult}"/> to invoke when <see cref="Execute"/> is used.
  44. /// </summary>
  45. /// <remarks>Only one between this and <see cref="execute"/> is not <see langword="null"/>.</remarks>
  46. private readonly Func<CancellationToken, Task>? cancelableExecute;
  47. private readonly Func<object?, CancellationToken, Task>? cancelableExecute1;
  48. /// <summary>
  49. /// The optional action to invoke when <see cref="CanExecute"/> is used.
  50. /// </summary>
  51. private readonly Func<bool>? canExecute;
  52. /// <summary>
  53. /// The options being set for the current command.
  54. /// </summary>
  55. private readonly AsyncRelayCommandOptions options;
  56. /// <summary>
  57. /// The <see cref="CancellationTokenSource"/> instance to use to cancel <see cref="cancelableExecute"/>.
  58. /// </summary>
  59. /// <remarks>This is only used when <see cref="cancelableExecute"/> is not <see langword="null"/>.</remarks>
  60. private CancellationTokenSource? cancellationTokenSource;
  61. /// <inheritdoc/>
  62. public event PropertyChangedEventHandler? PropertyChanged;
  63. /// <inheritdoc/>
  64. public event EventHandler? CanExecuteChanged;
  65. /// <summary>
  66. /// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class.
  67. /// </summary>
  68. /// <param name="execute">The execution logic.</param>
  69. /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="execute"/> is <see langword="null"/>.</exception>
  70. public AsyncRelayCommand(Func<Task> execute)
  71. {
  72. ArgumentNullException.ThrowIfNull(execute);
  73. this.execute = execute;
  74. }
  75. public AsyncRelayCommand(Func<object? ,Task> execute)
  76. {
  77. ArgumentNullException.ThrowIfNull(execute);
  78. this.execute1 = execute;
  79. }
  80. /// <summary>
  81. /// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class.
  82. /// </summary>
  83. /// <param name="execute">The execution logic.</param>
  84. /// <param name="options">The options to use to configure the async command.</param>
  85. /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="execute"/> is <see langword="null"/>.</exception>
  86. public AsyncRelayCommand(Func<Task> execute, AsyncRelayCommandOptions options)
  87. {
  88. ArgumentNullException.ThrowIfNull(execute);
  89. this.execute = execute;
  90. this.options = options;
  91. }
  92. public AsyncRelayCommand(Func<object?,Task> execute, AsyncRelayCommandOptions options)
  93. {
  94. ArgumentNullException.ThrowIfNull(execute);
  95. this.execute1 = execute;
  96. this.options = options;
  97. }
  98. /// <summary>
  99. /// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class.
  100. /// </summary>
  101. /// <param name="cancelableExecute">The cancelable execution logic.</param>
  102. /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="cancelableExecute"/> is <see langword="null"/>.</exception>
  103. public AsyncRelayCommand(Func<CancellationToken, Task> cancelableExecute)
  104. {
  105. ArgumentNullException.ThrowIfNull(cancelableExecute);
  106. this.cancelableExecute = cancelableExecute;
  107. }
  108. public AsyncRelayCommand(Func<object?,CancellationToken, Task> cancelableExecute)
  109. {
  110. ArgumentNullException.ThrowIfNull(cancelableExecute);
  111. this.cancelableExecute1 = cancelableExecute;
  112. }
  113. /// <summary>
  114. /// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class.
  115. /// </summary>
  116. /// <param name="cancelableExecute">The cancelable execution logic.</param>
  117. /// <param name="options">The options to use to configure the async command.</param>
  118. /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="cancelableExecute"/> is <see langword="null"/>.</exception>
  119. public AsyncRelayCommand(Func<CancellationToken, Task> cancelableExecute, AsyncRelayCommandOptions options)
  120. {
  121. ArgumentNullException.ThrowIfNull(cancelableExecute);
  122. this.cancelableExecute = cancelableExecute;
  123. this.options = options;
  124. }
  125. public AsyncRelayCommand(Func<object?,CancellationToken, Task> cancelableExecute, AsyncRelayCommandOptions options)
  126. {
  127. ArgumentNullException.ThrowIfNull(cancelableExecute);
  128. this.cancelableExecute1 = cancelableExecute;
  129. this.options = options;
  130. }
  131. /// <summary>
  132. /// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class.
  133. /// </summary>
  134. /// <param name="execute">The execution logic.</param>
  135. /// <param name="canExecute">The execution status logic.</param>
  136. /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="execute"/> or <paramref name="canExecute"/> are <see langword="null"/>.</exception>
  137. public AsyncRelayCommand(Func<Task> execute, Func<bool> canExecute)
  138. {
  139. ArgumentNullException.ThrowIfNull(execute);
  140. ArgumentNullException.ThrowIfNull(canExecute);
  141. this.execute = execute;
  142. this.canExecute = canExecute;
  143. }
  144. public AsyncRelayCommand(Func<object?,Task> execute, Func<bool> canExecute)
  145. {
  146. ArgumentNullException.ThrowIfNull(execute);
  147. ArgumentNullException.ThrowIfNull(canExecute);
  148. this.execute1 = execute;
  149. this.canExecute = canExecute;
  150. }
  151. /// <summary>
  152. /// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class.
  153. /// </summary>
  154. /// <param name="execute">The execution logic.</param>
  155. /// <param name="canExecute">The execution status logic.</param>
  156. /// <param name="options">The options to use to configure the async command.</param>
  157. /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="execute"/> or <paramref name="canExecute"/> are <see langword="null"/>.</exception>
  158. public AsyncRelayCommand(Func<Task> execute, Func<bool> canExecute, AsyncRelayCommandOptions options)
  159. {
  160. ArgumentNullException.ThrowIfNull(execute);
  161. ArgumentNullException.ThrowIfNull(canExecute);
  162. this.execute = execute;
  163. this.canExecute = canExecute;
  164. this.options = options;
  165. }
  166. public AsyncRelayCommand(Func<object?,Task> execute, Func<bool> canExecute, AsyncRelayCommandOptions options)
  167. {
  168. ArgumentNullException.ThrowIfNull(execute);
  169. ArgumentNullException.ThrowIfNull(canExecute);
  170. this.execute1 = execute;
  171. this.canExecute = canExecute;
  172. this.options = options;
  173. }
  174. /// <summary>
  175. /// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class.
  176. /// </summary>
  177. /// <param name="cancelableExecute">The cancelable execution logic.</param>
  178. /// <param name="canExecute">The execution status logic.</param>
  179. /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="cancelableExecute"/> or <paramref name="canExecute"/> are <see langword="null"/>.</exception>
  180. public AsyncRelayCommand(Func<CancellationToken, Task> cancelableExecute, Func<bool> canExecute)
  181. {
  182. ArgumentNullException.ThrowIfNull(cancelableExecute);
  183. ArgumentNullException.ThrowIfNull(canExecute);
  184. this.cancelableExecute = cancelableExecute;
  185. this.canExecute = canExecute;
  186. }
  187. public AsyncRelayCommand(Func<object?,CancellationToken, Task> cancelableExecute, Func<bool> canExecute)
  188. {
  189. ArgumentNullException.ThrowIfNull(cancelableExecute);
  190. ArgumentNullException.ThrowIfNull(canExecute);
  191. this.cancelableExecute1 = cancelableExecute;
  192. this.canExecute = canExecute;
  193. }
  194. /// <summary>
  195. /// Initializes a new instance of the <see cref="AsyncRelayCommand"/> class.
  196. /// </summary>
  197. /// <param name="cancelableExecute">The cancelable execution logic.</param>
  198. /// <param name="canExecute">The execution status logic.</param>
  199. /// <param name="options">The options to use to configure the async command.</param>
  200. /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="cancelableExecute"/> or <paramref name="canExecute"/> are <see langword="null"/>.</exception>
  201. public AsyncRelayCommand(Func<CancellationToken, Task> cancelableExecute, Func<bool> canExecute, AsyncRelayCommandOptions options)
  202. {
  203. ArgumentNullException.ThrowIfNull(cancelableExecute);
  204. ArgumentNullException.ThrowIfNull(canExecute);
  205. this.cancelableExecute = cancelableExecute;
  206. this.canExecute = canExecute;
  207. this.options = options;
  208. }
  209. public AsyncRelayCommand(Func<object?, CancellationToken, Task> cancelableExecute, Func<bool> canExecute, AsyncRelayCommandOptions options)
  210. {
  211. ArgumentNullException.ThrowIfNull(cancelableExecute);
  212. ArgumentNullException.ThrowIfNull(canExecute);
  213. this.cancelableExecute1 = cancelableExecute;
  214. this.canExecute = canExecute;
  215. this.options = options;
  216. }
  217. private Task? executionTask;
  218. /// <inheritdoc/>
  219. public Task? ExecutionTask
  220. {
  221. get => this.executionTask;
  222. private set
  223. {
  224. if (ReferenceEquals(this.executionTask, value))
  225. {
  226. return;
  227. }
  228. this.executionTask = value;
  229. PropertyChanged?.Invoke(this, ExecutionTaskChangedEventArgs);
  230. PropertyChanged?.Invoke(this, IsRunningChangedEventArgs);
  231. bool isAlreadyCompletedOrNull = value?.IsCompleted ?? true;
  232. if (this.cancellationTokenSource is not null)
  233. {
  234. PropertyChanged?.Invoke(this, CanBeCanceledChangedEventArgs);
  235. PropertyChanged?.Invoke(this, IsCancellationRequestedChangedEventArgs);
  236. }
  237. // The branch is on a condition evaluated before raising the events above if
  238. // needed, to avoid race conditions with a task completing right after them.
  239. if (isAlreadyCompletedOrNull)
  240. {
  241. return;
  242. }
  243. static async void MonitorTask(AsyncRelayCommand @this, Task task)
  244. {
  245. await task.GetAwaitableWithoutEndValidation();
  246. if (ReferenceEquals(@this.executionTask, task))
  247. {
  248. @this.PropertyChanged?.Invoke(@this, ExecutionTaskChangedEventArgs);
  249. @this.PropertyChanged?.Invoke(@this, IsRunningChangedEventArgs);
  250. if (@this.cancellationTokenSource is not null)
  251. {
  252. @this.PropertyChanged?.Invoke(@this, CanBeCanceledChangedEventArgs);
  253. }
  254. if ((@this.options & AsyncRelayCommandOptions.AllowConcurrentExecutions) == 0)
  255. {
  256. @this.CanExecuteChanged?.Invoke(@this, EventArgs.Empty);
  257. }
  258. }
  259. }
  260. MonitorTask(this, value!);
  261. }
  262. }
  263. /// <inheritdoc/>
  264. public bool CanBeCanceled => IsRunning && this.cancellationTokenSource is { IsCancellationRequested: false };
  265. /// <inheritdoc/>
  266. public bool IsCancellationRequested => this.cancellationTokenSource is { IsCancellationRequested: true };
  267. /// <inheritdoc/>
  268. public bool IsRunning => ExecutionTask is { IsCompleted: false };
  269. /// <inheritdoc/>
  270. bool ICancellationAwareCommand.IsCancellationSupported => this.execute is null;
  271. /// <inheritdoc/>
  272. public void NotifyCanExecuteChanged()
  273. {
  274. CanExecuteChanged?.Invoke(this, EventArgs.Empty);
  275. }
  276. /// <inheritdoc/>
  277. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  278. public bool CanExecute(object? parameter)
  279. {
  280. bool canExecute = this.canExecute?.Invoke() != false;
  281. return canExecute && ((this.options & AsyncRelayCommandOptions.AllowConcurrentExecutions) != 0 || ExecutionTask is not { IsCompleted: false });
  282. }
  283. /// <inheritdoc/>
  284. public void Execute(object? parameter)
  285. {
  286. Execute(null, parameter);
  287. }
  288. public void Execute(object?sender, object? parameter)
  289. {
  290. Task executionTask = ExecuteAsync(parameter);
  291. // If exceptions shouldn't flow to the task scheduler, await the resulting task. This is
  292. // delegated to a separate method to keep this one more compact in case the option is set.
  293. if ((this.options & AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler) == 0)
  294. {
  295. AwaitAndThrowIfFailed(executionTask);
  296. }
  297. }
  298. /// <inheritdoc/>
  299. public Task ExecuteAsync(object? parameter)
  300. {
  301. return ExecuteAsync(null, parameter);
  302. }
  303. public Task ExecuteAsync(object? sender,object? parameter)
  304. {
  305. Task executionTask;
  306. if (this.execute is not null)
  307. {
  308. // Non cancelable command delegate
  309. executionTask = ExecutionTask = this.execute();
  310. }
  311. else if(this.execute1 is not null)
  312. {
  313. executionTask = ExecutionTask = this.execute1(sender);
  314. }
  315. else
  316. {
  317. // Cancel the previous operation, if one is pending
  318. this.cancellationTokenSource?.Cancel();
  319. CancellationTokenSource cancellationTokenSource = this.cancellationTokenSource = new();
  320. // Invoke the cancelable command delegate with a new linked token
  321. executionTask = ExecutionTask = this.cancelableExecute1 != null ? this.cancelableExecute1!(sender,cancellationTokenSource.Token): this.cancelableExecute!(cancellationTokenSource.Token);
  322. }
  323. // If concurrent executions are disabled, notify the can execute change as well
  324. if ((this.options & AsyncRelayCommandOptions.AllowConcurrentExecutions) == 0)
  325. {
  326. CanExecuteChanged?.Invoke(this, EventArgs.Empty);
  327. }
  328. return executionTask;
  329. }
  330. /// <inheritdoc/>
  331. public void Cancel()
  332. {
  333. if (this.cancellationTokenSource is CancellationTokenSource { IsCancellationRequested: false } cancellationTokenSource)
  334. {
  335. cancellationTokenSource.Cancel();
  336. PropertyChanged?.Invoke(this, CanBeCanceledChangedEventArgs);
  337. PropertyChanged?.Invoke(this, IsCancellationRequestedChangedEventArgs);
  338. }
  339. }
  340. /// <summary>
  341. /// Awaits an input <see cref="Task"/> and throws an exception on the calling context, if the task fails.
  342. /// </summary>
  343. /// <param name="executionTask">The input <see cref="Task"/> instance to await.</param>
  344. internal static async void AwaitAndThrowIfFailed(Task executionTask)
  345. {
  346. // Note: this method is purposefully an async void method awaiting the input task. This is done so that
  347. // if an async relay command is invoked synchronously (ie. when Execute is called, eg. from a binding),
  348. // exceptions in the wrapped delegate will not be ignored or just become visible through the ExecutionTask
  349. // property, but will be rethrown in the original synchronization context by default. This makes the behavior
  350. // more consistent with how normal commands work (where exceptions are also just normally propagated to the
  351. // caller context), and avoids getting an app into an inconsistent state in case a method faults without
  352. // other components being notified. It is also possible to not await this task and to instead ignore exceptions
  353. // and then inspect them manually from the ExecutionTask property, by constructing an async command instance
  354. // using the AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler option. That will cause this call to
  355. // be skipped, and exceptions will just either normally be available through that property, or will otherwise
  356. // flow to the static TaskScheduler.UnobservedTaskException event if otherwise unobserved (eg. for logging).
  357. await executionTask;
  358. }
  359. }