Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AsyncDelegateCommand enhancements #2993

Merged
merged 9 commits into from
Nov 14, 2023
35 changes: 28 additions & 7 deletions src/Prism.Core/Commands/AsyncDelegateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ public class AsyncDelegateCommand : DelegateCommandBase, IAsyncCommand
/// </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)
#if NET6_0_OR_GREATER
: this (c => executeMethod().WaitAsync(c), () => true)
#else
: this(c => executeMethod(), () => true)
#endif
{

}
Expand All @@ -46,7 +50,11 @@ public AsyncDelegateCommand(Func<CancellationToken, Task> executeMethod)
/// <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)
#if NET6_0_OR_GREATER
: this(c => executeMethod().WaitAsync(c), canExecuteMethod)
#else
: this(c => executeMethod(), canExecuteMethod)
#endif
{
}

Expand Down Expand Up @@ -78,16 +86,17 @@ public bool IsExecuting
///<summary>
/// Executes the command.
///</summary>
public async Task Execute(CancellationToken cancellationToken = default)
public async Task Execute(CancellationToken? cancellationToken = null)
{
var token = cancellationToken ?? _getCancellationToken();
try
{
if (!_enableParallelExecution && IsExecuting)
return;

IsExecuting = true;
await _executeMethod(cancellationToken);
}
catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested)
{
// Do nothing... the Task was cancelled
await _executeMethod(token)
.ConfigureAwait(false);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -132,7 +141,11 @@ public bool CanExecute()
/// <param name="parameter">Command Parameter</param>
protected override async void Execute(object? parameter)
{
await Execute(_getCancellationToken());
// We don't want to wrap this in a try/catch because we already handle
// or mean to rethrow the exception in the call with the CancellationToken.
var cancellationToken = _getCancellationToken();
await Execute(cancellationToken)
.ConfigureAwait(false);
}

/// <summary>
Expand All @@ -155,6 +168,14 @@ public AsyncDelegateCommand EnableParallelExecution()
return this;
}

/// <summary>
/// Sets the <see cref="CancellationTokenSourceFactory(Func{CancellationToken})"/> based on the specified timeout.
/// </summary>
/// <param name="timeout">A specified timeout.</param>
/// <returns>The current instance of <see cref="AsyncDelegateCommand{T}"/>.</returns>
public AsyncDelegateCommand CancelAfter(TimeSpan timeout) =>
CancellationTokenSourceFactory(() => new CancellationTokenSource(timeout).Token);

/// <summary>
/// Provides a delegate callback to provide a default CancellationToken when the Command is invoked.
/// </summary>
Expand Down
41 changes: 34 additions & 7 deletions src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ public class AsyncDelegateCommand<T> : DelegateCommandBase, IAsyncCommand
/// </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)
#if NET6_0_OR_GREATER
: this((p,t) => executeMethod(p).WaitAsync(t), _ => true)
#else
: this((p, t) => executeMethod(p), _ => true)
#endif
{

}
Expand All @@ -47,7 +51,11 @@ public AsyncDelegateCommand(Func<T, CancellationToken, Task> executeMethod)
/// <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)
#if NET6_0_OR_GREATER
: this((p, c) => executeMethod(p).WaitAsync(c), canExecuteMethod)
#else
: this((p, c) => executeMethod(p), canExecuteMethod)
#endif
{

}
Expand Down Expand Up @@ -80,16 +88,18 @@ public bool IsExecuting
///<summary>
/// Executes the command.
///</summary>
public async Task Execute(T parameter, CancellationToken cancellationToken = default)
public async Task Execute(T parameter, CancellationToken? cancellationToken = null)
{
var token = cancellationToken ?? _getCancellationToken();

try
{
if (!_enableParallelExecution && IsExecuting)
return;

IsExecuting = true;
await _executeMethod(parameter, cancellationToken);
}
catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested)
{
// Do nothing... the Task was cancelled
await _executeMethod(parameter, token)
.ConfigureAwait(false);
}
catch (Exception ex)
{
Expand Down Expand Up @@ -134,17 +144,26 @@ public bool CanExecute(T parameter)
/// <param name="parameter">Command Parameter</param>
protected override async void Execute(object? parameter)
{
var cancellationToken = _getCancellationToken();
T parameterAsT;
try
{
await Execute((T)parameter!, _getCancellationToken());
parameterAsT = (T)parameter!;
}
catch (Exception ex)
{
if (!ExceptionHandler.CanHandle(ex))
throw;

ExceptionHandler.Handle(ex, parameter);
return;
}

// If we had an exception casting the parameter to T ,
// we would have already returned. We want to surface any
// exceptions thrown by the Execute method.
await Execute(parameterAsT, cancellationToken)
.ConfigureAwait(false);
}

/// <summary>
Expand Down Expand Up @@ -179,6 +198,14 @@ public AsyncDelegateCommand<T> EnableParallelExecution()
return this;
}

/// <summary>
/// Sets the <see cref="CancellationTokenSourceFactory(Func{CancellationToken})"/> based on the specified timeout.
/// </summary>
/// <param name="timeout">A specified timeout.</param>
/// <returns>The current instance of <see cref="AsyncDelegateCommand{T}"/>.</returns>
public AsyncDelegateCommand<T> CancelAfter(TimeSpan timeout) =>
CancellationTokenSourceFactory(() => new CancellationTokenSource(timeout).Token);

/// <summary>
/// Provides a delegate callback to provide a default CancellationToken when the Command is invoked.
/// </summary>
Expand Down
11 changes: 11 additions & 0 deletions tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
using (var cancellationTokenSource = new CancellationTokenSource())
{
cancellationTokenSource.CancelAfter(50); // Cancel after 50 milliseconds
await command.Execute(cancellationTokenSource.Token);

Check failure on line 113 in tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs

View workflow job for this annotation

GitHub Actions / build-prism-wpf / Build Prism.Wpf

Prism.Tests.Commands.AsyncDelegateCommandFixture.ExecuteAsync_WithCancellationToken_ShouldExecuteCommandAsynchronously

System.Threading.Tasks.TaskCanceledException : A task was canceled.

Check failure on line 113 in tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs

View workflow job for this annotation

GitHub Actions / build-prism-core / Build Prism.Core

Prism.Tests.Commands.AsyncDelegateCommandFixture.ExecuteAsync_WithCancellationToken_ShouldExecuteCommandAsynchronously

System.Threading.Tasks.TaskCanceledException : A task was canceled.

Check failure on line 113 in tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs

View workflow job for this annotation

GitHub Actions / build-prism-maui / Build Prism.Maui

Prism.Tests.Commands.AsyncDelegateCommandFixture.ExecuteAsync_WithCancellationToken_ShouldExecuteCommandAsynchronously

System.Threading.Tasks.TaskCanceledException : A task was canceled.

Check failure on line 113 in tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs

View workflow job for this annotation

GitHub Actions / build-prism-forms / Build Prism.Forms

Prism.Tests.Commands.AsyncDelegateCommandFixture.ExecuteAsync_WithCancellationToken_ShouldExecuteCommandAsynchronously

System.Threading.Tasks.TaskCanceledException : A task was canceled.
}

// Assert
Expand All @@ -129,7 +129,18 @@

Assert.True(command.IsExecuting);
cts.Cancel();
await Task.Delay(10);

Assert.False(command.IsExecuting);
}

[Fact]
public void ICommandExecute_HandlesErrorOnce()
{
var handled = 0;
ICommand command = new AsyncDelegateCommand<string>(str => throw new System.Exception("Test"))
.Catch(ex => handled++);
command.Execute(string.Empty);
Assert.Equal(1, handled);
}
}
Loading