diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand.cs b/src/Prism.Core/Commands/AsyncDelegateCommand.cs index ad4082d160..8c7eec460f 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand.cs @@ -24,7 +24,11 @@ public class AsyncDelegateCommand : DelegateCommandBase, IAsyncCommand /// /// The to invoke when is called. public AsyncDelegateCommand(Func executeMethod) +#if NET6_0_OR_GREATER + : this (c => executeMethod().WaitAsync(c), () => true) +#else : this(c => executeMethod(), () => true) +#endif { } @@ -46,7 +50,11 @@ public AsyncDelegateCommand(Func executeMethod) /// The to invoke when is called. /// The delegate to invoke when is called public AsyncDelegateCommand(Func executeMethod, Func canExecuteMethod) +#if NET6_0_OR_GREATER + : this(c => executeMethod().WaitAsync(c), canExecuteMethod) +#else : this(c => executeMethod(), canExecuteMethod) +#endif { } @@ -78,16 +86,17 @@ public bool IsExecuting /// /// Executes the command. /// - 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) { @@ -132,7 +141,11 @@ public bool CanExecute() /// Command Parameter 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); } /// @@ -155,6 +168,14 @@ public AsyncDelegateCommand EnableParallelExecution() return this; } + /// + /// Sets the based on the specified timeout. + /// + /// A specified timeout. + /// The current instance of . + public AsyncDelegateCommand CancelAfter(TimeSpan timeout) => + CancellationTokenSourceFactory(() => new CancellationTokenSource(timeout).Token); + /// /// Provides a delegate callback to provide a default CancellationToken when the Command is invoked. /// diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs index ed257ce196..a449f5c67b 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs @@ -25,7 +25,11 @@ public class AsyncDelegateCommand : DelegateCommandBase, IAsyncCommand /// /// The to invoke when is called. public AsyncDelegateCommand(Func executeMethod) +#if NET6_0_OR_GREATER + : this((p,t) => executeMethod(p).WaitAsync(t), _ => true) +#else : this((p, t) => executeMethod(p), _ => true) +#endif { } @@ -47,7 +51,11 @@ public AsyncDelegateCommand(Func executeMethod) /// The to invoke when is called. /// The delegate to invoke when is called public AsyncDelegateCommand(Func executeMethod, Func canExecuteMethod) +#if NET6_0_OR_GREATER + : this((p, c) => executeMethod(p).WaitAsync(c), canExecuteMethod) +#else : this((p, c) => executeMethod(p), canExecuteMethod) +#endif { } @@ -80,16 +88,18 @@ public bool IsExecuting /// /// Executes the command. /// - 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) { @@ -134,9 +144,11 @@ public bool CanExecute(T parameter) /// Command Parameter protected override async void Execute(object? parameter) { + var cancellationToken = _getCancellationToken(); + T parameterAsT; try { - await Execute((T)parameter!, _getCancellationToken()); + parameterAsT = (T)parameter!; } catch (Exception ex) { @@ -144,7 +156,14 @@ protected override async void Execute(object? parameter) 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); } /// @@ -179,6 +198,14 @@ public AsyncDelegateCommand EnableParallelExecution() return this; } + /// + /// Sets the based on the specified timeout. + /// + /// A specified timeout. + /// The current instance of . + public AsyncDelegateCommand CancelAfter(TimeSpan timeout) => + CancellationTokenSourceFactory(() => new CancellationTokenSource(timeout).Token); + /// /// Provides a delegate callback to provide a default CancellationToken when the Command is invoked. /// diff --git a/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs b/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs index 99cda221a3..da4a5ad2d7 100644 --- a/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs +++ b/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs @@ -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(str => throw new System.Exception("Test")) + .Catch(ex => handled++); + command.Execute(string.Empty); + Assert.Equal(1, handled); + } }