Skip to content

Commit

Permalink
Merge pull request #2993 from PrismLibrary/dev/ds/asynccommand-enhanc…
Browse files Browse the repository at this point in the history
…ements
  • Loading branch information
dansiegel authored Nov 14, 2023
2 parents fdc0bbd + 56e4d88 commit b5c5f72
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 14 deletions.
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 @@ -129,7 +129,18 @@ public async Task ICommandExecute_UsesDefaultTokenSourceFactory()

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);
}
}

0 comments on commit b5c5f72

Please sign in to comment.