From 5da69e3241259988ca8bafb4cacf16ce4a272c95 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sat, 8 Jul 2023 22:38:49 -0600 Subject: [PATCH 1/3] feat: adding AsyncDelegateCommand --- .../Commands/AsyncDelegateCommand.cs | 213 ++++++++++++++ .../Commands/AsyncDelegateCommand{T}.cs | 260 ++++++++++++++++++ .../Commands/DelegateCommandBase.cs | 14 +- src/Prism.Core/Commands/IAsyncCommand.cs | 27 ++ .../Commands/AsyncDelegateCommandFixture.cs | 119 ++++++++ 5 files changed, 623 insertions(+), 10 deletions(-) create mode 100644 src/Prism.Core/Commands/AsyncDelegateCommand.cs create mode 100644 src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs create mode 100644 src/Prism.Core/Commands/IAsyncCommand.cs create mode 100644 tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand.cs b/src/Prism.Core/Commands/AsyncDelegateCommand.cs new file mode 100644 index 0000000000..86cc5d525c --- /dev/null +++ b/src/Prism.Core/Commands/AsyncDelegateCommand.cs @@ -0,0 +1,213 @@ +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; + +/// +/// Provides an implementation of the +/// +public class AsyncDelegateCommand : DelegateCommandBase, IAsyncCommand +{ + private bool _enableParallelExecution = false; + private bool _isExecuting = false; + private readonly Func _executeMethod; + private Func _canExecuteMethod; + + /// + /// Creates a new instance of with the to invoke on execution. + /// + /// The to invoke when is called. + public AsyncDelegateCommand(Func executeMethod) + : this(c => executeMethod(), () => true) + { + + } + + /// + /// Creates a new instance of with the to invoke on execution. + /// + /// The to invoke when is called. + public AsyncDelegateCommand(Func executeMethod) + : this(executeMethod, () => true) + { + + } + + /// + /// Creates a new instance of with the to invoke on execution + /// and a to query for determining if the command can execute. + /// + /// The to invoke when is called. + /// The delegate to invoke when is called + public AsyncDelegateCommand(Func executeMethod, Func canExecuteMethod) + : this(c => executeMethod(), canExecuteMethod) + { + } + + /// + /// Creates a new instance of with the to invoke on execution + /// and a to query for determining if the command can execute. + /// + /// The to invoke when is called. + /// The delegate to invoke when is called + public AsyncDelegateCommand(Func executeMethod, Func canExecuteMethod) + : base() + { + if (executeMethod == null || canExecuteMethod == null) + throw new ArgumentNullException(nameof(executeMethod), Resources.DelegateCommandDelegatesCannotBeNull); + + _executeMethod = executeMethod; + _canExecuteMethod = canExecuteMethod; + } + + /// + /// Gets the current state of the AsyncDelegateCommand + /// + public bool IsExecuting + { + get => _isExecuting; + private set => SetProperty(ref _isExecuting, value, OnCanExecuteChanged); + } + + /// + /// Executes the command. + /// + 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; + } + } + + /// + /// Determines if the command can be executed. + /// + /// Returns if the command can execute,otherwise 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; + } + } + + /// + /// Handle the internal invocation of + /// + /// Command Parameter + protected override async void Execute(object parameter) + { + await Execute(); + } + + /// + /// Handle the internal invocation of + /// + /// + /// if the Command Can Execute, otherwise + protected override bool CanExecute(object parameter) + { + return CanExecute(); + } + + /// + /// Enables Parallel Execution of Async Tasks + /// + /// + public AsyncDelegateCommand EnableParallelExecution() + { + _enableParallelExecution = true; + return this; + } + + /// + /// Observes a property that implements INotifyPropertyChanged, and automatically calls DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications. + /// + /// The object type containing the property specified in the expression. + /// The property expression. Example: ObservesProperty(() => PropertyName). + /// The current instance of DelegateCommand + public AsyncDelegateCommand ObservesProperty(Expression> propertyExpression) + { + ObservesPropertyInternal(propertyExpression); + return this; + } + + /// + /// 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. + /// + /// The property expression. Example: ObservesCanExecute(() => PropertyName). + /// The current instance of DelegateCommand + public AsyncDelegateCommand ObservesCanExecute(Expression> canExecuteExpression) + { + _canExecuteMethod = canExecuteExpression.Compile(); + ObservesPropertyInternal(canExecuteExpression); + return this; + } + + /// + /// Provides the ability to connect a delegate to catch exceptions encountered by CanExecute or the Execute methods of the DelegateCommand + /// + /// TThe callback when a specific exception is encountered + /// The current instance of DelegateCommand + public AsyncDelegateCommand Catch(Action @catch) + where TException : Exception + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Provides the ability to connect a delegate to catch exceptions encountered by CanExecute or the Execute methods of the DelegateCommand + /// + /// The generic / default callback when an exception is encountered + /// The current instance of DelegateCommand + public AsyncDelegateCommand Catch(Action @catch) + { + ExceptionHandler.Register(@catch); + return this; + } + + Task IAsyncCommand.ExecuteAsync(object? parameter) + { + return Execute(default); + } + + Task IAsyncCommand.ExecuteAsync(object? parameter, CancellationToken cancellationToken) + { + return Execute(cancellationToken); + } +} diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs new file mode 100644 index 0000000000..b575d12692 --- /dev/null +++ b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs @@ -0,0 +1,260 @@ +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; + +/// +/// Provides an implementation of the with a generic parameter type. +/// +/// +public class AsyncDelegateCommand : DelegateCommandBase, IAsyncCommand +{ + private bool _enableParallelExecution = false; + private bool _isExecuting = false; + private readonly Func _executeMethod; + private Func _canExecuteMethod; + + /// + /// Creates a new instance of with the to invoke on execution. + /// + /// The to invoke when is called. + public AsyncDelegateCommand(Func executeMethod) + : this((p, t) => executeMethod(p), _ => true) + { + + } + + /// + /// Creates a new instance of with the to invoke on execution. + /// + /// The to invoke when is called. + public AsyncDelegateCommand(Func executeMethod) + : this(executeMethod, _ => true) + { + + } + + /// + /// Creates a new instance of with the to invoke on execution + /// and a to query for determining if the command can execute. + /// + /// The to invoke when is called. + /// The delegate to invoke when is called + public AsyncDelegateCommand(Func executeMethod, Func canExecuteMethod) + : this((p, c) => executeMethod(p), canExecuteMethod) + { + + } + + /// + /// Creates a new instance of with the to invoke on execution + /// and a to query for determining if the command can execute. + /// + /// The to invoke when is called. + /// The delegate to invoke when is called + public AsyncDelegateCommand(Func executeMethod, Func canExecuteMethod) + : base() + { + if (executeMethod == null || canExecuteMethod == null) + throw new ArgumentNullException(nameof(executeMethod), Resources.DelegateCommandDelegatesCannotBeNull); + + _executeMethod = executeMethod; + _canExecuteMethod = canExecuteMethod; + } + + /// + /// Gets the current state of the AsyncDelegateCommand + /// + public bool IsExecuting + { + get => _isExecuting; + private set => SetProperty(ref _isExecuting, value, OnCanExecuteChanged); + } + + /// + /// Executes the command. + /// + 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; + } + } + + /// + /// Determines if the command can be executed. + /// + /// Returns if the command can execute,otherwise 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; + } + } + + /// + /// Handle the internal invocation of + /// + /// Command Parameter + protected override async void Execute(object parameter) + { + try + { + await Execute((T)parameter); + } + catch (Exception ex) + { + if (!ExceptionHandler.CanHandle(ex)) + throw; + + ExceptionHandler.Handle(ex, parameter); + } + } + + /// + /// Handle the internal invocation of + /// + /// + /// if the Command Can Execute, otherwise + 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; + } + } + + /// + /// Enables Parallel Execution of Async Tasks + /// + /// + public AsyncDelegateCommand EnableParallelExecution() + { + _enableParallelExecution = true; + return this; + } + + /// + /// Observes a property that implements INotifyPropertyChanged, and automatically calls DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications. + /// + /// The type of the return value of the method that this delegate encapsulates + /// The property expression. Example: ObservesProperty(() => PropertyName). + /// The current instance of DelegateCommand + public AsyncDelegateCommand ObservesProperty(Expression> propertyExpression) + { + ObservesPropertyInternal(propertyExpression); + return this; + } + + /// + /// 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. + /// + /// The property expression. Example: ObservesCanExecute(() => PropertyName). + /// The current instance of DelegateCommand + public AsyncDelegateCommand ObservesCanExecute(Expression> canExecuteExpression) + { + Expression> expression = Expression.Lambda>(canExecuteExpression.Body, Expression.Parameter(typeof(T), "o")); + _canExecuteMethod = expression.Compile(); + ObservesPropertyInternal(canExecuteExpression); + return this; + } + + /// + /// Provides the ability to connect a delegate to catch exceptions encountered by CanExecute or the Execute methods of the DelegateCommand + /// + /// TThe callback when a specific exception is encountered + /// The current instance of DelegateCommand + public AsyncDelegateCommand Catch(Action @catch) + where TException : Exception + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Provides the ability to connect a delegate to catch exceptions encountered by CanExecute or the Execute methods of the DelegateCommand + /// + /// The generic / default callback when an exception is encountered + /// The current instance of DelegateCommand + public AsyncDelegateCommand Catch(Action @catch) + { + ExceptionHandler.Register(@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); + } + } +} diff --git a/src/Prism.Core/Commands/DelegateCommandBase.cs b/src/Prism.Core/Commands/DelegateCommandBase.cs index 13e2a88037..69108eb1b5 100644 --- a/src/Prism.Core/Commands/DelegateCommandBase.cs +++ b/src/Prism.Core/Commands/DelegateCommandBase.cs @@ -5,13 +5,14 @@ using System.Threading; using System.Windows.Input; using Prism.Common; +using Prism.Mvvm; namespace Prism.Commands { /// /// An whose delegates can be attached for and . /// - public abstract class DelegateCommandBase : ICommand, IActiveAware + public abstract class DelegateCommandBase : BindableBase, ICommand, IActiveAware { private bool _isActive; @@ -113,15 +114,8 @@ protected internal void ObservesPropertyInternal(Expression> property /// if the object is active; otherwise . public bool IsActive { - get { return _isActive; } - set - { - if (_isActive != value) - { - _isActive = value; - OnIsActiveChanged(); - } - } + get => _isActive; + set => SetProperty(ref _isActive, value, OnIsActiveChanged); } /// diff --git a/src/Prism.Core/Commands/IAsyncCommand.cs b/src/Prism.Core/Commands/IAsyncCommand.cs new file mode 100644 index 0000000000..e2a0203213 --- /dev/null +++ b/src/Prism.Core/Commands/IAsyncCommand.cs @@ -0,0 +1,27 @@ +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; + +#nullable enable +namespace Prism.Commands; + +/// +/// Provides an abstraction layer for custom controls which want to make use of Async Commands +/// +public interface IAsyncCommand : ICommand +{ + /// + /// Executes the Command with a specified parameter and the Default . + /// + /// The Command Parameter + /// An Asynchronous Task + Task ExecuteAsync(object? parameter); + + /// + /// Executes the Command with a specified parameter and using a + /// + /// The Command Parameter + /// The . + /// An Asynchronous Task + Task ExecuteAsync(object? parameter, CancellationToken cancellationToken); +} diff --git a/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs b/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs new file mode 100644 index 0000000000..f682af969d --- /dev/null +++ b/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs @@ -0,0 +1,119 @@ +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(); + 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(); + 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 (); + 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(); + 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); + } +} From a601158c7b5baca854d3bcbab3dedcfc11d5fd4d Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sat, 8 Jul 2023 22:57:02 -0600 Subject: [PATCH 2/3] chore: add constraint to suppress TaskCanceledException --- 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 86cc5d525c..ff5ff22200 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand.cs @@ -84,7 +84,7 @@ public async Task Execute(CancellationToken cancellationToken = default) IsExecuting = true; await _executeMethod(cancellationToken); } - catch (TaskCanceledException) + catch (TaskCanceledException) when (cancellationToken.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 b575d12692..2712aab701 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs @@ -86,7 +86,7 @@ public async Task Execute(T parameter, CancellationToken cancellationToken = def IsExecuting = true; await _executeMethod(parameter, cancellationToken); } - catch (TaskCanceledException) + catch (TaskCanceledException) when (cancellationToken.IsCancellationRequested) { // Do nothing... the Task was cancelled } From 40a268f70820aa5d19dae8a2757dc94d78e5f017 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sat, 8 Jul 2023 23:15:32 -0600 Subject: [PATCH 3/3] chore: add support for providing a default CancellationToken factory --- .../Commands/AsyncDelegateCommand.cs | 26 ++++++++++++++----- .../Commands/AsyncDelegateCommand{T}.cs | 26 ++++++++++++++----- .../Commands/AsyncDelegateCommandFixture.cs | 20 ++++++++++++-- 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand.cs b/src/Prism.Core/Commands/AsyncDelegateCommand.cs index ff5ff22200..e29562eef9 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand.cs @@ -17,6 +17,7 @@ public class AsyncDelegateCommand : DelegateCommandBase, IAsyncCommand private bool _isExecuting = false; private readonly Func _executeMethod; private Func _canExecuteMethod; + private Func _getCancellationToken = () => CancellationToken.None; /// /// Creates a new instance of with the to invoke on execution. @@ -131,7 +132,7 @@ public bool CanExecute() /// Command Parameter protected override async void Execute(object parameter) { - await Execute(); + await Execute(_getCancellationToken()); } /// @@ -147,19 +148,30 @@ protected override bool CanExecute(object parameter) /// /// Enables Parallel Execution of Async Tasks /// - /// + /// The current instance of . public AsyncDelegateCommand EnableParallelExecution() { _enableParallelExecution = true; return this; } + /// + /// Provides a delegate callback to provide a default CancellationToken when the Command is invoked. + /// + /// The default Factory. + /// The current instance of . + public AsyncDelegateCommand CancellationTokenSourceFactory(Func factory) + { + _getCancellationToken = factory; + return this; + } + /// /// Observes a property that implements INotifyPropertyChanged, and automatically calls DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications. /// /// The object type containing the property specified in the expression. /// The property expression. Example: ObservesProperty(() => PropertyName). - /// The current instance of DelegateCommand + /// The current instance of . public AsyncDelegateCommand ObservesProperty(Expression> propertyExpression) { ObservesPropertyInternal(propertyExpression); @@ -170,7 +182,7 @@ public AsyncDelegateCommand ObservesProperty(Expression> propertyExpr /// 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. /// /// The property expression. Example: ObservesCanExecute(() => PropertyName). - /// The current instance of DelegateCommand + /// The current instance of . public AsyncDelegateCommand ObservesCanExecute(Expression> canExecuteExpression) { _canExecuteMethod = canExecuteExpression.Compile(); @@ -182,7 +194,7 @@ public AsyncDelegateCommand ObservesCanExecute(Expression> canExecute /// Provides the ability to connect a delegate to catch exceptions encountered by CanExecute or the Execute methods of the DelegateCommand /// /// TThe callback when a specific exception is encountered - /// The current instance of DelegateCommand + /// The current instance of . public AsyncDelegateCommand Catch(Action @catch) where TException : Exception { @@ -194,7 +206,7 @@ public AsyncDelegateCommand Catch(Action @catch) /// Provides the ability to connect a delegate to catch exceptions encountered by CanExecute or the Execute methods of the DelegateCommand /// /// The generic / default callback when an exception is encountered - /// The current instance of DelegateCommand + /// The current instance of . public AsyncDelegateCommand Catch(Action @catch) { ExceptionHandler.Register(@catch); @@ -203,7 +215,7 @@ public AsyncDelegateCommand Catch(Action @catch) Task IAsyncCommand.ExecuteAsync(object? parameter) { - return Execute(default); + return Execute(_getCancellationToken()); } Task IAsyncCommand.ExecuteAsync(object? parameter, CancellationToken cancellationToken) diff --git a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs index 2712aab701..6565d20003 100644 --- a/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs +++ b/src/Prism.Core/Commands/AsyncDelegateCommand{T}.cs @@ -18,6 +18,7 @@ public class AsyncDelegateCommand : DelegateCommandBase, IAsyncCommand private bool _isExecuting = false; private readonly Func _executeMethod; private Func _canExecuteMethod; + private Func _getCancellationToken = () => CancellationToken.None; /// /// Creates a new instance of with the to invoke on execution. @@ -135,7 +136,7 @@ protected override async void Execute(object parameter) { try { - await Execute((T)parameter); + await Execute((T)parameter, _getCancellationToken()); } catch (Exception ex) { @@ -171,19 +172,30 @@ protected override bool CanExecute(object parameter) /// /// Enables Parallel Execution of Async Tasks /// - /// + /// The current instance of . public AsyncDelegateCommand EnableParallelExecution() { _enableParallelExecution = true; return this; } + /// + /// Provides a delegate callback to provide a default CancellationToken when the Command is invoked. + /// + /// The default Factory. + /// The current instance of . + public AsyncDelegateCommand CancellationTokenSourceFactory(Func factory) + { + _getCancellationToken = factory; + return this; + } + /// /// Observes a property that implements INotifyPropertyChanged, and automatically calls DelegateCommandBase.RaiseCanExecuteChanged on property changed notifications. /// /// The type of the return value of the method that this delegate encapsulates /// The property expression. Example: ObservesProperty(() => PropertyName). - /// The current instance of DelegateCommand + /// The current instance of . public AsyncDelegateCommand ObservesProperty(Expression> propertyExpression) { ObservesPropertyInternal(propertyExpression); @@ -194,7 +206,7 @@ public AsyncDelegateCommand ObservesProperty(Expression> p /// 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. /// /// The property expression. Example: ObservesCanExecute(() => PropertyName). - /// The current instance of DelegateCommand + /// The current instance of . public AsyncDelegateCommand ObservesCanExecute(Expression> canExecuteExpression) { Expression> expression = Expression.Lambda>(canExecuteExpression.Body, Expression.Parameter(typeof(T), "o")); @@ -207,7 +219,7 @@ public AsyncDelegateCommand ObservesCanExecute(Expression> canExec /// Provides the ability to connect a delegate to catch exceptions encountered by CanExecute or the Execute methods of the DelegateCommand /// /// TThe callback when a specific exception is encountered - /// The current instance of DelegateCommand + /// The current instance of . public AsyncDelegateCommand Catch(Action @catch) where TException : Exception { @@ -219,7 +231,7 @@ public AsyncDelegateCommand Catch(Action @catch) /// Provides the ability to connect a delegate to catch exceptions encountered by CanExecute or the Execute methods of the DelegateCommand /// /// The generic / default callback when an exception is encountered - /// The current instance of DelegateCommand + /// The current instance of . public AsyncDelegateCommand Catch(Action @catch) { ExceptionHandler.Register(@catch); @@ -231,7 +243,7 @@ async Task IAsyncCommand.ExecuteAsync(object? parameter) try { // If T is not nullable this may throw an exception - await Execute((T)parameter, default); + await Execute((T)parameter, _getCancellationToken()); } catch (Exception ex) { diff --git a/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs b/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs index f682af969d..99cda221a3 100644 --- a/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs +++ b/tests/Prism.Core.Tests/Commands/AsyncDelegateCommandFixture.cs @@ -1,5 +1,6 @@ -using System.Threading.Tasks; -using System.Threading; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; using Prism.Commands; using Xunit; @@ -116,4 +117,19 @@ async Task Execute(CancellationToken token) Assert.True(executionStarted); Assert.False(executed); } + + [Fact] + public async Task ICommandExecute_UsesDefaultTokenSourceFactory() + { + var cts = new CancellationTokenSource(); + var command = new AsyncDelegateCommand((token) => Task.Delay(1000, token)) + .CancellationTokenSourceFactory(() => cts.Token); + ICommand iCommand = command; + iCommand.Execute(null); + + Assert.True(command.IsExecuting); + cts.Cancel(); + + Assert.False(command.IsExecuting); + } }