// 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 namespace CommunityToolkit.Mvvm.Input; /// /// A generic command that provides a more specific version of . /// /// The type of parameter being passed as input to the callbacks. public sealed class AsyncRelayCommand : IAsyncRelayCommand, ICancellationAwareCommand { /// /// The to invoke when is used. /// private readonly Func? execute; /// /// The cancelable to invoke when is used. /// private readonly Func? cancelableExecute; /// /// The optional action to invoke when is used. /// private readonly Predicate? canExecute; /// /// Indicates whether or not concurrent executions of the command are allowed. /// private readonly bool allowConcurrentExecutions; /// /// The instance to use to cancel . /// private CancellationTokenSource? cancellationTokenSource; /// public event PropertyChangedEventHandler? PropertyChanged; /// public event EventHandler? CanExecuteChanged; /// /// Initializes a new instance of the class. /// /// The execution logic. /// See notes in . /// Thrown if is . public AsyncRelayCommand(Func execute) { ArgumentNullException.ThrowIfNull(execute); this.execute = execute; } /// /// Initializes a new instance of the class. /// /// The execution logic. /// Whether or not to allow concurrent executions of the command. /// See notes in . /// Thrown if is . public AsyncRelayCommand(Func execute, bool allowConcurrentExecutions) { ArgumentNullException.ThrowIfNull(execute); this.execute = execute; this.allowConcurrentExecutions = allowConcurrentExecutions; } /// /// Initializes a new instance of the class. /// /// The cancelable execution logic. /// See notes in . /// Thrown if is . public AsyncRelayCommand(Func cancelableExecute) { ArgumentNullException.ThrowIfNull(cancelableExecute); this.cancelableExecute = cancelableExecute; } /// /// Initializes a new instance of the class. /// /// The cancelable execution logic. /// Whether or not to allow concurrent executions of the command. /// See notes in . /// Thrown if is . public AsyncRelayCommand(Func cancelableExecute, bool allowConcurrentExecutions) { ArgumentNullException.ThrowIfNull(cancelableExecute); this.cancelableExecute = cancelableExecute; this.allowConcurrentExecutions = allowConcurrentExecutions; } /// /// Initializes a new instance of the class. /// /// The execution logic. /// The execution status logic. /// See notes in . /// Thrown if or are . public AsyncRelayCommand(Func execute, Predicate canExecute) { ArgumentNullException.ThrowIfNull(execute); ArgumentNullException.ThrowIfNull(canExecute); this.execute = execute; this.canExecute = canExecute; } /// /// Initializes a new instance of the class. /// /// The execution logic. /// The execution status logic. /// Whether or not to allow concurrent executions of the command. /// See notes in . /// Thrown if or are . public AsyncRelayCommand(Func execute, Predicate canExecute, bool allowConcurrentExecutions) { ArgumentNullException.ThrowIfNull(execute); ArgumentNullException.ThrowIfNull(canExecute); this.execute = execute; this.canExecute = canExecute; this.allowConcurrentExecutions = allowConcurrentExecutions; } /// /// Initializes a new instance of the class. /// /// The cancelable execution logic. /// The execution status logic. /// See notes in . /// Thrown if or are . public AsyncRelayCommand(Func cancelableExecute, Predicate canExecute) { ArgumentNullException.ThrowIfNull(cancelableExecute); ArgumentNullException.ThrowIfNull(canExecute); this.cancelableExecute = cancelableExecute; this.canExecute = canExecute; } /// /// Initializes a new instance of the class. /// /// The cancelable execution logic. /// The execution status logic. /// Whether or not to allow concurrent executions of the command. /// See notes in . /// Thrown if or are . public AsyncRelayCommand(Func cancelableExecute, Predicate canExecute, bool allowConcurrentExecutions) { ArgumentNullException.ThrowIfNull(cancelableExecute); ArgumentNullException.ThrowIfNull(canExecute); this.cancelableExecute = cancelableExecute; this.canExecute = canExecute; this.allowConcurrentExecutions = allowConcurrentExecutions; } private Task? executionTask; /// public Task? ExecutionTask { get => this.executionTask; private set { if (ReferenceEquals(this.executionTask, value)) { return; } this.executionTask = value; PropertyChanged?.Invoke(this, AsyncRelayCommand.ExecutionTaskChangedEventArgs); PropertyChanged?.Invoke(this, AsyncRelayCommand.IsRunningChangedEventArgs); bool isAlreadyCompletedOrNull = value?.IsCompleted ?? true; if (this.cancellationTokenSource is not null) { PropertyChanged?.Invoke(this, AsyncRelayCommand.CanBeCanceledChangedEventArgs); PropertyChanged?.Invoke(this, AsyncRelayCommand.IsCancellationRequestedChangedEventArgs); } if (isAlreadyCompletedOrNull) { return; } static async void MonitorTask(AsyncRelayCommand @this, Task task) { await task.GetAwaitableWithoutEndValidation(); if (ReferenceEquals(@this.executionTask, task)) { @this.PropertyChanged?.Invoke(@this, AsyncRelayCommand.ExecutionTaskChangedEventArgs); @this.PropertyChanged?.Invoke(@this, AsyncRelayCommand.IsRunningChangedEventArgs); if (@this.cancellationTokenSource is not null) { @this.PropertyChanged?.Invoke(@this, AsyncRelayCommand.CanBeCanceledChangedEventArgs); } if (!@this.allowConcurrentExecutions) { @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(T? parameter) { bool canExecute = this.canExecute?.Invoke(parameter) != false; return canExecute && (this.allowConcurrentExecutions || ExecutionTask is not { IsCompleted: false }); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool CanExecute(object? parameter) { if (default(T) is not null && parameter is null) { return false; } return CanExecute((T?)parameter); } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public async void Execute(T? parameter) { await ExecuteAsync(parameter).ConfigureAwait(false); } /// public async void Execute(object? parameter) { await ExecuteAsync((T?)parameter).ConfigureAwait(false); } /// public Task ExecuteAsync(T? parameter) { Task executionTask; if (this.execute is not null) { // Non cancelable command delegate executionTask = ExecutionTask = this.execute(parameter); } 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.cancelableExecute!(parameter, cancellationTokenSource.Token); } // If concurrent executions are disabled, notify the can execute change as well if (!this.allowConcurrentExecutions) { CanExecuteChanged?.Invoke(this, EventArgs.Empty); } return executionTask; } /// public Task ExecuteAsync(object? parameter) { return ExecuteAsync((T?)parameter); } /// public void Cancel() { if (this.cancellationTokenSource is CancellationTokenSource { IsCancellationRequested: false } cancellationTokenSource) { cancellationTokenSource.Cancel(); PropertyChanged?.Invoke(this, AsyncRelayCommand.CanBeCanceledChangedEventArgs); PropertyChanged?.Invoke(this, AsyncRelayCommand.IsCancellationRequestedChangedEventArgs); } } }