// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel.__Internals; using CommunityToolkit.Mvvm.Input.Internals; #pragma warning disable CS0618, CA1001 namespace CommunityToolkit.Mvvm.Input; /// /// A command that mirrors the functionality of , with the addition of /// accepting a returning a as the execute /// action, and providing an property that notifies changes when /// is invoked and when the returned completes. /// public sealed partial class AsyncRelayCommand : IAsyncRelayCommand, ICancellationAwareCommand { /// /// The cached for . /// internal static readonly PropertyChangedEventArgs ExecutionTaskChangedEventArgs = new(nameof(ExecutionTask)); /// /// The cached for . /// internal static readonly PropertyChangedEventArgs CanBeCanceledChangedEventArgs = new(nameof(CanBeCanceled)); /// /// The cached for . /// internal static readonly PropertyChangedEventArgs IsCancellationRequestedChangedEventArgs = new(nameof(IsCancellationRequested)); /// /// The cached for . /// internal static readonly PropertyChangedEventArgs IsRunningChangedEventArgs = new(nameof(IsRunning)); /// /// The to invoke when is used. /// private readonly Func? execute; private readonly Func? execute1; /// /// The cancelable to invoke when is used. /// /// Only one between this and is not . private readonly Func? cancelableExecute; private readonly Func? cancelableExecute1; /// /// The optional action to invoke when is used. /// private readonly Func? canExecute; /// /// The options being set for the current command. /// private readonly AsyncRelayCommandOptions options; /// /// The instance to use to cancel . /// /// This is only used when is not . private CancellationTokenSource? cancellationTokenSource; /// public event PropertyChangedEventHandler? PropertyChanged; /// public event EventHandler? CanExecuteChanged; /// /// Initializes a new instance of the class. /// /// The execution logic. /// Thrown if is . public AsyncRelayCommand(Func execute) { ArgumentNullException.ThrowIfNull(execute); this.execute = execute; } public AsyncRelayCommand(Func execute) { ArgumentNullException.ThrowIfNull(execute); this.execute1 = execute; } /// /// Initializes a new instance of the class. /// /// The execution logic. /// The options to use to configure the async command. /// Thrown if is . public AsyncRelayCommand(Func execute, AsyncRelayCommandOptions options) { ArgumentNullException.ThrowIfNull(execute); this.execute = execute; this.options = options; } public AsyncRelayCommand(Func execute, AsyncRelayCommandOptions options) { ArgumentNullException.ThrowIfNull(execute); this.execute1 = execute; this.options = options; } /// /// Initializes a new instance of the class. /// /// The cancelable execution logic. /// Thrown if is . public AsyncRelayCommand(Func cancelableExecute) { ArgumentNullException.ThrowIfNull(cancelableExecute); this.cancelableExecute = cancelableExecute; } public AsyncRelayCommand(Func cancelableExecute) { ArgumentNullException.ThrowIfNull(cancelableExecute); this.cancelableExecute1 = cancelableExecute; } /// /// Initializes a new instance of the class. /// /// The cancelable execution logic. /// The options to use to configure the async command. /// Thrown if is . public AsyncRelayCommand(Func cancelableExecute, AsyncRelayCommandOptions options) { ArgumentNullException.ThrowIfNull(cancelableExecute); this.cancelableExecute = cancelableExecute; this.options = options; } public AsyncRelayCommand(Func cancelableExecute, AsyncRelayCommandOptions options) { ArgumentNullException.ThrowIfNull(cancelableExecute); this.cancelableExecute1 = cancelableExecute; this.options = options; } /// /// Initializes a new instance of the class. /// /// The execution logic. /// The execution status logic. /// Thrown if or are . public AsyncRelayCommand(Func execute, Func canExecute) { ArgumentNullException.ThrowIfNull(execute); ArgumentNullException.ThrowIfNull(canExecute); this.execute = execute; this.canExecute = canExecute; } public AsyncRelayCommand(Func execute, Func canExecute) { ArgumentNullException.ThrowIfNull(execute); ArgumentNullException.ThrowIfNull(canExecute); this.execute1 = execute; this.canExecute = canExecute; } /// /// Initializes a new instance of the class. /// /// The execution logic. /// The execution status logic. /// The options to use to configure the async command. /// Thrown if or are . public AsyncRelayCommand(Func execute, Func canExecute, AsyncRelayCommandOptions options) { ArgumentNullException.ThrowIfNull(execute); ArgumentNullException.ThrowIfNull(canExecute); this.execute = execute; this.canExecute = canExecute; this.options = options; } public AsyncRelayCommand(Func execute, Func canExecute, AsyncRelayCommandOptions options) { ArgumentNullException.ThrowIfNull(execute); ArgumentNullException.ThrowIfNull(canExecute); this.execute1 = execute; this.canExecute = canExecute; this.options = options; } /// /// Initializes a new instance of the class. /// /// The cancelable execution logic. /// The execution status logic. /// Thrown if or are . public AsyncRelayCommand(Func cancelableExecute, Func canExecute) { ArgumentNullException.ThrowIfNull(cancelableExecute); ArgumentNullException.ThrowIfNull(canExecute); this.cancelableExecute = cancelableExecute; this.canExecute = canExecute; } public AsyncRelayCommand(Func cancelableExecute, Func canExecute) { ArgumentNullException.ThrowIfNull(cancelableExecute); ArgumentNullException.ThrowIfNull(canExecute); this.cancelableExecute1 = cancelableExecute; this.canExecute = canExecute; } /// /// Initializes a new instance of the class. /// /// The cancelable execution logic. /// The execution status logic. /// The options to use to configure the async command. /// Thrown if or are . public AsyncRelayCommand(Func cancelableExecute, Func canExecute, AsyncRelayCommandOptions options) { ArgumentNullException.ThrowIfNull(cancelableExecute); ArgumentNullException.ThrowIfNull(canExecute); this.cancelableExecute = cancelableExecute; this.canExecute = canExecute; this.options = options; } public AsyncRelayCommand(Func cancelableExecute, Func canExecute, AsyncRelayCommandOptions options) { ArgumentNullException.ThrowIfNull(cancelableExecute); ArgumentNullException.ThrowIfNull(canExecute); this.cancelableExecute1 = cancelableExecute; this.canExecute = canExecute; this.options = options; } private Task? executionTask; /// public Task? ExecutionTask { get => this.executionTask; private set { if (ReferenceEquals(this.executionTask, value)) { return; } this.executionTask = value; PropertyChanged?.Invoke(this, ExecutionTaskChangedEventArgs); PropertyChanged?.Invoke(this, IsRunningChangedEventArgs); bool isAlreadyCompletedOrNull = value?.IsCompleted ?? true; if (this.cancellationTokenSource is not null) { PropertyChanged?.Invoke(this, CanBeCanceledChangedEventArgs); PropertyChanged?.Invoke(this, IsCancellationRequestedChangedEventArgs); } // The branch is on a condition evaluated before raising the events above if // needed, to avoid race conditions with a task completing right after them. if (isAlreadyCompletedOrNull) { return; } static async void MonitorTask(AsyncRelayCommand @this, Task task) { await task.GetAwaitableWithoutEndValidation(); if (ReferenceEquals(@this.executionTask, task)) { @this.PropertyChanged?.Invoke(@this, ExecutionTaskChangedEventArgs); @this.PropertyChanged?.Invoke(@this, IsRunningChangedEventArgs); if (@this.cancellationTokenSource is not null) { @this.PropertyChanged?.Invoke(@this, CanBeCanceledChangedEventArgs); } if ((@this.options & AsyncRelayCommandOptions.AllowConcurrentExecutions) == 0) { @this.CanExecuteChanged?.Invoke(@this, EventArgs.Empty); } } } MonitorTask(this, value!); } } /// public bool CanBeCanceled => IsRunning && this.cancellationTokenSource is { IsCancellationRequested: false }; /// public bool IsCancellationRequested => this.cancellationTokenSource is { IsCancellationRequested: true }; /// public bool IsRunning => ExecutionTask is { IsCompleted: false }; /// bool ICancellationAwareCommand.IsCancellationSupported => this.execute is null; /// public void NotifyCanExecuteChanged() { CanExecuteChanged?.Invoke(this, EventArgs.Empty); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CanExecute(object? parameter) { bool canExecute = this.canExecute?.Invoke() != false; return canExecute && ((this.options & AsyncRelayCommandOptions.AllowConcurrentExecutions) != 0 || ExecutionTask is not { IsCompleted: false }); } /// public void Execute(object? parameter) { Execute(null, parameter); } public void Execute(object?sender, object? parameter) { Task executionTask = ExecuteAsync(parameter); // If exceptions shouldn't flow to the task scheduler, await the resulting task. This is // delegated to a separate method to keep this one more compact in case the option is set. if ((this.options & AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler) == 0) { AwaitAndThrowIfFailed(executionTask); } } /// public Task ExecuteAsync(object? parameter) { return ExecuteAsync(null, parameter); } public Task ExecuteAsync(object? sender,object? parameter) { Task executionTask; if (this.execute is not null) { // Non cancelable command delegate executionTask = ExecutionTask = this.execute(); } else if(this.execute1 is not null) { executionTask = ExecutionTask = this.execute1(sender); } else { // Cancel the previous operation, if one is pending this.cancellationTokenSource?.Cancel(); CancellationTokenSource cancellationTokenSource = this.cancellationTokenSource = new(); // Invoke the cancelable command delegate with a new linked token executionTask = ExecutionTask = this.cancelableExecute1 != null ? this.cancelableExecute1!(sender,cancellationTokenSource.Token): this.cancelableExecute!(cancellationTokenSource.Token); } // If concurrent executions are disabled, notify the can execute change as well if ((this.options & AsyncRelayCommandOptions.AllowConcurrentExecutions) == 0) { CanExecuteChanged?.Invoke(this, EventArgs.Empty); } return executionTask; } /// public void Cancel() { if (this.cancellationTokenSource is CancellationTokenSource { IsCancellationRequested: false } cancellationTokenSource) { cancellationTokenSource.Cancel(); PropertyChanged?.Invoke(this, CanBeCanceledChangedEventArgs); PropertyChanged?.Invoke(this, IsCancellationRequestedChangedEventArgs); } } /// /// Awaits an input and throws an exception on the calling context, if the task fails. /// /// The input instance to await. internal static async void AwaitAndThrowIfFailed(Task executionTask) { // Note: this method is purposefully an async void method awaiting the input task. This is done so that // if an async relay command is invoked synchronously (ie. when Execute is called, eg. from a binding), // exceptions in the wrapped delegate will not be ignored or just become visible through the ExecutionTask // property, but will be rethrown in the original synchronization context by default. This makes the behavior // more consistent with how normal commands work (where exceptions are also just normally propagated to the // caller context), and avoids getting an app into an inconsistent state in case a method faults without // other components being notified. It is also possible to not await this task and to instead ignore exceptions // and then inspect them manually from the ExecutionTask property, by constructing an async command instance // using the AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler option. That will cause this call to // be skipped, and exceptions will just either normally be available through that property, or will otherwise // flow to the static TaskScheduler.UnobservedTaskException event if otherwise unobserved (eg. for logging). await executionTask; } }