// 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; 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 class AsyncRelayCommand : IAsyncRelayCommand { /// /// 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; /// /// The cancelable to invoke when is used. /// /// Only one between this and is not . private readonly Func? cancelableExecute; /// /// The optional action to invoke when is used. /// private readonly Func? canExecute; /// /// Indicates whether or not concurrent executions of the command are allowed. /// private readonly bool allowConcurrentExecutions; /// /// 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 that can always execute. /// /// The execution logic. /// Thrown if is . public AsyncRelayCommand(Func execute) { ArgumentNullException.ThrowIfNull(execute); this.execute = execute; this.allowConcurrentExecutions = true; } /// /// Initializes a new instance of the class that can always execute. /// /// The execution logic. /// Whether or not to allow concurrent executions of the command. /// 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 that can always execute. /// /// The cancelable execution logic. /// Thrown if is . public AsyncRelayCommand(Func cancelableExecute) { ArgumentNullException.ThrowIfNull(cancelableExecute); this.cancelableExecute = cancelableExecute; this.allowConcurrentExecutions = true; } /// /// Initializes a new instance of the class that can always execute. /// /// The cancelable execution logic. /// Whether or not to allow concurrent executions of the command. /// 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. /// Thrown if or are . public AsyncRelayCommand(Func execute, Func canExecute) { ArgumentNullException.ThrowIfNull(execute); ArgumentNullException.ThrowIfNull(canExecute); this.execute = execute; this.canExecute = canExecute; this.allowConcurrentExecutions = true; } /// /// Initializes a new instance of the class. /// /// The execution logic. /// The execution status logic. /// Whether or not to allow concurrent executions of the command. /// Thrown if or are . public AsyncRelayCommand(Func execute, Func 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. /// Thrown if or are . public AsyncRelayCommand(Func cancelableExecute, Func canExecute) { ArgumentNullException.ThrowIfNull(cancelableExecute); ArgumentNullException.ThrowIfNull(canExecute); this.cancelableExecute = cancelableExecute; this.canExecute = canExecute; this.allowConcurrentExecutions = true; } /// /// 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. /// Thrown if or are . public AsyncRelayCommand(Func cancelableExecute, Func 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, ExecutionTaskChangedEventArgs); PropertyChanged?.Invoke(this, IsRunningChangedEventArgs); PropertyChanged?.Invoke(this, CanBeCanceledChangedEventArgs); if (value?.IsCompleted ?? true) { return; } static async void MonitorTask(AsyncRelayCommand @this, Task task) { try { await task; } catch { } if (ReferenceEquals(@this.executionTask, task)) { @this.PropertyChanged?.Invoke(@this, ExecutionTaskChangedEventArgs); @this.PropertyChanged?.Invoke(@this, IsRunningChangedEventArgs); @this.PropertyChanged?.Invoke(@this, CanBeCanceledChangedEventArgs); } } MonitorTask(this, value!); } } /// public bool CanBeCanceled => this.cancelableExecute is not null && IsRunning; /// public bool IsCancellationRequested => this.cancellationTokenSource?.IsCancellationRequested == true; /// public bool IsRunning => ExecutionTask?.IsCompleted == false; /// 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.allowConcurrentExecutions || ExecutionTask?.IsCompleted != false); } /// public void Execute(object? parameter) { _ = ExecuteAsync(parameter); } /// public Task ExecuteAsync(object? parameter) { if (CanExecute(parameter)) { // Non cancelable command delegate if (this.execute is not null) { return ExecutionTask = this.execute(); } // Cancel the previous operation, if one is pending this.cancellationTokenSource?.Cancel(); CancellationTokenSource cancellationTokenSource = this.cancellationTokenSource = new(); PropertyChanged?.Invoke(this, IsCancellationRequestedChangedEventArgs); // Invoke the cancelable command delegate with a new linked token return ExecutionTask = this.cancelableExecute!(cancellationTokenSource.Token); } return Task.CompletedTask; } /// public void Cancel() { this.cancellationTokenSource?.Cancel(); PropertyChanged?.Invoke(this, IsCancellationRequestedChangedEventArgs); PropertyChanged?.Invoke(this, CanBeCanceledChangedEventArgs); } }