提交 5dca30b8 编写于 作者: D Dan Siegel

feat: adding AsyncDelegateCommand

上级 a128f206
using System.Linq.Expressions;
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Prism.Properties;
#nullable enable
namespace Prism.Commands;
/// <summary>
/// Provides an implementation of the <see cref="IAsyncCommand"/>
/// </summary>
public class AsyncDelegateCommand : DelegateCommandBase, IAsyncCommand
{
private bool _enableParallelExecution = false;
private bool _isExecuting = false;
private readonly Func<CancellationToken, Task> _executeMethod;
private Func<bool> _canExecuteMethod;
/// <summary>
/// Creates a new instance of <see cref="AsyncDelegateCommand"/> with the <see cref="Func{Task}"/> to invoke on execution.
/// </summary>
/// <param name="executeMethod">The <see cref="Func{Task}"/> to invoke when <see cref="ICommand.Execute(object)"/> is called.</param>
public AsyncDelegateCommand(Func<Task> executeMethod)
: this(c => executeMethod(), () => true)
{
}
/// <summary>
/// Creates a new instance of <see cref="AsyncDelegateCommand"/> with the <see cref="Func{Task}"/> to invoke on execution.
/// </summary>
/// <param name="executeMethod">The <see cref="Func{CancellationToken, Task}"/> to invoke when <see cref="ICommand.Execute(object)"/> is called.</param>
public AsyncDelegateCommand(Func<CancellationToken, Task> executeMethod)
: this(executeMethod, () => true)
{
}
/// <summary>
/// Creates a new instance of <see cref="DelegateCommand"/> with the <see cref="Func{Task}"/> to invoke on execution
/// and a <see langword="Func" /> to query for determining if the command can execute.
/// </summary>
/// <param name="executeMethod">The <see cref="Func{Task}"/> to invoke when <see cref="ICommand.Execute"/> is called.</param>
/// <param name="canExecuteMethod">The delegate to invoke when <see cref="ICommand.CanExecute"/> is called</param>
public AsyncDelegateCommand(Func<Task> executeMethod, Func<bool> canExecuteMethod)
: this(c => executeMethod(), canExecuteMethod)
{
}
/// <summary>
/// Creates a new instance of <see cref="DelegateCommand"/> with the <see cref="Func{Task}"/> to invoke on execution
/// and a <see langword="Func" /> to query for determining if the command can execute.
/// </summary>
/// <param name="executeMethod">The <see cref="Func{CancellationToken, Task}"/> to invoke when <see cref="ICommand.Execute"/> is called.</param>
/// <param name="canExecuteMethod">The delegate to invoke when <see cref="ICommand.CanExecute"/> is called</param>
public AsyncDelegateCommand(Func<CancellationToken, Task> executeMethod, Func<bool> canExecuteMethod)
: base()
{
if (executeMethod == null || canExecuteMethod == null)
throw new ArgumentNullException(nameof(executeMethod), Resources.DelegateCommandDelegatesCannotBeNull);
_executeMethod = executeMethod;
_canExecuteMethod = canExecuteMethod;
}
/// <summary>
/// Gets the current state of the AsyncDelegateCommand
/// </summary>
public bool IsExecuting
{
get => _isExecuting;
private set => SetProperty(ref _isExecuting, value, OnCanExecuteChanged);
}
///<summary>
/// Executes the command.
///</summary>
public async Task Execute(CancellationToken cancellationToken = default)
{
try
{
IsExecuting = true;
await _executeMethod(cancellationToken);
}
catch (TaskCanceledException)
{
// Do nothing... the Task was cancelled
}
catch (Exception ex)
{
if (!ExceptionHandler.CanHandle(ex))
throw;
ExceptionHandler.Handle(ex, null);
}
finally
{
IsExecuting = false;
}
}
/// <summary>
/// Determines if the command can be executed.
/// </summary>
/// <returns>Returns <see langword="true"/> if the command can execute,otherwise returns <see langword="false"/>.</returns>
public bool CanExecute()
{
try
{
if (!_enableParallelExecution && IsExecuting)
return false;
return _canExecuteMethod?.Invoke() ?? true;
}
catch (Exception ex)
{
if (!ExceptionHandler.CanHandle(ex))
throw;
ExceptionHandler.Handle(ex, null);
return false;
}
}
/// <summary>
/// Handle the internal invocation of <see cref="ICommand.Execute(object)"/>
/// </summary>
/// <param name="parameter">Command Parameter</param>
protected override async void Execute(object parameter)
{
await Execute();
}
/// <summary>
/// Handle the internal invocation of <see cref="ICommand.CanExecute(object)"/>
/// </summary>
/// <param name="parameter"></param>
/// <returns><see langword="true"/> if the Command Can Execute, otherwise <see langword="false" /></returns>
protected override bool CanExecute(object parameter)
{
return CanExecute();
}
/// <summary>
/// Enables Parallel Execution of Async Tasks
/// </summary>
/// <returns></returns>
public AsyncDelegateCommand EnableParallelExecution()
{
_enableParallelExecution = true;
return this;
}
/// <summary>
/// Observes a property that implements INotifyPropertyChanged, and automatically calls DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications.
/// </summary>
/// <typeparam name="T">The object type containing the property specified in the expression.</typeparam>
/// <param name="propertyExpression">The property expression. Example: ObservesProperty(() => PropertyName).</param>
/// <returns>The current instance of DelegateCommand</returns>
public AsyncDelegateCommand ObservesProperty<T>(Expression<Func<T>> propertyExpression)
{
ObservesPropertyInternal(propertyExpression);
return this;
}
/// <summary>
/// Observes a property that is used to determine if this command can execute, and if it implements INotifyPropertyChanged it will automatically call DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications.
/// </summary>
/// <param name="canExecuteExpression">The property expression. Example: ObservesCanExecute(() => PropertyName).</param>
/// <returns>The current instance of DelegateCommand</returns>
public AsyncDelegateCommand ObservesCanExecute(Expression<Func<bool>> canExecuteExpression)
{
_canExecuteMethod = canExecuteExpression.Compile();
ObservesPropertyInternal(canExecuteExpression);
return this;
}
/// <summary>
/// Provides the ability to connect a delegate to catch exceptions encountered by CanExecute or the Execute methods of the DelegateCommand
/// </summary>
/// <param name="catch">TThe callback when a specific exception is encountered</param>
/// <returns>The current instance of DelegateCommand</returns>
public AsyncDelegateCommand Catch<TException>(Action<TException> @catch)
where TException : Exception
{
ExceptionHandler.Register<TException>(@catch);
return this;
}
/// <summary>
/// Provides the ability to connect a delegate to catch exceptions encountered by CanExecute or the Execute methods of the DelegateCommand
/// </summary>
/// <param name="catch">The generic / default callback when an exception is encountered</param>
/// <returns>The current instance of DelegateCommand</returns>
public AsyncDelegateCommand Catch(Action<Exception> @catch)
{
ExceptionHandler.Register<Exception>(@catch);
return this;
}
Task IAsyncCommand.ExecuteAsync(object? parameter)
{
return Execute(default);
}
Task IAsyncCommand.ExecuteAsync(object? parameter, CancellationToken cancellationToken)
{
return Execute(cancellationToken);
}
}
using System.Linq.Expressions;
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Prism.Properties;
#nullable enable
namespace Prism.Commands;
/// <summary>
/// Provides an implementation of the <see cref="IAsyncCommand"/> with a generic parameter type.
/// </summary>
/// <typeparam name="T"></typeparam>
public class AsyncDelegateCommand<T> : DelegateCommandBase, IAsyncCommand
{
private bool _enableParallelExecution = false;
private bool _isExecuting = false;
private readonly Func<T, CancellationToken, Task> _executeMethod;
private Func<T, bool> _canExecuteMethod;
/// <summary>
/// Creates a new instance of <see cref="AsyncDelegateCommand{T}"/> with the <see cref="Func{Task}"/> to invoke on execution.
/// </summary>
/// <param name="executeMethod">The <see cref="Func{T, Task}"/> to invoke when <see cref="ICommand.Execute(object)"/> is called.</param>
public AsyncDelegateCommand(Func<T, Task> executeMethod)
: this((p, t) => executeMethod(p), _ => true)
{
}
/// <summary>
/// Creates a new instance of <see cref="AsyncDelegateCommand{T}"/> with the <see cref="Func{Task}"/> to invoke on execution.
/// </summary>
/// <param name="executeMethod">The <see cref="Func{T, CancellationToken, Task}"/> to invoke when <see cref="ICommand.Execute(object)"/> is called.</param>
public AsyncDelegateCommand(Func<T, CancellationToken, Task> executeMethod)
: this(executeMethod, _ => true)
{
}
/// <summary>
/// Creates a new instance of <see cref="DelegateCommand"/> with the <see cref="Func{Task}"/> to invoke on execution
/// and a <see langword="Func" /> to query for determining if the command can execute.
/// </summary>
/// <param name="executeMethod">The <see cref="Func{T, Task}"/> to invoke when <see cref="ICommand.Execute"/> is called.</param>
/// <param name="canExecuteMethod">The delegate to invoke when <see cref="ICommand.CanExecute"/> is called</param>
public AsyncDelegateCommand(Func<T, Task> executeMethod, Func<T, bool> canExecuteMethod)
: this((p, c) => executeMethod(p), canExecuteMethod)
{
}
/// <summary>
/// Creates a new instance of <see cref="DelegateCommand"/> with the <see cref="Func{Task}"/> to invoke on execution
/// and a <see langword="Func" /> to query for determining if the command can execute.
/// </summary>
/// <param name="executeMethod">The <see cref="Func{T, CancellationToken, Task}"/> to invoke when <see cref="ICommand.Execute"/> is called.</param>
/// <param name="canExecuteMethod">The delegate to invoke when <see cref="ICommand.CanExecute"/> is called</param>
public AsyncDelegateCommand(Func<T, CancellationToken, Task> executeMethod, Func<T, bool> canExecuteMethod)
: base()
{
if (executeMethod == null || canExecuteMethod == null)
throw new ArgumentNullException(nameof(executeMethod), Resources.DelegateCommandDelegatesCannotBeNull);
_executeMethod = executeMethod;
_canExecuteMethod = canExecuteMethod;
}
/// <summary>
/// Gets the current state of the AsyncDelegateCommand
/// </summary>
public bool IsExecuting
{
get => _isExecuting;
private set => SetProperty(ref _isExecuting, value, OnCanExecuteChanged);
}
///<summary>
/// Executes the command.
///</summary>
public async Task Execute(T parameter, CancellationToken cancellationToken = default)
{
try
{
IsExecuting = true;
await _executeMethod(parameter, cancellationToken);
}
catch (TaskCanceledException)
{
// Do nothing... the Task was cancelled
}
catch (Exception ex)
{
if (!ExceptionHandler.CanHandle(ex))
throw;
ExceptionHandler.Handle(ex, parameter);
}
finally
{
IsExecuting = false;
}
}
/// <summary>
/// Determines if the command can be executed.
/// </summary>
/// <returns>Returns <see langword="true"/> if the command can execute,otherwise returns <see langword="false"/>.</returns>
public bool CanExecute(T parameter)
{
try
{
if (!_enableParallelExecution && IsExecuting)
return false;
return _canExecuteMethod?.Invoke(parameter) ?? true;
}
catch (Exception ex)
{
if (!ExceptionHandler.CanHandle(ex))
throw;
ExceptionHandler.Handle(ex, parameter);
return false;
}
}
/// <summary>
/// Handle the internal invocation of <see cref="ICommand.Execute(object)"/>
/// </summary>
/// <param name="parameter">Command Parameter</param>
protected override async void Execute(object parameter)
{
try
{
await Execute((T)parameter);
}
catch (Exception ex)
{
if (!ExceptionHandler.CanHandle(ex))
throw;
ExceptionHandler.Handle(ex, parameter);
}
}
/// <summary>
/// Handle the internal invocation of <see cref="ICommand.CanExecute(object)"/>
/// </summary>
/// <param name="parameter"></param>
/// <returns><see langword="true"/> if the Command Can Execute, otherwise <see langword="false" /></returns>
protected override bool CanExecute(object parameter)
{
try
{
return CanExecute((T)parameter);
}
catch (Exception ex)
{
if (!ExceptionHandler.CanHandle(ex))
throw;
ExceptionHandler.Handle(ex, parameter);
return false;
}
}
/// <summary>
/// Enables Parallel Execution of Async Tasks
/// </summary>
/// <returns></returns>
public AsyncDelegateCommand<T> EnableParallelExecution()
{
_enableParallelExecution = true;
return this;
}
/// <summary>
/// Observes a property that implements INotifyPropertyChanged, and automatically calls DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications.
/// </summary>
/// <typeparam name="TType">The type of the return value of the method that this delegate encapsulates</typeparam>
/// <param name="propertyExpression">The property expression. Example: ObservesProperty(() => PropertyName).</param>
/// <returns>The current instance of DelegateCommand</returns>
public AsyncDelegateCommand<T> ObservesProperty<TType>(Expression<Func<TType>> propertyExpression)
{
ObservesPropertyInternal(propertyExpression);
return this;
}
/// <summary>
/// Observes a property that is used to determine if this command can execute, and if it implements INotifyPropertyChanged it will automatically call DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications.
/// </summary>
/// <param name="canExecuteExpression">The property expression. Example: ObservesCanExecute(() => PropertyName).</param>
/// <returns>The current instance of DelegateCommand</returns>
public AsyncDelegateCommand<T> ObservesCanExecute(Expression<Func<bool>> canExecuteExpression)
{
Expression<Func<T, bool>> expression = Expression.Lambda<Func<T, bool>>(canExecuteExpression.Body, Expression.Parameter(typeof(T), "o"));
_canExecuteMethod = expression.Compile();
ObservesPropertyInternal(canExecuteExpression);
return this;
}
/// <summary>
/// Provides the ability to connect a delegate to catch exceptions encountered by CanExecute or the Execute methods of the DelegateCommand
/// </summary>
/// <param name="catch">TThe callback when a specific exception is encountered</param>
/// <returns>The current instance of DelegateCommand</returns>
public AsyncDelegateCommand<T> Catch<TException>(Action<TException> @catch)
where TException : Exception
{
ExceptionHandler.Register<TException>(@catch);
return this;
}
/// <summary>
/// Provides the ability to connect a delegate to catch exceptions encountered by CanExecute or the Execute methods of the DelegateCommand
/// </summary>
/// <param name="catch">The generic / default callback when an exception is encountered</param>
/// <returns>The current instance of DelegateCommand</returns>
public AsyncDelegateCommand<T> Catch(Action<Exception> @catch)
{
ExceptionHandler.Register<Exception>(@catch);
return this;
}
async Task IAsyncCommand.ExecuteAsync(object? parameter)
{
try
{
// If T is not nullable this may throw an exception
await Execute((T)parameter, default);
}
catch (Exception ex)
{
if (!ExceptionHandler.CanHandle(ex))
throw;
ExceptionHandler.Handle(ex, parameter);
}
}
async Task IAsyncCommand.ExecuteAsync(object? parameter, CancellationToken cancellationToken)
{
try
{
// If T is not nullable this may throw an exception
await Execute((T)parameter, cancellationToken);
}
catch (Exception ex)
{
if (!ExceptionHandler.CanHandle(ex))
throw;
ExceptionHandler.Handle(ex, parameter);
}
}
}
......@@ -5,13 +5,14 @@ using System.Linq.Expressions;
using System.Threading;
using System.Windows.Input;
using Prism.Common;
using Prism.Mvvm;
namespace Prism.Commands
{
/// <summary>
/// An <see cref="ICommand"/> whose delegates can be attached for <see cref="Execute"/> and <see cref="CanExecute"/>.
/// </summary>
public abstract class DelegateCommandBase : ICommand, IActiveAware
public abstract class DelegateCommandBase : BindableBase, ICommand, IActiveAware
{
private bool _isActive;
......@@ -113,15 +114,8 @@ namespace Prism.Commands
/// <value><see langword="true" /> if the object is active; otherwise <see langword="false" />.</value>
public bool IsActive
{
get { return _isActive; }
set
{
if (_isActive != value)
{
_isActive = value;
OnIsActiveChanged();
}
}
get => _isActive;
set => SetProperty(ref _isActive, value, OnIsActiveChanged);
}
/// <summary>
......
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
#nullable enable
namespace Prism.Commands;
/// <summary>
/// Provides an abstraction layer for custom controls which want to make use of Async Commands
/// </summary>
public interface IAsyncCommand : ICommand
{
/// <summary>
/// Executes the Command with a specified parameter and the Default <see cref="CancellationToken"/>.
/// </summary>
/// <param name="parameter">The Command Parameter</param>
/// <returns>An Asynchronous Task</returns>
Task ExecuteAsync(object? parameter);
/// <summary>
/// Executes the Command with a specified parameter and using a <see cref="CancellationToken"/>
/// </summary>
/// <param name="parameter">The Command Parameter</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <returns>An Asynchronous Task</returns>
Task ExecuteAsync(object? parameter, CancellationToken cancellationToken);
}
using System.Threading.Tasks;
using System.Threading;
using Prism.Commands;
using Xunit;
namespace Prism.Tests.Commands;
public class AsyncDelegateCommandFixture
{
[Fact]
public void WhenConstructedWithDelegate_InitializesValues()
{
var actual = new AsyncDelegateCommand(() => default);
Assert.NotNull(actual);
}
[Fact]
public async Task CannotExecuteWhileExecuting()
{
var tcs = new TaskCompletionSource<object>();
var command = new AsyncDelegateCommand(async () => await tcs.Task);
Assert.True(command.CanExecute());
var task = command.Execute();
Assert.False(command.CanExecute());
tcs.SetResult("complete");
await task;
Assert.True(command.CanExecute());
}
[Fact]
public async Task CanExecuteParallelTaskWhenEnabled()
{
var tcs = new TaskCompletionSource<object>();
var command = new AsyncDelegateCommand(async () => await tcs.Task)
.EnableParallelExecution();
Assert.True(command.CanExecute());
var task = command.Execute();
Assert.True(command.CanExecute());
tcs.SetResult("complete");
await task;
Assert.True(command.CanExecute());
}
[Fact]
public async Task CanExecuteChangedFiresWhenExecuting()
{
var tcs = new TaskCompletionSource<object> ();
var command = new AsyncDelegateCommand(async () => await tcs.Task);
bool canExecuteChanged = false;
command.CanExecuteChanged += Command_CanExecuteChanged;
void Command_CanExecuteChanged(object sender, System.EventArgs e)
{
canExecuteChanged = true;
}
var task = command.Execute();
command.CanExecuteChanged -= Command_CanExecuteChanged;
Assert.True(command.IsExecuting);
Assert.True(canExecuteChanged);
tcs.SetResult(null);
await task;
Assert.False(command.IsExecuting);
}
[Fact]
public async Task ExecuteAsync_ShouldExecuteCommandAsynchronously()
{
// Arrange
bool executed = false;
var tcs = new TaskCompletionSource<object>();
var command = new AsyncDelegateCommand(async (_) =>
{
await tcs.Task;
executed = true;
});
// Act
var task = command.Execute();
Assert.False(executed);
tcs.SetResult("complete");
await task;
// Assert
Assert.True(executed);
}
[Fact]
public async Task ExecuteAsync_WithCancellationToken_ShouldExecuteCommandAsynchronously()
{
// Arrange
bool executionStarted = false;
bool executed = false;
var command = new AsyncDelegateCommand(Execute);
async Task Execute(CancellationToken token)
{
executionStarted = true;
await Task.Delay(1000, token);
executed = true;
}
// Act
using (var cancellationTokenSource = new CancellationTokenSource())
{
cancellationTokenSource.CancelAfter(50); // Cancel after 50 milliseconds
await command.Execute(cancellationTokenSource.Token);
}
// Assert
Assert.True(executionStarted);
Assert.False(executed);
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册