diff --git a/src/BitzArt.Blazor.MVVM/Components/PageBase{TViewModel}.cs b/src/BitzArt.Blazor.MVVM/Components/PageBase{TViewModel}.cs index 3be1e96..f0f83e2 100644 --- a/src/BitzArt.Blazor.MVVM/Components/PageBase{TViewModel}.cs +++ b/src/BitzArt.Blazor.MVVM/Components/PageBase{TViewModel}.cs @@ -21,6 +21,25 @@ public abstract class PageBase : ComponentBase, IStateCo private const string StateKey = "state"; + public delegate Task PageExceptionHandler(object sender, Exception ex); + + /// + /// Called when an exception is thrown within the scope of the page and needs to be handled. + /// + public PageExceptionHandler? ExceptionHandler { get; set; } + + override protected void OnInitialized() + { + base.OnInitialized(); + ViewModel.ExceptionHandler += (sender, ex) => + { + if (ExceptionHandler is null) + return Task.CompletedTask; + + return ExceptionHandler.Invoke(sender, ex); + }; + } + protected override async Task RestoreStateAsync() { var state = await Js.InvokeAsync("getInnerText", [StateKey]); diff --git a/src/BitzArt.Blazor.MVVM/Services/ViewModelFactory.cs b/src/BitzArt.Blazor.MVVM/Services/ViewModelFactory.cs index d2fceb5..42b32c9 100644 --- a/src/BitzArt.Blazor.MVVM/Services/ViewModelFactory.cs +++ b/src/BitzArt.Blazor.MVVM/Services/ViewModelFactory.cs @@ -75,6 +75,14 @@ public ViewModel Create(IServiceProvider serviceProvider, Type viewModelType, Co if (injectedViewModel.OnComponentStateContainerWasSet is not null) await injectedViewModel.OnComponentStateContainerWasSet.Invoke(container); }; + + injectedViewModel.ExceptionHandler += async (sender, exception) => + { + if (viewModel.ExceptionHandler is null) + return; + + await viewModel.ExceptionHandler.Invoke(sender, exception); + }; } else if (injection.IsParentViewModelInjection) diff --git a/tests/BitzArt.Blazor.MVVM.Tests/ExceptionHandlingTests.cs b/tests/BitzArt.Blazor.MVVM.Tests/ExceptionHandlingTests.cs new file mode 100644 index 0000000..56b06fa --- /dev/null +++ b/tests/BitzArt.Blazor.MVVM.Tests/ExceptionHandlingTests.cs @@ -0,0 +1,68 @@ +using BitzArt.Blazor.MVVM.Tests.ViewModels; +using Microsoft.Extensions.DependencyInjection; + +namespace BitzArt.Blazor.MVVM.Tests; + +public class ExceptionHandlingTests +{ + [Fact] + public async Task HandleAsync_WhenExceptionIsThrown_ShouldInvokeExceptionHandler() + { + // Arrange + var viewModel = new TestLayer1ViewModel(); + var exceptionHandlerInvoked = false; + + viewModel.ExceptionHandler = (sender, ex) => + { + exceptionHandlerInvoked = true; + return Task.CompletedTask; + }; + + // Act + try + { + await viewModel.HandleAsync(() => throw new Exception()); + } + catch (Exception) + { + } + + // Assert + Assert.True(exceptionHandlerInvoked); + } + + [Fact] + public async Task HandleAsync_HierarchicalStructure_ShouldForwardToRoot() + { + // Arrange + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddBlazorMvvm() + .AddViewModel() + .AddViewModel(); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + var rootViewModel = serviceProvider.GetRequiredService(); + var childViewModel = rootViewModel.TestLayer2ViewModel; + + var rootExceptionHandlerInvoked = false; + rootViewModel.ExceptionHandler += (sender, ex) => + { + rootExceptionHandlerInvoked = true; + return Task.CompletedTask; + }; + + // Act + try + { + await childViewModel.HandleAsync(() => throw new Exception()); + } + catch (Exception) + { + } + + // Assert + Assert.True(rootExceptionHandlerInvoked); + } +} diff --git a/tests/BitzArt.Blazor.MVVM.Tests/ViewModels/ExceptionHandlingTests.cs b/tests/BitzArt.Blazor.MVVM.Tests/ViewModels/ExceptionHandlingTests.cs deleted file mode 100644 index 723db94..0000000 --- a/tests/BitzArt.Blazor.MVVM.Tests/ViewModels/ExceptionHandlingTests.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace BitzArt.Blazor.MVVM.Tests.ViewModels; - -public class ExceptionHandlingTests -{ - [Fact] - public async Task HandleAsync_WhenExceptionIsThrown_ShouldInvokeExceptionHandler() - { - // Arrange - var viewModel = new TestLayer1ViewModel(); - var exceptionHandlerInvoked = false; - - viewModel.ExceptionHandler = (sender, ex) => - { - exceptionHandlerInvoked = true; - return Task.CompletedTask; - }; - - // Act - try - { - await viewModel.HandleAsync(() => throw new Exception()); - } - catch (Exception) - { - } - - // Assert - Assert.True(exceptionHandlerInvoked); - } -}