From 7dbaa85313707b5bc323d3711e1b8ead958be214 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sat, 28 Oct 2023 19:50:51 -0600 Subject: [PATCH 1/9] chore: enable CancellationToken on delegates without CancellationToken --- src/Prism.Core/Commands/AsyncDelegateCommand.cs | 10 +++++++++- src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand.cs b/src/Prism.Core/Commands/AsyncDelegateCommand.cs index ad4082d160..f424f7b4e7 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand.cs @@ -1,4 +1,4 @@ -using System.Linq.Expressions; +using System.Linq.Expressions; using System; using System.Threading; using System.Threading.Tasks; @@ -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 { } diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs index ed257ce196..71a74a1c2d 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs @@ -1,4 +1,4 @@ -using System.Linq.Expressions; +using System.Linq.Expressions; using System; using System.Threading; using System.Threading.Tasks; @@ -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 { } From f95c05e02db21955ac2fcdf52edfa3e0792beafd Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sat, 28 Oct 2023 19:51:47 -0600 Subject: [PATCH 2/9] feat: adding CancelAfter configuration API --- src/Prism.Core/Commands/AsyncDelegateCommand.cs | 8 ++++++++ src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand.cs b/src/Prism.Core/Commands/AsyncDelegateCommand.cs index f424f7b4e7..0126fc9310 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand.cs @@ -163,6 +163,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 71a74a1c2d..2a5e163f27 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs @@ -187,6 +187,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. /// From 2bf0f69aab768f0a1bef16aced02ae873077f54e Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sat, 28 Oct 2023 19:57:31 -0600 Subject: [PATCH 3/9] feat: suppress TaskCanceledException if token was canceled. --- .../Commands/AsyncDelegateCommand.cs | 18 +++++++++++++++++- .../Commands/AsyncDelegateCommand{T}.cs | 8 +++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand.cs b/src/Prism.Core/Commands/AsyncDelegateCommand.cs index 0126fc9310..36c6d68da9 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand.cs @@ -140,7 +140,23 @@ public bool CanExecute() /// Command Parameter protected override async void Execute(object? parameter) { - await Execute(_getCancellationToken()); + var cancellationToken = _getCancellationToken(); + try + { + await Execute(cancellationToken) + .ConfigureAwait(false); + } + catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Do nothing... the Task was cancelled + } + catch (Exception ex) + { + if (!ExceptionHandler.CanHandle(ex)) + throw; + + ExceptionHandler.Handle(ex, parameter); + } } /// diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs index 2a5e163f27..4bcc54a559 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs @@ -142,9 +142,15 @@ public bool CanExecute(T parameter) /// Command Parameter protected override async void Execute(object? parameter) { + var cancellationToken = _getCancellationToken(); try { - await Execute((T)parameter!, _getCancellationToken()); + await Execute((T)parameter!, cancellationToken) + .ConfigureAwait(false); + } + catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) + { + // Do nothing... the Task was cancelled } catch (Exception ex) { From 0bac0e56bc2a80723614ebd7f8665b51ab1b1dfe Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sat, 28 Oct 2023 19:58:14 -0600 Subject: [PATCH 4/9] feat: ensure we prevent parallel execution if disabled --- src/Prism.Core/Commands/AsyncDelegateCommand.cs | 3 +++ src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand.cs b/src/Prism.Core/Commands/AsyncDelegateCommand.cs index 36c6d68da9..c85e400659 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand.cs @@ -90,6 +90,9 @@ public async Task Execute(CancellationToken cancellationToken = default) { try { + if (!_enableParallelExecution && IsExecuting) + return; + IsExecuting = true; await _executeMethod(cancellationToken); } diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs index 4bcc54a559..2c6f573e2f 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs @@ -92,6 +92,9 @@ public async Task Execute(T parameter, CancellationToken cancellationToken = def { try { + if (!_enableParallelExecution && IsExecuting) + return; + IsExecuting = true; await _executeMethod(parameter, cancellationToken); } From d351e88af07a8448089c8624583a9e00358147b3 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sat, 28 Oct 2023 19:58:53 -0600 Subject: [PATCH 5/9] feat: set ConfigureAwait(false) on the delegate task --- src/Prism.Core/Commands/AsyncDelegateCommand.cs | 10 ++++++---- src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs | 11 +++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand.cs b/src/Prism.Core/Commands/AsyncDelegateCommand.cs index c85e400659..7781457969 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand.cs @@ -1,4 +1,4 @@ -using System.Linq.Expressions; +using System.Linq.Expressions; using System; using System.Threading; using System.Threading.Tasks; @@ -86,17 +86,19 @@ public bool IsExecuting /// /// Executes the command. /// - public async Task Execute(CancellationToken cancellationToken = default) + public async Task Execute(CancellationToken? cancellationToken) { + var token = cancellationToken ?? _getCancellationToken(); try { if (!_enableParallelExecution && IsExecuting) return; IsExecuting = true; - await _executeMethod(cancellationToken); + await _executeMethod(token) + .ConfigureAwait(false); } - catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) + catch (TaskCanceledException) when (token.IsCancellationRequested) { // Do nothing... the Task was cancelled } diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs index 2c6f573e2f..4f367471f8 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs @@ -1,4 +1,4 @@ -using System.Linq.Expressions; +using System.Linq.Expressions; using System; using System.Threading; using System.Threading.Tasks; @@ -88,17 +88,20 @@ public bool IsExecuting /// /// Executes the command. /// - public async Task Execute(T parameter, CancellationToken cancellationToken = default) + public async Task Execute(T parameter, CancellationToken? cancellationToken) { + var token = cancellationToken ?? _getCancellationToken(); + try { if (!_enableParallelExecution && IsExecuting) return; IsExecuting = true; - await _executeMethod(parameter, cancellationToken); + await _executeMethod(parameter, token) + .ConfigureAwait(false); } - catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) + catch (TaskCanceledException) when (token.IsCancellationRequested) { // Do nothing... the Task was cancelled } From ebe67f57007ce30a37d6b4bab8757fe1f306ae91 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sat, 28 Oct 2023 20:06:30 -0600 Subject: [PATCH 6/9] test: adding test to ensure Exception is handled once --- .../Commands/AsyncDelegateCommandFixture.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs b/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs index 99cda221a3..ff1ef506f8 100644 --- a/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs +++ b/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs @@ -132,4 +132,14 @@ public async Task ICommandExecute_UsesDefaultTokenSourceFactory() 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); + } } From 59d23841558bc82e5d22993966e3c91803a9dfc9 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sat, 28 Oct 2023 20:15:49 -0600 Subject: [PATCH 7/9] chore: fixing cancellationToken should have default null value --- src/Prism.Core/Commands/AsyncDelegateCommand.cs | 2 +- src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand.cs b/src/Prism.Core/Commands/AsyncDelegateCommand.cs index 7781457969..e748ad40d8 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand.cs @@ -86,7 +86,7 @@ public bool IsExecuting /// /// Executes the command. /// - public async Task Execute(CancellationToken? cancellationToken) + public async Task Execute(CancellationToken? cancellationToken = null) { var token = cancellationToken ?? _getCancellationToken(); try diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs index 4f367471f8..4ed68524da 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs @@ -88,7 +88,7 @@ public bool IsExecuting /// /// Executes the command. /// - public async Task Execute(T parameter, CancellationToken? cancellationToken) + public async Task Execute(T parameter, CancellationToken? cancellationToken = null) { var token = cancellationToken ?? _getCancellationToken(); From 8a60502418eede21a19feb549580f7577b92b386 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Fri, 3 Nov 2023 16:44:59 -0600 Subject: [PATCH 8/9] test: add delay for CancellationToken to take effect --- tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs b/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs index ff1ef506f8..da4a5ad2d7 100644 --- a/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs +++ b/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs @@ -129,6 +129,7 @@ public async Task ICommandExecute_UsesDefaultTokenSourceFactory() Assert.True(command.IsExecuting); cts.Cancel(); + await Task.Delay(10); Assert.False(command.IsExecuting); } From 56e4d88db0a1de0945215d2e930d848ad225baf3 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Mon, 13 Nov 2023 18:22:56 -0600 Subject: [PATCH 9/9] chore: remove TaskCanceledException catch --- .../Commands/AsyncDelegateCommand.cs | 24 ++++--------------- .../Commands/AsyncDelegateCommand{T}.cs | 19 +++++++-------- 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand.cs b/src/Prism.Core/Commands/AsyncDelegateCommand.cs index e748ad40d8..8c7eec460f 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand.cs @@ -98,10 +98,6 @@ public async Task Execute(CancellationToken? cancellationToken = null) await _executeMethod(token) .ConfigureAwait(false); } - catch (TaskCanceledException) when (token.IsCancellationRequested) - { - // Do nothing... the Task was cancelled - } catch (Exception ex) { if (!ExceptionHandler.CanHandle(ex)) @@ -145,23 +141,11 @@ public bool CanExecute() /// Command Parameter protected override async void Execute(object? parameter) { + // 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(); - try - { - await Execute(cancellationToken) - .ConfigureAwait(false); - } - catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) - { - // Do nothing... the Task was cancelled - } - catch (Exception ex) - { - if (!ExceptionHandler.CanHandle(ex)) - throw; - - ExceptionHandler.Handle(ex, parameter); - } + await Execute(cancellationToken) + .ConfigureAwait(false); } /// diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs index 4ed68524da..a449f5c67b 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs @@ -101,10 +101,6 @@ public async Task Execute(T parameter, CancellationToken? cancellationToken = nu await _executeMethod(parameter, token) .ConfigureAwait(false); } - catch (TaskCanceledException) when (token.IsCancellationRequested) - { - // Do nothing... the Task was cancelled - } catch (Exception ex) { if (!ExceptionHandler.CanHandle(ex)) @@ -149,14 +145,10 @@ public bool CanExecute(T parameter) protected override async void Execute(object? parameter) { var cancellationToken = _getCancellationToken(); + T parameterAsT; try { - await Execute((T)parameter!, cancellationToken) - .ConfigureAwait(false); - } - catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) - { - // Do nothing... the Task was cancelled + parameterAsT = (T)parameter!; } catch (Exception ex) { @@ -164,7 +156,14 @@ await Execute((T)parameter!, cancellationToken) 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); } ///