#region Apache License 2.0
/*
Nuclex Foundation libraries for .NET
Copyright (C) 2002-2026 Markus Ewald / Nuclex Development Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#endregion // Apache License 2.0
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
namespace Nuclex.Avalonia.Commands {
/// Asynchronous command that delegates work to an external method
/// Type of argument accepted by the command
internal sealed class AsyncRelayCommand : IAsyncCommand {
/// Raised when the command's executable state has changed
public event EventHandler? CanExecuteChanged;
/// Initializes a new asynchronous relay command
/// Action that is executed when the command runs
///
/// Optional predicate that decides whether execution is currently allowed
///
///
/// Whether the command may be executed while a previous execution is still running
///
public AsyncRelayCommand(
Func executeAsync,
Predicate? canExecute = null,
bool allowConcurrentExecutions = false
) {
this.executeAsync = executeAsync ?? throw new ArgumentNullException(nameof(executeAsync));
this.canExecute = canExecute;
this.allowConcurrentExecutions = allowConcurrentExecutions;
}
/// Whether the command is currently executing
public bool IsRunning {
get { return (Volatile.Read(ref this.executionCount) > 0); }
}
/// Checks whether the command may currently execute
/// Parameter to be passed to the command callback
/// True if command execution is currently allowed
public bool CanExecute(object? parameter) {
if((!this.allowConcurrentExecutions) && IsRunning) {
return false;
}
if(this.canExecute == null) {
return true;
}
return this.canExecute(getParameter(parameter));
}
/// Executes the command callback
/// Parameter passed to the command callback
public void Execute(object? parameter) {
_ = ExecuteAsync(getParameter(parameter));
}
/// Executes the command callback asynchronously
/// Parameter passed to the command callback
/// A task that finishes when command execution has completed
public async Task ExecuteAsync(TArgument parameter) {
if(!CanExecute(parameter)) {
return;
}
if(this.allowConcurrentExecutions) {
Interlocked.Increment(ref this.executionCount);
try {
await this.executeAsync(parameter).ConfigureAwait(false);
}
finally {
Interlocked.Decrement(ref this.executionCount);
}
} else {
Interlocked.Increment(ref this.executionCount);
NotifyCanExecuteChanged();
try {
await this.executeAsync(parameter).ConfigureAwait(false);
}
finally {
Interlocked.Decrement(ref this.executionCount);
NotifyCanExecuteChanged();
}
} // if concurrent executions allowed / not allowed
}
///
/// Notifies listeners that should be reevaluated
///
public void NotifyCanExecuteChanged() {
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
/// Converts and validates the untyped command parameter
/// Untyped parameter passed through
/// The parameter cast to the command's parameter type
private static TArgument getParameter(object? parameter) {
if(parameter is TArgument typedParameter) {
return typedParameter;
}
if(parameter == null) {
if(default(TArgument) == null) {
return default!;
}
throw new ArgumentException(
"This command expects a non-null parameter of the configured type.",
nameof(parameter)
);
}
throw new ArgumentException(
$"This command expects a parameter of type {typeof(TArgument).FullName}.",
nameof(parameter)
);
}
/// Asynchronous callback invoked when command execution is requested
private readonly Func executeAsync;
/// Optional callback deciding whether command execution is currently allowed
private readonly Predicate? canExecute;
/// Whether the command may run while a previous invocation is still active
private readonly bool allowConcurrentExecutions;
/// Number of currently active command executions
private int executionCount;
}
} // namespace Nuclex.Avalonia.Commands