diff --git a/src/Prism.Core/Commands/DelegateCommand.cs b/src/Prism.Core/Commands/DelegateCommand.cs index 7f5fb4650f..52ad7c5331 100644 --- a/src/Prism.Core/Commands/DelegateCommand.cs +++ b/src/Prism.Core/Commands/DelegateCommand.cs @@ -1,5 +1,6 @@ using System; using System.Linq.Expressions; +using System.Threading.Tasks; using System.Windows.Input; using Prism.Properties; @@ -46,7 +47,17 @@ public DelegateCommand(Action executeMethod, Func canExecuteMethod) /// public void Execute() { - _executeMethod(); + try + { + _executeMethod(); + } + catch (Exception ex) + { + if (!ExceptionHandler.CanHandle(ex)) + throw; + + ExceptionHandler.Handle(ex, null); + } } /// @@ -55,7 +66,19 @@ public void Execute() /// Returns if the command can execute,otherwise returns . public bool CanExecute() { - return _canExecuteMethod(); + try + { + return _canExecuteMethod(); + } + catch (Exception ex) + { + if (!ExceptionHandler.CanHandle(ex)) + throw; + + ExceptionHandler.Handle(ex, null); + + return false; + } } /// @@ -100,5 +123,101 @@ public DelegateCommand ObservesCanExecute(Expression> canExecuteExpre ObservesPropertyInternal(canExecuteExpression); return this; } + + /// + /// Registers an callback if an exception is encountered while executing the + /// + /// The Callback + /// The current instance of + public DelegateCommand Catch(Action @catch) + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Registers an callback if an exception is encountered while executing the + /// + /// The Callback + /// The current instance of + public DelegateCommand Catch(Action @catch) + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Registers an callback if an exception is encountered while executing the + /// + /// The Exception Type + /// The Callback + /// The current instance of + public DelegateCommand Catch(Action @catch) + where TException : Exception + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Registers an callback if an exception is encountered while executing the + /// + /// The Exception Type + /// The Callback + /// The current instance of + public DelegateCommand Catch(Action @catch) + where TException : Exception + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Registers an async callback if an exception is encountered while executing the + /// + /// The Callback + /// The current instance of + public DelegateCommand Catch(Func @catch) + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Registers an async callback if an exception is encountered while executing the + /// + /// The Callback + /// The current instance of + public DelegateCommand Catch(Func @catch) + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Registers an async callback if an exception is encountered while executing the + /// + /// The Exception Type + /// The Callback + /// The current instance of + public DelegateCommand Catch(Func @catch) + where TException : Exception + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Registers an async callback if an exception is encountered while executing the + /// + /// The Exception Type + /// The Callback + /// The current instance of + public DelegateCommand Catch(Func @catch) + where TException : Exception + { + ExceptionHandler.Register(@catch); + return this; + } } } diff --git a/src/Prism.Core/Commands/DelegateCommandBase.cs b/src/Prism.Core/Commands/DelegateCommandBase.cs index d4e7831835..13e2a88037 100644 --- a/src/Prism.Core/Commands/DelegateCommandBase.cs +++ b/src/Prism.Core/Commands/DelegateCommandBase.cs @@ -4,6 +4,7 @@ using System.Linq.Expressions; using System.Threading; using System.Windows.Input; +using Prism.Common; namespace Prism.Commands { @@ -17,6 +18,11 @@ public abstract class DelegateCommandBase : ICommand, IActiveAware private SynchronizationContext _synchronizationContext; private readonly HashSet _observedPropertiesExpressions = new HashSet(); + /// + /// Provides an Exception Handler to register callbacks or handle encountered exceptions within + /// + protected readonly MulticastExceptionHandler ExceptionHandler = new MulticastExceptionHandler(); + /// /// Creates a new instance of a , specifying both the execute action and the can execute function. /// @@ -32,7 +38,7 @@ protected DelegateCommandBase() /// /// Raises so every - /// command invoker can requery . + /// command invoker can re-query . /// protected virtual void OnCanExecuteChanged() { @@ -48,7 +54,7 @@ protected virtual void OnCanExecuteChanged() /// /// Raises so every command invoker - /// can requery to check if the command can execute. + /// can re-query to check if the command can execute. /// /// Note that this will trigger the execution of once for each invoker. [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate")] diff --git a/src/Prism.Core/Commands/DelegateCommand{T}.cs b/src/Prism.Core/Commands/DelegateCommand{T}.cs index 9f1244f8cb..8d4bdf7ae8 100644 --- a/src/Prism.Core/Commands/DelegateCommand{T}.cs +++ b/src/Prism.Core/Commands/DelegateCommand{T}.cs @@ -1,6 +1,7 @@ using System; using System.Linq.Expressions; using System.Reflection; +using System.Threading.Tasks; using System.Windows.Input; using Prism.Properties; @@ -79,7 +80,17 @@ public DelegateCommand(Action executeMethod, Func canExecuteMethod) ///Data used by the command. public void Execute(T parameter) { - _executeMethod(parameter); + try + { + _executeMethod(parameter); + } + catch (Exception ex) + { + if (!ExceptionHandler.CanHandle(ex)) + throw; + + ExceptionHandler.Handle(ex, parameter); + } } /// @@ -91,7 +102,19 @@ public void Execute(T parameter) /// public bool CanExecute(T parameter) { - return _canExecuteMethod(parameter); + try + { + return _canExecuteMethod(parameter); + } + catch (Exception ex) + { + if (!ExceptionHandler.CanHandle(ex)) + throw; + + ExceptionHandler.Handle(ex, parameter); + + return false; + } } /// @@ -100,7 +123,19 @@ public bool CanExecute(T parameter) /// Command Parameter protected override void Execute(object parameter) { - Execute((T)parameter); + try + { + // Note: We don't call Execute because we would potentially invoke the Try/Catch twice. + // It is also needed here incase (T)parameter throws the exception + _executeMethod((T)parameter); + } + catch (Exception ex) + { + if (!ExceptionHandler.CanHandle(ex)) + throw; + + ExceptionHandler.Handle(ex, parameter); + } } /// @@ -110,7 +145,21 @@ protected override void Execute(object parameter) /// if the Command Can Execute, otherwise protected override bool CanExecute(object parameter) { - return CanExecute((T)parameter); + try + { + // Note: We don't call Execute because we would potentially invoke the Try/Catch twice. + // It is also needed here incase (T)parameter throws the exception + return CanExecute((T)parameter); + } + catch (Exception ex) + { + if (!ExceptionHandler.CanHandle(ex)) + throw; + + ExceptionHandler.Handle(ex, parameter); + + return false; + } } /// @@ -137,5 +186,101 @@ public DelegateCommand ObservesCanExecute(Expression> canExecuteEx ObservesPropertyInternal(canExecuteExpression); return this; } + + /// + /// Registers an callback if an exception is encountered while executing the + /// + /// The Callback + /// The current instance of + public DelegateCommand Catch(Action @catch) + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Registers an callback if an exception is encountered while executing the + /// + /// The Callback + /// The current instance of + public DelegateCommand Catch(Action @catch) + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Registers an callback if an exception is encountered while executing the + /// + /// The Exception Type + /// The Callback + /// The current instance of + public DelegateCommand Catch(Action @catch) + where TException : Exception + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Registers an callback if an exception is encountered while executing the + /// + /// The Exception Type + /// The Callback + /// The current instance of + public DelegateCommand Catch(Action @catch) + where TException : Exception + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Registers an async callback if an exception is encountered while executing the + /// + /// The Callback + /// The current instance of + public DelegateCommand Catch(Func @catch) + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Registers an async callback if an exception is encountered while executing the + /// + /// The Callback + /// The current instance of + public DelegateCommand Catch(Func @catch) + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Registers an async callback if an exception is encountered while executing the + /// + /// The Exception Type + /// The Callback + /// The current instance of + public DelegateCommand Catch(Func @catch) + where TException : Exception + { + ExceptionHandler.Register(@catch); + return this; + } + + /// + /// Registers an async callback if an exception is encountered while executing the + /// + /// The Exception Type + /// The Callback + /// The current instance of + public DelegateCommand Catch(Func @catch) + where TException : Exception + { + ExceptionHandler.Register(@catch); + return this; + } } } diff --git a/src/Prism.Core/Common/MulticastExceptionHandler.cs b/src/Prism.Core/Common/MulticastExceptionHandler.cs new file mode 100644 index 0000000000..a54e07982f --- /dev/null +++ b/src/Prism.Core/Common/MulticastExceptionHandler.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Prism.Common; + +#nullable enable +/// +/// Provides a wrapper for managing multicast delegates for handling specific errors +/// +public struct MulticastExceptionHandler +{ + private readonly Dictionary _handlers; + + /// + /// Initializes a new MulticastExceptionHandler + /// + public MulticastExceptionHandler() + { + _handlers = new Dictionary(); + } + + public void Register(MulticastDelegate callback) + where TException : Exception + { + _handlers.Add(typeof(TException), callback); + } + + /// + /// Determines if there is a callback registered to handle the specified exception + /// + /// An to handle or rethrow + /// True if a Callback has been registered for the given type of . + public bool CanHandle(Exception exception) => + GetDelegate(exception.GetType()) is not null; + + /// + /// Handles a specified + /// + /// + /// + public async void Handle(Exception exception, object? parameter = null) => + await HandleAsync(exception, parameter); + + public async Task HandleAsync(Exception exception, object? parameter = null) + { + var multicastDelegate = GetDelegate(exception.GetType()); + + if (multicastDelegate is null) + return; + + // Get Invoke() method of the delegate + var invokeMethod = multicastDelegate.GetType().GetMethod("Invoke"); + + if (invokeMethod == null) + throw new InvalidOperationException($"Could not find Invoke() method for delegate of type {multicastDelegate.GetType().Name}"); + + var parameters = invokeMethod.GetParameters(); + var arguments = parameters.Length switch + { + 0 => Array.Empty(), + 1 => typeof(Exception).IsAssignableFrom(parameters[0].ParameterType) ? new object?[] { exception } : new object?[] { parameter }, + 2 => typeof(Exception).IsAssignableFrom(parameters[0].ParameterType) ? new object?[] { exception, parameter } : new object?[] { parameter, exception }, + _ => throw new InvalidOperationException($"Handler of type {multicastDelegate.GetType().Name} is not supported", exception) + }; + + // Invoke the delegate + var result = invokeMethod.Invoke(multicastDelegate, arguments); + + // If the handler is async (returns a Task), then we await the task + if (result is Task task) + { + await task; + } +#if NET6_0_OR_GREATER + else if (result is ValueTask valueTask) + { + await valueTask; + } +#endif + } + + private MulticastDelegate? GetDelegate(Type type) + { + if (_handlers.ContainsKey(type)) + return _handlers[type]; + else if (type.BaseType is not null) + return GetDelegate(type.BaseType); + + return null; + } +} diff --git a/tests/Prism.Core.Tests/Commands/DelegateCommandFixture.cs b/tests/Prism.Core.Tests/Commands/DelegateCommandFixture.cs index 9d7d472a38..b8c4de7573 100644 --- a/tests/Prism.Core.Tests/Commands/DelegateCommandFixture.cs +++ b/tests/Prism.Core.Tests/Commands/DelegateCommandFixture.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Windows.Input; using Prism.Commands; using Prism.Mvvm; @@ -819,6 +820,148 @@ public void GenericDelegateCommandPropertyObserverUnsubscribeUnusedListeners() Assert.Equal(0, complexProp.GetPropertyChangedSubscribledLenght()); } + [Fact] + public void DelegateCommand_Catch_CatchesException() + { + var caught = false; + var command = new DelegateCommand(() => { throw new Exception(); }) + .Catch(ex => caught = true); + command.Execute(); + + Assert.True(caught); + } + + [Fact] + public void DelegateCommandT_Catch_CatchesException() + { + var caught = false; + var command = new DelegateCommand(x => { throw new Exception(); }) + .Catch(ex => caught = true); + command.Execute(new MyClass()); + + Assert.True(caught); + } + + [Fact] + public void DelegateCommand_Catch_CatchesExceptionWithParameter() + { + var caught = false; + object parameter = new object(); + var command = new DelegateCommand(() => { throw new Exception(); }) + .Catch((ex, value) => + { + caught = true; + parameter = value; + }); + command.Execute(); + + Assert.True(caught); + Assert.Null(parameter); + } + + [Fact] + public void DelegateCommandT_Catch_InvalidCastException() + { + var caught = false; + ICommand command = new DelegateCommand(x => { }) + .Catch(ex => + { + Assert.IsType(ex); + caught = true; + }); + + command.Execute("Test"); + Assert.True(caught); + } + + [Fact] + public void DelegateCommandT_Catch_CatchesExceptionWithParameter() + { + var caught = false; + var parameter = new object(); + var command = new DelegateCommand(x => { throw new Exception(); }) + .Catch((ex, value) => + { + caught = true; + parameter = value; + }); + command.Execute(new MyClass { Value = 3 }); + + Assert.True(caught); + Assert.IsType(parameter); + Assert.Equal(3, ((MyClass)parameter).Value); + } + + [Fact] + public void DelegateCommand_Catch_CatchesSpecificException() + { + var caught = false; + var command = new DelegateCommand(() => { throw new FileNotFoundException(); }) + .Catch(ex => caught = true); + command.Execute(); + + Assert.True(caught); + } + + [Fact] + public void DelegateCommandT_Catch_CatchesSpecificException() + { + var caught = false; + var command = new DelegateCommand(x => { throw new FileNotFoundException(); }) + .Catch(ex => caught = true); + command.Execute(new MyClass()); + + Assert.True(caught); + } + + [Fact] + public void DelegateCommand_Catch_CatchesChildException() + { + var caught = false; + var command = new DelegateCommand(() => { throw new FileNotFoundException(); }) + .Catch(ex => caught = true); + command.Execute(); + + Assert.True(caught); + } + + [Fact] + public void DelegateCommandT_Catch_CatchesChildException() + { + var caught = false; + var command = new DelegateCommand(x => { throw new FileNotFoundException(); }) + .Catch(ex => caught = true); + command.Execute(new MyClass()); + + Assert.True(caught); + } + + [Fact] + public void DelegateCommand_Throws_CatchesException() + { + var caught = false; + var command = new DelegateCommand(() => { throw new Exception(); }) + .Catch(ex => caught = true); + var ex = Record.Exception(() => command.Execute()); + + Assert.False(caught); + Assert.NotNull(ex); + Assert.IsType(ex); + } + + [Fact] + public void DelegateCommandT_Throws_CatchesException() + { + var caught = false; + var command = new DelegateCommand(x => { throw new Exception(); }) + .Catch(ex => caught = true); + var ex = Record.Exception(() => command.Execute(new MyClass())); + + Assert.False(caught); + Assert.NotNull(ex); + Assert.IsType(ex); + } + public class ComplexType : TestPurposeBindableBase { private int _intProperty; @@ -891,5 +1034,6 @@ public void Execute(object parameter) internal class MyClass { + public int Value { get; set; } } } diff --git a/tests/Prism.Core.Tests/Common/MulticastExceptionHandlerFixture.cs b/tests/Prism.Core.Tests/Common/MulticastExceptionHandlerFixture.cs new file mode 100644 index 0000000000..d5b7026fcf --- /dev/null +++ b/tests/Prism.Core.Tests/Common/MulticastExceptionHandlerFixture.cs @@ -0,0 +1,182 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Prism.Common; +using Xunit; + +namespace Prism.Core.Tests.Common; + +#nullable enable +public class MulticastExceptionHandlerFixture +{ + [Fact] + public void CanHandleGenericException() + { + var handler = new MulticastExceptionHandler(); + void Callback(Exception exception) { } + handler.Register(Callback); + Assert.True(handler.CanHandle(new Exception())); + } + + [Fact] + public void DidHandleGenericException() + { + var handler = new MulticastExceptionHandler(); + handler.Register(Callback); + bool handled = false; + void Callback(Exception exception) + { + handled = true; + } + handler.Handle(new Exception()); + + Assert.True(handled); + } + + [Fact] + public void CanHandleGenericExceptionAsync() + { + var handler = new MulticastExceptionHandler(); + handler.Register(Callback); + Task Callback(Exception exception) + { + return Task.CompletedTask; + } + Assert.True(handler.CanHandle(new Exception())); + } + + [Fact] + public void CanHandleUsingBaseExceptionType() + { + var handler = new MulticastExceptionHandler(); + handler.Register(Callback); + Task Callback(IOException exception) + { + return Task.CompletedTask; + } + Assert.True(handler.CanHandle(new FileNotFoundException())); + } + + [Fact] + public void DidHandleGenericExceptionAsync() + { + var handler = new MulticastExceptionHandler(); + handler.Register(Callback); + bool handled = false; + Task Callback(Exception exception) + { + handled = true; + return Task.CompletedTask; + } + handler.Handle(new Exception()); + + Assert.True(handled); + } + [Fact] + public void CanHandleSpecificException() + { + var handler = new MulticastExceptionHandler(); + handler.Register(Callback); + void Callback(FileNotFoundException exception) { } + Assert.True(handler.CanHandle(new FileNotFoundException())); + } + + [Fact] + public async Task DidHandleSpecificException() + { + var handler = new MulticastExceptionHandler(); + handler.Register(Callback); + bool handled = false; + void Callback(FileNotFoundException exception) + { + handled = true; + } + await handler.HandleAsync(new FileNotFoundException()); + + Assert.True(handled); + } + + [Fact] + public async Task DidHandleSpecificExceptionWithParameter() + { + var handler = new MulticastExceptionHandler(); + var expected = Guid.NewGuid(); + bool handled = false; + handler.Register(Callback); + + void Callback(FileNotFoundException fnfe, object? parameter) + { + Assert.Equal(expected, parameter); + handled = true; + } + await handler.HandleAsync(new FileNotFoundException(), expected); + + Assert.True(handled); + } + + [Fact] + public void CanHandleSpecificExceptionAsync() + { + var handler = new MulticastExceptionHandler(); + handler.Register(Callback); + Task Callback(FileNotFoundException exception) + { + return Task.CompletedTask; + } + Assert.True(handler.CanHandle(new FileNotFoundException())); + } + + [Fact] + public async Task DidHandleSpecificExceptionAsync() + { + var handler = new MulticastExceptionHandler(); + handler.Register(Callback); + bool handled = false; + Task Callback(FileNotFoundException exception) + { + handled = true; + return Task.CompletedTask; + } + await handler.HandleAsync(new FileNotFoundException()); + + Assert.True(handled); + } + + [Fact] + public async Task DidHandleSpecificExceptionAsyncWithParameter() + { + var handler = new MulticastExceptionHandler(); + bool handled = false; + var expected = Guid.NewGuid(); + handler.Register(Callback); + + Task Callback(FileNotFoundException fnfe, object? parameter) + { + Assert.Equal(expected, parameter); + handled = true; + return Task.CompletedTask; + } + await handler.HandleAsync(new FileNotFoundException(), expected); + + Assert.True(handled); + } + + [Fact] + public async Task DidHandleSpecificExceptionAsyncWithParameterFirst() + { + var handler = new MulticastExceptionHandler(); + bool handled = false; + var expected = Guid.NewGuid(); + handler.Register(Callback); + + Task Callback(object? parameter, FileNotFoundException fnfe) + { + Assert.Equal(expected, parameter); + handled = true; + return Task.CompletedTask; + } + await handler.HandleAsync(new FileNotFoundException(), expected); + + Assert.True(handled); + } +}