From 3c9bc8485c83ba84618e82f5a12002b356218e95 Mon Sep 17 00:00:00 2001 From: michaelpduda Date: Thu, 15 Aug 2024 14:27:48 -0700 Subject: [PATCH 1/9] Adds an optional fatal error callback to the Hosted service --- .../HostedUpbeatBuilder.cs | 7 +++ .../HostedUpbeatService.cs | 56 +++++++++++++++---- .../IHostedUpbeatBuilder.cs | 9 +++ 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/source/UpbeatUI.Extensions.Hosting/HostedUpbeatBuilder.cs b/source/UpbeatUI.Extensions.Hosting/HostedUpbeatBuilder.cs index 9461ea2..495c044 100644 --- a/source/UpbeatUI.Extensions.Hosting/HostedUpbeatBuilder.cs +++ b/source/UpbeatUI.Extensions.Hosting/HostedUpbeatBuilder.cs @@ -16,6 +16,7 @@ internal sealed class HostedUpbeatBuilder : IHostedUpbeatBuilder internal Func BaseViewModelParametersCreator { get; private set; } internal Collection> MappingRegisterers { get; } = new Collection>(); internal Func WindowCreator { get; private set; } = () => new UpbeatMainWindow(); + internal Action FatalErrorHandler { get; private set; } public IHostedUpbeatBuilder ConfigureWindow(Func windowCreator) { @@ -59,6 +60,12 @@ public IHostedUpbeatBuilder SetDefaultViewModelLocators(bool allowUnresolvedDepe return this; } + public IHostedUpbeatBuilder SetFatalErrorHandler(Action fatalErrorHandler) + { + FatalErrorHandler = fatalErrorHandler; + return this; + } + public IHostedUpbeatBuilder SetViewModelLocators( Func parameterToViewModelLocator, bool allowUnresolvedDependencies = false) diff --git a/source/UpbeatUI.Extensions.Hosting/HostedUpbeatService.cs b/source/UpbeatUI.Extensions.Hosting/HostedUpbeatService.cs index 61f9638..58a7ab6 100644 --- a/source/UpbeatUI.Extensions.Hosting/HostedUpbeatService.cs +++ b/source/UpbeatUI.Extensions.Hosting/HostedUpbeatService.cs @@ -7,12 +7,13 @@ using System.Threading; using System.Threading.Tasks; using System.Windows; +using System.Windows.Threading; using Microsoft.Extensions.Hosting; using UpbeatUI.Extensions.DependencyInjection; namespace UpbeatUI.Extensions.Hosting { - internal sealed class HostedUpbeatService : IHostedService + internal sealed class HostedUpbeatService : IHostedService, IDisposable { private readonly HostedUpbeatBuilder _upbeatHostBuilder; @@ -22,6 +23,7 @@ internal sealed class HostedUpbeatService : IHostedService private Task _executeTask; private TaskCompletionSource _closeRequestedTask = new TaskCompletionSource(); private Window _mainWindow; + private Exception _exception; internal HostedUpbeatService( HostedUpbeatBuilder upbeatHostBuilder, @@ -47,6 +49,14 @@ public Task StopAsync(CancellationToken cancellationToken) return _executeTask; } + public void Dispose() + { + if (_exception != null && _upbeatHostBuilder.FatalErrorHandler != null) + { + _upbeatHostBuilder.FatalErrorHandler(_exception); + } + } + private async Task ExecuteAsync() { try @@ -61,21 +71,33 @@ private async Task ExecuteAsync() _mainWindow.DataContext = upbeatStack; _mainWindow.Closing += HandleMainWindowClosing; upbeatStack.ViewModelsEmptied += HandleUpbeatStackViewModelsEmptied; - upbeatStack.OpenViewModel(_upbeatHostBuilder.BaseViewModelParametersCreator?.Invoke() ?? throw new InvalidOperationException($"No {nameof(_upbeatHostBuilder.BaseViewModelParametersCreator)} provided.")); - _mainWindow.Show(); - while (true) + Application.Current.DispatcherUnhandledException += HandleApplicationException; + try { - _ = await _closeRequestedTask.Task.ConfigureAwait(true); - if (await upbeatStack.TryCloseAllViewModelsAsync().ConfigureAwait(true)) + upbeatStack.OpenViewModel(_upbeatHostBuilder.BaseViewModelParametersCreator?.Invoke() ?? throw new InvalidOperationException($"No {nameof(_upbeatHostBuilder.BaseViewModelParametersCreator)} provided.")); + _mainWindow.Show(); + while (true) { - break; + _ = await _closeRequestedTask.Task.ConfigureAwait(true); + if (await upbeatStack.TryCloseAllViewModelsAsync().ConfigureAwait(true)) + { + break; + } + _closeRequestedTask = new TaskCompletionSource(); } - _closeRequestedTask = new TaskCompletionSource(); } - upbeatStack.ViewModelsEmptied -= HandleUpbeatStackViewModelsEmptied; - _mainWindow.Closing -= HandleMainWindowClosing; - _mainWindow.Close(); - _upbeatApplicationService.CloseRequested -= HandleUpbeatApplicationServiceCloseRequested; + catch (Exception e) + { + _exception ??= e; + } + finally + { + Application.Current.DispatcherUnhandledException -= HandleApplicationException; + upbeatStack.ViewModelsEmptied -= HandleUpbeatStackViewModelsEmptied; + _mainWindow.Closing -= HandleMainWindowClosing; + _mainWindow.Close(); + _upbeatApplicationService.CloseRequested -= HandleUpbeatApplicationServiceCloseRequested; + } } finally { @@ -95,5 +117,15 @@ private void HandleUpbeatApplicationServiceCloseRequested(object sender, EventAr private void HandleUpbeatStackViewModelsEmptied(object sender, EventArgs e) => _ = _closeRequestedTask.TrySetResult(true); + + private void HandleApplicationException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + if (_upbeatHostBuilder.FatalErrorHandler != null) + { + _ = e ?? throw new ArgumentNullException(nameof(e)); + e.Handled = true; + _ = _closeRequestedTask.TrySetException(e.Exception); + } + } } } diff --git a/source/UpbeatUI.Extensions.Hosting/IHostedUpbeatBuilder.cs b/source/UpbeatUI.Extensions.Hosting/IHostedUpbeatBuilder.cs index 0ec18da..a80005a 100644 --- a/source/UpbeatUI.Extensions.Hosting/IHostedUpbeatBuilder.cs +++ b/source/UpbeatUI.Extensions.Hosting/IHostedUpbeatBuilder.cs @@ -67,6 +67,15 @@ IHostedUpbeatBuilder MapViewModel( /// The for chaining. IHostedUpbeatBuilder SetDefaultViewModelLocators(bool allowUnresolvedDependencies = false); + /// + /// Sets a delegate that will be executed prior to application shutdown if there is an unhandled . This can be used to notify the user, log the error, etc... + /// Note: This delegate will execute after all ViewModels have been removed and disposed, and also after the has been disposed. Any other services instantiated by the will likely also have been disposed as well, though this cannot be guaranteed. + /// + /// A delegate with the offending as its parameter. + /// + public IHostedUpbeatBuilder SetFatalErrorHandler(Action fatalErrorHandler); + + /// /// Sets delegates the can use to automatically map a representation of a Parameters to represetantions of a ViewModel and a View . /// Note: Each representation is a From 2c9191e2ce734137fec50b08972e5846dd45bd33 Mon Sep 17 00:00:00 2001 From: michaelpduda Date: Thu, 15 Aug 2024 14:28:49 -0700 Subject: [PATCH 2/9] Updates versions for release candidate --- .../UpbeatUI.Extensions.Hosting.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/UpbeatUI.Extensions.Hosting/UpbeatUI.Extensions.Hosting.csproj b/source/UpbeatUI.Extensions.Hosting/UpbeatUI.Extensions.Hosting.csproj index a75cb4e..49f0917 100644 --- a/source/UpbeatUI.Extensions.Hosting/UpbeatUI.Extensions.Hosting.csproj +++ b/source/UpbeatUI.Extensions.Hosting/UpbeatUI.Extensions.Hosting.csproj @@ -22,8 +22,8 @@ © Michael P. Duda 2020-2024 MIT - 4.2.0 - 4.1.0 + 5.0.0-rc1 + From 829d44f325a527d259d01a527ea5ceb76dbdcfc3 Mon Sep 17 00:00:00 2001 From: michaelpduda Date: Thu, 15 Aug 2024 14:35:25 -0700 Subject: [PATCH 3/9] Adds fatal error handling to samples --- samples/HostedUpbeatUISample/App.xaml.cs | 3 +- .../HostedUpbeatUISample.csproj | 2 +- samples/ManualUpbeatUISample/App.xaml.cs | 154 ++++++++++-------- .../ServiceProvidedUpbeatUISample/App.xaml.cs | 139 ++++++++++------ 4 files changed, 178 insertions(+), 120 deletions(-) diff --git a/samples/HostedUpbeatUISample/App.xaml.cs b/samples/HostedUpbeatUISample/App.xaml.cs index 032d1b5..21351b5 100644 --- a/samples/HostedUpbeatUISample/App.xaml.cs +++ b/samples/HostedUpbeatUISample/App.xaml.cs @@ -37,7 +37,8 @@ await Host.CreateDefaultBuilder(e?.Args ?? Array.Empty()) // Use the .NE WindowStartupLocation = WindowStartupLocation.CenterScreen, ModalBackground = new SolidColorBrush(Brushes.LightGray.Color) { Opacity = 0.5 }, // The brush to display underneath the top View. ModalBlurEffect = new BlurEffect() { Radius = 10.0, KernelType = KernelType.Gaussian }, // The blur effect to apply to Views that are not on top. This is optional, as blur effects can significantly impact performance. - })) + }) + .SetFatalErrorHandler(e => MessageBox.Show($"Exception: {e.GetType().FullName} {e.Message}"))) .Build() .RunAsync().ConfigureAwait(true); } diff --git a/samples/HostedUpbeatUISample/HostedUpbeatUISample.csproj b/samples/HostedUpbeatUISample/HostedUpbeatUISample.csproj index 626def8..37be1ae 100644 --- a/samples/HostedUpbeatUISample/HostedUpbeatUISample.csproj +++ b/samples/HostedUpbeatUISample/HostedUpbeatUISample.csproj @@ -23,7 +23,7 @@ + Version="5.0.0-rc1" /> diff --git a/samples/ManualUpbeatUISample/App.xaml.cs b/samples/ManualUpbeatUISample/App.xaml.cs index 36db9e1..7d7cd5a 100644 --- a/samples/ManualUpbeatUISample/App.xaml.cs +++ b/samples/ManualUpbeatUISample/App.xaml.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using System.Security.Cryptography; using System.Windows.Media.Effects; +using System.Windows.Threading; namespace ManualUpbeatUISample; @@ -19,84 +20,109 @@ public partial class App : Application { // This app would like to provide a way for the user to cancel exiting. To do so, we create a shared task that can be triggered and reset. private TaskCompletionSource _closeRequestedTask = new(); + private Exception _exception; private async void HandleApplicationStartup(object sender, StartupEventArgs e) { - using var sharedTimer = new SharedTimer(); + { + using var sharedTimer = new SharedTimer(); - // The UpbeatStack is the central data structure for an UpbeatUI app. One must be created for the life of the application and should be disposed at the end. - using var upbeatStack = new UpbeatStack(); + // The UpbeatStack is the central data structure for an UpbeatUI app. One must be created for the life of the application and should be disposed at the end. + using var upbeatStack = new UpbeatStack(); - // The UpbeatStack depends on mappings of Parameters types to ViewModels and controls to determine which ViewModel to create and which View to show. Without an IServiceProvider, you must manually map each Parameters, ViewModel, and View type, along with a constructur the IUpbeatStack can call to create a ViewModel. - upbeatStack.MapViewModel( - (service, parameters) => new BottomViewModel(service, sharedTimer)); - upbeatStack.MapViewModel( - (upbeatService, parameters) => new ConfirmPopupViewModel(upbeatService, parameters, sharedTimer)); + // The UpbeatStack depends on mappings of Parameters types to ViewModels and controls to determine which ViewModel to create and which View to show. Without an IServiceProvider, you must manually map each Parameters, ViewModel, and View type, along with a constructur the IUpbeatStack can call to create a ViewModel. + upbeatStack.MapViewModel( + (service, parameters) => new BottomViewModel(service, sharedTimer)); + upbeatStack.MapViewModel( + (upbeatService, parameters) => new ConfirmPopupViewModel(upbeatService, parameters, sharedTimer)); - // The MenuViewModel's constructor requires an Action that it can use to start closing the application. We will provide the shared _closeRequestedTask's TrySetResult() method to indicate the user has requested to close the application. - upbeatStack.MapViewModel( - (upbeatService, parameters) => new MenuViewModel(upbeatService, () => _closeRequestedTask.TrySetResult(), sharedTimer)); - upbeatStack.MapViewModel( - (upbeatService, parameters) => new PopupViewModel(parameters, sharedTimer)); - upbeatStack.MapViewModel( - (upbeatService, parameters) => new RandomDataViewModel(upbeatService, RandomNumberGenerator.Create(), sharedTimer)); - upbeatStack.MapViewModel( - (upbeatService, parameters) => - { - // The SharedListViewModel shares an IUpbeatService and scoped SharedList service with its child ViewModel, the SharedListDataViewModel. The scoped service can be manually created and provided to both. - var sharedList = new SharedList(); - return new SharedListViewModel(upbeatService, sharedList, sharedTimer, - new SharedListDataViewModel(upbeatService, sharedList)); - }); - upbeatStack.MapViewModel( - (upbeatService, parameters) => new TextEntryPopupViewModel(upbeatService, parameters, sharedTimer)); + // The MenuViewModel's constructor requires an Action that it can use to start closing the application. We will provide the shared _closeRequestedTask's TrySetResult() method to indicate the user has requested to close the application. + upbeatStack.MapViewModel( + (upbeatService, parameters) => new MenuViewModel(upbeatService, () => _closeRequestedTask.TrySetResult(), sharedTimer)); + upbeatStack.MapViewModel( + (upbeatService, parameters) => new PopupViewModel(parameters, sharedTimer)); + upbeatStack.MapViewModel( + (upbeatService, parameters) => new RandomDataViewModel(upbeatService, RandomNumberGenerator.Create(), sharedTimer)); + upbeatStack.MapViewModel( + (upbeatService, parameters) => + { + // The SharedListViewModel shares an IUpbeatService and scoped SharedList service with its child ViewModel, the SharedListDataViewModel. The scoped service can be manually created and provided to both. + var sharedList = new SharedList(); + return new SharedListViewModel(upbeatService, sharedList, sharedTimer, + new SharedListDataViewModel(upbeatService, sharedList)); + }); + upbeatStack.MapViewModel( + (upbeatService, parameters) => new TextEntryPopupViewModel(upbeatService, parameters, sharedTimer)); - // The included UpdateMainWindow class already provides the necessary controls to display Views for ViewModels when an IUpbeatStack is set as the DataContext. - var mainWindow = new UpbeatMainWindow() - { - // You must set the DataContext to the UpbeatStack. - DataContext = upbeatStack, - Title = "UpbeatUI Sample Application", - MinHeight = 275, - MinWidth = 275, - Height = 400, - Width = 400, - WindowStartupLocation = WindowStartupLocation.CenterScreen, - ModalBackground = new SolidColorBrush(Brushes.LightGray.Color) { Opacity = 0.5 }, // The brush to display underneath the top View. - ModalBlurEffect = new BlurEffect() { Radius = 10.0, KernelType = KernelType.Gaussian }, // The blur effect to apply to Views that are not on top. This is optional, as blur effects can significantly impact performance. - }; + // The included UpdateMainWindow class already provides the necessary controls to display Views for ViewModels when an IUpbeatStack is set as the DataContext. + var mainWindow = new UpbeatMainWindow() + { + // You must set the DataContext to the UpbeatStack. + DataContext = upbeatStack, + Title = "UpbeatUI Sample Application", + MinHeight = 275, + MinWidth = 275, + Height = 400, + Width = 400, + WindowStartupLocation = WindowStartupLocation.CenterScreen, + ModalBackground = new SolidColorBrush(Brushes.LightGray.Color) { Opacity = 0.5 }, // The brush to display underneath the top View. + ModalBlurEffect = new BlurEffect() { Radius = 10.0, KernelType = KernelType.Gaussian }, // The blur effect to apply to Views that are not on top. This is optional, as blur effects can significantly impact performance. + }; - // Override the default Window Closing event to request a close instead of immediately closing itself. - void HandleMainWindowClosing(object sender, CancelEventArgs e) - { - e.Cancel = true; - _ = _closeRequestedTask.TrySetResult(); - } - mainWindow.Closing += HandleMainWindowClosing; - // When the UpbeatStack has no more view models, the 'ViewModelsEmptied' event will be triggered, so we can count that as a request to close the application. - void HandleUpbeatStackEmpied(object sender, EventArgs e) => _closeRequestedTask.TrySetResult(); - upbeatStack.ViewModelsEmptied += HandleUpbeatStackEmpied; + // Override the default Window Closing event to request a close instead of immediately closing itself. + void HandleMainWindowClosing(object sender, CancelEventArgs e) + { + e.Cancel = true; + _ = _closeRequestedTask.TrySetResult(); + } + mainWindow.Closing += HandleMainWindowClosing; + // When the UpbeatStack has no more view models, the 'ViewModelsEmptied' event will be triggered, so we can count that as a request to close the application. + void HandleUpbeatStackEmpied(object sender, EventArgs e) => _closeRequestedTask.TrySetResult(); + // Add a mechanism to catch unhandled exceptions. + upbeatStack.ViewModelsEmptied += HandleUpbeatStackEmpied; + void HandleApplicationException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + e.Handled = true; + _exception = e.Exception; + _ = _closeRequestedTask.TrySetException(e.Exception); + } + Current.DispatcherUnhandledException += HandleApplicationException; - // Add a base BottomViewModel to the UpbeatStack. - upbeatStack.OpenViewModel(new BottomViewModel.Parameters()); + try + { + // Add a base BottomViewModel to the UpbeatStack. + upbeatStack.OpenViewModel(new BottomViewModel.Parameters()); - // We are now ready to show the main window. - mainWindow.Show(); + // We are now ready to show the main window. + mainWindow.Show(); - // We set up an infinite loop to await the shared _closeRequestedTask, then attempt to close all ViewModels. If successful, we can exit the application. If not successful, possibly because the user cancelled closing, then reset the shared task and await again. - while (true) - { - await _closeRequestedTask.Task.ConfigureAwait(true); - if (await upbeatStack.TryCloseAllViewModelsAsync().ConfigureAwait(true)) + // We set up an infinite loop to await the shared _closeRequestedTask, then attempt to close all ViewModels. If successful, we can exit the application. If not successful, possibly because the user cancelled closing, then reset the shared task and await again. + while (true) + { + await _closeRequestedTask.Task.ConfigureAwait(true); + if (await upbeatStack.TryCloseAllViewModelsAsync().ConfigureAwait(true)) + { + break; + } + _closeRequestedTask = new TaskCompletionSource(); + } + } + catch (Exception ex) { - break; + _exception ??= ex; + } + finally + { + Current.DispatcherUnhandledException -= HandleApplicationException; + upbeatStack.ViewModelsEmptied -= HandleUpbeatStackEmpied; + mainWindow.Closing -= HandleMainWindowClosing; + mainWindow.Close(); } - _closeRequestedTask = new TaskCompletionSource(); } - - upbeatStack.ViewModelsEmptied -= HandleUpbeatStackEmpied; - mainWindow.Closing -= HandleMainWindowClosing; - mainWindow.Close(); + if (_exception is not null) + { + _ = MessageBox.Show($"Exception: {_exception.GetType().FullName} {_exception.Message}"); + } } } diff --git a/samples/ServiceProvidedUpbeatUISample/App.xaml.cs b/samples/ServiceProvidedUpbeatUISample/App.xaml.cs index d5be4f7..4d77d28 100644 --- a/samples/ServiceProvidedUpbeatUISample/App.xaml.cs +++ b/samples/ServiceProvidedUpbeatUISample/App.xaml.cs @@ -13,6 +13,7 @@ using System.Threading.Tasks; using System.Security.Cryptography; using System.Windows.Media.Effects; +using System.Windows.Threading; namespace ServiceProvidedUpbeatUISample; @@ -20,75 +21,105 @@ public partial class App : Application { // This app would like to provide a way for the user to cancel exiting. To do so, we create a shared task that can be triggered and reset. private TaskCompletionSource _closeRequestedTask = new(); + private Exception _exception; private async void HandleApplicationStartup(object sender, StartupEventArgs e) { - // Use a ServiceProvider (built from a ServiceCollection) to set up dependencies that the ServiceProvidedUpbeatStack will inject into ViewModels. Scoped services are supported, and each ViewModel within the stack is a separate scope. - using var serviceProvider = new ServiceCollection() - .AddTransient(sp => RandomNumberGenerator.Create()) - .AddSingleton() - .AddScoped() - .BuildServiceProvider(); + { + // Use a ServiceProvider (built from a ServiceCollection) to set up dependencies that the ServiceProvidedUpbeatStack will inject into ViewModels. Scoped services are supported, and each ViewModel within the stack is a separate scope. + using var serviceProvider = new ServiceCollection() + .AddTransient(sp => RandomNumberGenerator.Create()) + .AddSingleton() + .AddScoped() + .BuildServiceProvider(); - // The ServiceProvidedUpbeatStack is the central data structure for an UpbeatUI app. One must be created for the life of the application and should be disposed at the end. Unlike the basic UpbeatStack, the ServiceProvidedUpbeatStack requires an IServiceProvider to resolve dependencies for ViewModels. - using var upbeatStack = new ServiceProvidedUpbeatStack(serviceProvider); + // The ServiceProvidedUpbeatStack is the central data structure for an UpbeatUI app. One must be created for the life of the application and should be disposed at the end. Unlike the basic UpbeatStack, the ServiceProvidedUpbeatStack requires an IServiceProvider to resolve dependencies for ViewModels. + using var upbeatStack = new ServiceProvidedUpbeatStack(serviceProvider); - // Instead of manually mapping Parameters types to ViewModels and controls, the ServiceProvidedUpbeatStack can automatically map types based on naming convention. Use this method to enable the default naming convention, but other methods enable you to use your own naming conventions. - upbeatStack.SetDefaultViewModelLocators(); + // Instead of manually mapping Parameters types to ViewModels and controls, the ServiceProvidedUpbeatStack can automatically map types based on naming convention. Use this method to enable the default naming convention, but other methods enable you to use your own naming conventions. + upbeatStack.SetDefaultViewModelLocators(); - // The MenuViewModel's constructor requires an Action that it can use to start closing the application. We will provide the shared _closeRequestedTask's TrySetResult() method to indicate the user has requested to close the application. Because a simple Action should not be registered with the ServiceProvider (at least without creating an additional service class), we will instead manually map the MenuViewModel's constructor to its Parameters type. - upbeatStack.MapViewModel( - (upbeatService, parameters, serviceProvider) => new MenuViewModel( - upbeatService, - () => _closeRequestedTask.TrySetResult(), - serviceProvider.GetRequiredService())); + // The MenuViewModel's constructor requires an Action that it can use to start closing the application. We will provide the shared _closeRequestedTask's TrySetResult() method to indicate the user has requested to close the application. Because a simple Action should not be registered with the ServiceProvider (at least without creating an additional service class), we will instead manually map the MenuViewModel's constructor to its Parameters type. + upbeatStack.MapViewModel( + (upbeatService, parameters, serviceProvider) => new MenuViewModel( + upbeatService, + () => _closeRequestedTask.TrySetResult(), + serviceProvider.GetRequiredService())); - // The included UpdateMainWindow class already provides the necessary controls to display Views for ViewModels when an IUpbeatStack is set as the DataContext. - var mainWindow = new UpbeatMainWindow() - { - // You must set the DataContext to the ServiceProvidedUpbeatStack. - DataContext = upbeatStack, - Title = "UpbeatUI Sample Application", - MinHeight = 275, - MinWidth = 275, - Height = 400, - Width = 400, - WindowStartupLocation = WindowStartupLocation.CenterScreen, - ModalBackground = new SolidColorBrush(Brushes.LightGray.Color) { Opacity = 0.5 }, // The brush to display underneath the top View. - ModalBlurEffect = new BlurEffect() { Radius = 10.0, KernelType = KernelType.Gaussian }, // The blur effect to apply to Views that are not on top. This is optional, as blur effects can significantly impact performance. - }; + // The included UpdateMainWindow class already provides the necessary controls to display Views for ViewModels when an IUpbeatStack is set as the DataContext. + var mainWindow = new UpbeatMainWindow() + { + // You must set the DataContext to the ServiceProvidedUpbeatStack. + DataContext = upbeatStack, + Title = "UpbeatUI Sample Application", + MinHeight = 275, + MinWidth = 275, + Height = 400, + Width = 400, + WindowStartupLocation = WindowStartupLocation.CenterScreen, + ModalBackground = new SolidColorBrush(Brushes.LightGray.Color) { Opacity = 0.5 }, // The brush to display underneath the top View. + ModalBlurEffect = new BlurEffect() { Radius = 10.0, KernelType = KernelType.Gaussian }, // The blur effect to apply to Views that are not on top. This is optional, as blur effects can significantly impact performance. + }; - // Override the default Window Closing event to request a close instead of immediately closing itself. - void HandleMainWindowClosing(object sender, CancelEventArgs e) - { - e.Cancel = true; - _ = _closeRequestedTask.TrySetResult(); - } - mainWindow.Closing += HandleMainWindowClosing; - // When the UpbeatStack has no more view models, the 'ViewModelsEmptied' event will be triggered, so we can count that as a request to close the application. - void HandleUpbeatStackEmpied(object sender, EventArgs e) => _closeRequestedTask.TrySetResult(); - upbeatStack.ViewModelsEmptied += HandleUpbeatStackEmpied; + // Override the default Window Closing event to request a close instead of immediately closing itself. + void HandleMainWindowClosing(object sender, CancelEventArgs e) + { + e.Cancel = true; + _ = _closeRequestedTask.TrySetResult(); + } + mainWindow.Closing += HandleMainWindowClosing; + // When the UpbeatStack has no more view models, the 'ViewModelsEmptied' event will be triggered, so we can count that as a request to close the application. + void HandleUpbeatStackEmpied(object sender, EventArgs e) => _closeRequestedTask.TrySetResult(); + upbeatStack.ViewModelsEmptied += HandleUpbeatStackEmpied; + // Add a mechanism to catch unhandled exceptions. + void HandleApplicationException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + e.Handled = true; + _exception = e.Exception; + _ = _closeRequestedTask.TrySetException(e.Exception); + } + Current.DispatcherUnhandledException += HandleApplicationException; - // Add a base BottomViewModel to the UpbeatStack. - upbeatStack.OpenViewModel(new BottomViewModel.Parameters()); + try + { + // Add a base BottomViewModel to the UpbeatStack. + upbeatStack.OpenViewModel(new BottomViewModel.Parameters()); - // We are now ready to show the main window. - mainWindow.Show(); + // We are now ready to show the main window. + mainWindow.Show(); - // We set up an infinite loop to await the shared _closeRequestedTask, then attempt to close all ViewModels. If successful, we can exit the application. If not successful, possibly because the user cancelled closing, then reset the shared task and await again. - while (true) - { - await _closeRequestedTask.Task.ConfigureAwait(true); - if (await upbeatStack.TryCloseAllViewModelsAsync().ConfigureAwait(true)) + // We set up an infinite loop to await the shared _closeRequestedTask, then attempt to close all ViewModels. If successful, we can exit the application. If not successful, possibly because the user cancelled closing, then reset the shared task and await again. + while (true) + { + await _closeRequestedTask.Task.ConfigureAwait(true); + if (await upbeatStack.TryCloseAllViewModelsAsync().ConfigureAwait(true)) + { + break; + } + _closeRequestedTask = new TaskCompletionSource(); + } + } + catch (Exception ex) + { + _exception ??= ex; + } + finally { - break; + upbeatStack.ViewModelsEmptied -= HandleUpbeatStackEmpied; + mainWindow.Closing -= HandleMainWindowClosing; + mainWindow.Close(); } - _closeRequestedTask = new TaskCompletionSource(); } + if (_exception is not null) + { + _ = MessageBox.Show($"Exception: {_exception.GetType().FullName} {_exception.Message}"); + } + } - upbeatStack.ViewModelsEmptied -= HandleUpbeatStackEmpied; - mainWindow.Closing -= HandleMainWindowClosing; - mainWindow.Close(); + private void HandleApplicationException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + e.Handled = true; + _ = _closeRequestedTask.TrySetException(e.Exception); } } From 61b681565869ca2ad986693b1686ae5066d92438 Mon Sep 17 00:00:00 2001 From: michaelpduda Date: Fri, 16 Aug 2024 14:44:02 -0700 Subject: [PATCH 4/9] Moves error handler execution to earlier in the application lifecycle --- .../HostedUpbeatBuilder.cs | 10 ++- .../HostedUpbeatService.cs | 74 +++++++++---------- .../IHostedUpbeatBuilder.cs | 12 ++- 3 files changed, 54 insertions(+), 42 deletions(-) diff --git a/source/UpbeatUI.Extensions.Hosting/HostedUpbeatBuilder.cs b/source/UpbeatUI.Extensions.Hosting/HostedUpbeatBuilder.cs index 495c044..ba65a9a 100644 --- a/source/UpbeatUI.Extensions.Hosting/HostedUpbeatBuilder.cs +++ b/source/UpbeatUI.Extensions.Hosting/HostedUpbeatBuilder.cs @@ -16,7 +16,7 @@ internal sealed class HostedUpbeatBuilder : IHostedUpbeatBuilder internal Func BaseViewModelParametersCreator { get; private set; } internal Collection> MappingRegisterers { get; } = new Collection>(); internal Func WindowCreator { get; private set; } = () => new UpbeatMainWindow(); - internal Action FatalErrorHandler { get; private set; } + internal Action FatalErrorHandler { get; private set; } public IHostedUpbeatBuilder ConfigureWindow(Func windowCreator) { @@ -60,12 +60,18 @@ public IHostedUpbeatBuilder SetDefaultViewModelLocators(bool allowUnresolvedDepe return this; } - public IHostedUpbeatBuilder SetFatalErrorHandler(Action fatalErrorHandler) + public IHostedUpbeatBuilder SetFatalErrorHandler(Action fatalErrorHandler) { FatalErrorHandler = fatalErrorHandler; return this; } + public IHostedUpbeatBuilder SetFatalErrorHandler(Action fatalErrorHandler) + { + FatalErrorHandler = (_, e) => fatalErrorHandler(e); + return this; + } + public IHostedUpbeatBuilder SetViewModelLocators( Func parameterToViewModelLocator, bool allowUnresolvedDependencies = false) diff --git a/source/UpbeatUI.Extensions.Hosting/HostedUpbeatService.cs b/source/UpbeatUI.Extensions.Hosting/HostedUpbeatService.cs index 58a7ab6..508e305 100644 --- a/source/UpbeatUI.Extensions.Hosting/HostedUpbeatService.cs +++ b/source/UpbeatUI.Extensions.Hosting/HostedUpbeatService.cs @@ -13,7 +13,7 @@ namespace UpbeatUI.Extensions.Hosting { - internal sealed class HostedUpbeatService : IHostedService, IDisposable + internal sealed class HostedUpbeatService : IHostedService { private readonly HostedUpbeatBuilder _upbeatHostBuilder; @@ -49,54 +49,52 @@ public Task StopAsync(CancellationToken cancellationToken) return _executeTask; } - public void Dispose() - { - if (_exception != null && _upbeatHostBuilder.FatalErrorHandler != null) - { - _upbeatHostBuilder.FatalErrorHandler(_exception); - } - } - private async Task ExecuteAsync() { try { - using var upbeatStack = new ServiceProvidedUpbeatStack(_serviceProvider); - _upbeatApplicationService.CloseRequested += HandleUpbeatApplicationServiceCloseRequested; - foreach (var registerer in _upbeatHostBuilder.MappingRegisterers ?? throw new InvalidOperationException($"No {nameof(_upbeatHostBuilder.MappingRegisterers)} provided.")) - { - registerer.Invoke(upbeatStack); - } - _mainWindow = _upbeatHostBuilder.WindowCreator?.Invoke() ?? throw new InvalidOperationException($"No {nameof(_upbeatHostBuilder.WindowCreator)} provided."); - _mainWindow.DataContext = upbeatStack; - _mainWindow.Closing += HandleMainWindowClosing; - upbeatStack.ViewModelsEmptied += HandleUpbeatStackViewModelsEmptied; - Application.Current.DispatcherUnhandledException += HandleApplicationException; - try + using (var upbeatStack = new ServiceProvidedUpbeatStack(_serviceProvider)) { - upbeatStack.OpenViewModel(_upbeatHostBuilder.BaseViewModelParametersCreator?.Invoke() ?? throw new InvalidOperationException($"No {nameof(_upbeatHostBuilder.BaseViewModelParametersCreator)} provided.")); - _mainWindow.Show(); - while (true) + _upbeatApplicationService.CloseRequested += HandleUpbeatApplicationServiceCloseRequested; + foreach (var registerer in _upbeatHostBuilder.MappingRegisterers ?? throw new InvalidOperationException($"No {nameof(_upbeatHostBuilder.MappingRegisterers)} provided.")) { - _ = await _closeRequestedTask.Task.ConfigureAwait(true); - if (await upbeatStack.TryCloseAllViewModelsAsync().ConfigureAwait(true)) + registerer.Invoke(upbeatStack); + } + _mainWindow = _upbeatHostBuilder.WindowCreator?.Invoke() ?? throw new InvalidOperationException($"No {nameof(_upbeatHostBuilder.WindowCreator)} provided."); + _mainWindow.DataContext = upbeatStack; + _mainWindow.Closing += HandleMainWindowClosing; + upbeatStack.ViewModelsEmptied += HandleUpbeatStackViewModelsEmptied; + Application.Current.DispatcherUnhandledException += HandleApplicationException; + try + { + upbeatStack.OpenViewModel(_upbeatHostBuilder.BaseViewModelParametersCreator?.Invoke() ?? throw new InvalidOperationException($"No {nameof(_upbeatHostBuilder.BaseViewModelParametersCreator)} provided.")); + _mainWindow.Show(); + while (true) { - break; + _ = await _closeRequestedTask.Task.ConfigureAwait(true); + if (await upbeatStack.TryCloseAllViewModelsAsync().ConfigureAwait(true)) + { + break; + } + _closeRequestedTask = new TaskCompletionSource(); } - _closeRequestedTask = new TaskCompletionSource(); + } + catch (Exception e) + { + _exception ??= e; + } + finally + { + Application.Current.DispatcherUnhandledException -= HandleApplicationException; + upbeatStack.ViewModelsEmptied -= HandleUpbeatStackViewModelsEmptied; + _mainWindow.Closing -= HandleMainWindowClosing; + _mainWindow.Close(); + _upbeatApplicationService.CloseRequested -= HandleUpbeatApplicationServiceCloseRequested; } } - catch (Exception e) - { - _exception ??= e; - } - finally + if (_exception != null && _upbeatHostBuilder.FatalErrorHandler != null) { - Application.Current.DispatcherUnhandledException -= HandleApplicationException; - upbeatStack.ViewModelsEmptied -= HandleUpbeatStackViewModelsEmptied; - _mainWindow.Closing -= HandleMainWindowClosing; - _mainWindow.Close(); - _upbeatApplicationService.CloseRequested -= HandleUpbeatApplicationServiceCloseRequested; + _upbeatHostBuilder.FatalErrorHandler(_serviceProvider, _exception); } } finally diff --git a/source/UpbeatUI.Extensions.Hosting/IHostedUpbeatBuilder.cs b/source/UpbeatUI.Extensions.Hosting/IHostedUpbeatBuilder.cs index a80005a..b9f3b0c 100644 --- a/source/UpbeatUI.Extensions.Hosting/IHostedUpbeatBuilder.cs +++ b/source/UpbeatUI.Extensions.Hosting/IHostedUpbeatBuilder.cs @@ -67,11 +67,19 @@ IHostedUpbeatBuilder MapViewModel( /// The for chaining. IHostedUpbeatBuilder SetDefaultViewModelLocators(bool allowUnresolvedDependencies = false); + /// + /// Sets a delegate that will be executed prior to application shutdown if there is an unhandled . This can be used to notify the user, log the error, etc... The application's is also available to access registered services, though the health of any singleton services cannot be guaranteed and will depend on their use within the application and the nature of the offending . + /// Note: This delegate will execute after all ViewModels have been removed and disposed, and also after the has been disposed. + /// + /// A delegate with the application's and the offending as its parameters. + /// + public IHostedUpbeatBuilder SetFatalErrorHandler(Action fatalErrorHandler); + /// /// Sets a delegate that will be executed prior to application shutdown if there is an unhandled . This can be used to notify the user, log the error, etc... - /// Note: This delegate will execute after all ViewModels have been removed and disposed, and also after the has been disposed. Any other services instantiated by the will likely also have been disposed as well, though this cannot be guaranteed. + /// Note: This delegate will execute after all ViewModels have been removed and disposed, and also after the has been disposed. /// - /// A delegate with the offending as its parameter. + /// A delegate with the the offending as its parameter. /// public IHostedUpbeatBuilder SetFatalErrorHandler(Action fatalErrorHandler); From d6e049917d4ed1737776b8779b0aa498084fb139 Mon Sep 17 00:00:00 2001 From: michaelpduda Date: Fri, 16 Aug 2024 14:48:00 -0700 Subject: [PATCH 5/9] Updates versions for release candidate --- .../UpbeatUI.Extensions.Hosting.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/UpbeatUI.Extensions.Hosting/UpbeatUI.Extensions.Hosting.csproj b/source/UpbeatUI.Extensions.Hosting/UpbeatUI.Extensions.Hosting.csproj index 49f0917..619a48c 100644 --- a/source/UpbeatUI.Extensions.Hosting/UpbeatUI.Extensions.Hosting.csproj +++ b/source/UpbeatUI.Extensions.Hosting/UpbeatUI.Extensions.Hosting.csproj @@ -22,7 +22,7 @@ © Michael P. Duda 2020-2024 MIT - 5.0.0-rc1 + 5.0.0-rc2 From 2ae5519c7269e65ed43b687bc09dc5d9b6ca505c Mon Sep 17 00:00:00 2001 From: michaelpduda Date: Fri, 16 Aug 2024 15:19:32 -0700 Subject: [PATCH 6/9] Updates error handling in samples --- samples/HostedUpbeatUISample/App.xaml.cs | 16 +++++++++- .../HostedUpbeatUISample.csproj | 4 +-- samples/ManualUpbeatUISample/App.xaml.cs | 21 ++++++++++--- .../ServiceProvidedUpbeatUISample/App.xaml.cs | 31 +++++++++++++------ 4 files changed, 54 insertions(+), 18 deletions(-) diff --git a/samples/HostedUpbeatUISample/App.xaml.cs b/samples/HostedUpbeatUISample/App.xaml.cs index 21351b5..ceb7a86 100644 --- a/samples/HostedUpbeatUISample/App.xaml.cs +++ b/samples/HostedUpbeatUISample/App.xaml.cs @@ -38,7 +38,21 @@ await Host.CreateDefaultBuilder(e?.Args ?? Array.Empty()) // Use the .NE ModalBackground = new SolidColorBrush(Brushes.LightGray.Color) { Opacity = 0.5 }, // The brush to display underneath the top View. ModalBlurEffect = new BlurEffect() { Radius = 10.0, KernelType = KernelType.Gaussian }, // The blur effect to apply to Views that are not on top. This is optional, as blur effects can significantly impact performance. }) - .SetFatalErrorHandler(e => MessageBox.Show($"Exception: {e.GetType().FullName} {e.Message}"))) + .SetFatalErrorHandler(e => + { + if (MessageBox.Show( + $"Error message: {e.Message}. See stack trace?", + "Fatal Error", + MessageBoxButton.YesNo, + MessageBoxImage.Error) == MessageBoxResult.Yes) + { + _ = MessageBox.Show( + e.StackTrace, + "Fatal Error", + MessageBoxButton.OK, + MessageBoxImage.None); + } + })) .Build() .RunAsync().ConfigureAwait(true); } diff --git a/samples/HostedUpbeatUISample/HostedUpbeatUISample.csproj b/samples/HostedUpbeatUISample/HostedUpbeatUISample.csproj index 37be1ae..8789a58 100644 --- a/samples/HostedUpbeatUISample/HostedUpbeatUISample.csproj +++ b/samples/HostedUpbeatUISample/HostedUpbeatUISample.csproj @@ -17,13 +17,13 @@ Version="8.2.*" /> + Version="7.0.1" /> + Version="5.0.0-rc2" /> diff --git a/samples/ManualUpbeatUISample/App.xaml.cs b/samples/ManualUpbeatUISample/App.xaml.cs index 7d7cd5a..da32786 100644 --- a/samples/ManualUpbeatUISample/App.xaml.cs +++ b/samples/ManualUpbeatUISample/App.xaml.cs @@ -24,11 +24,11 @@ public partial class App : Application private async void HandleApplicationStartup(object sender, StartupEventArgs e) { - { - using var sharedTimer = new SharedTimer(); + using var sharedTimer = new SharedTimer(); - // The UpbeatStack is the central data structure for an UpbeatUI app. One must be created for the life of the application and should be disposed at the end. - using var upbeatStack = new UpbeatStack(); + // The UpbeatStack is the central data structure for an UpbeatUI app. One must be created for the life of the application and should be disposed at the end. + using (var upbeatStack = new UpbeatStack()) + { // The UpbeatStack depends on mappings of Parameters types to ViewModels and controls to determine which ViewModel to create and which View to show. Without an IServiceProvider, you must manually map each Parameters, ViewModel, and View type, along with a constructur the IUpbeatStack can call to create a ViewModel. upbeatStack.MapViewModel( @@ -121,7 +121,18 @@ void HandleApplicationException(object sender, DispatcherUnhandledExceptionEvent } if (_exception is not null) { - _ = MessageBox.Show($"Exception: {_exception.GetType().FullName} {_exception.Message}"); + if (MessageBox.Show( + $"Error message: {_exception.Message}. See stack trace?", + "Fatal Error", + MessageBoxButton.YesNo, + MessageBoxImage.Error) == MessageBoxResult.Yes) + { + _ = MessageBox.Show( + _exception.StackTrace, + "Fatal Error", + MessageBoxButton.OK, + MessageBoxImage.None); + } } } } diff --git a/samples/ServiceProvidedUpbeatUISample/App.xaml.cs b/samples/ServiceProvidedUpbeatUISample/App.xaml.cs index 4d77d28..d144cd0 100644 --- a/samples/ServiceProvidedUpbeatUISample/App.xaml.cs +++ b/samples/ServiceProvidedUpbeatUISample/App.xaml.cs @@ -25,16 +25,16 @@ public partial class App : Application private async void HandleApplicationStartup(object sender, StartupEventArgs e) { - { - // Use a ServiceProvider (built from a ServiceCollection) to set up dependencies that the ServiceProvidedUpbeatStack will inject into ViewModels. Scoped services are supported, and each ViewModel within the stack is a separate scope. - using var serviceProvider = new ServiceCollection() - .AddTransient(sp => RandomNumberGenerator.Create()) - .AddSingleton() - .AddScoped() - .BuildServiceProvider(); + // Use a ServiceProvider (built from a ServiceCollection) to set up dependencies that the ServiceProvidedUpbeatStack will inject into ViewModels. Scoped services are supported, and each ViewModel within the stack is a separate scope. + using var serviceProvider = new ServiceCollection() + .AddTransient(sp => RandomNumberGenerator.Create()) + .AddSingleton() + .AddScoped() + .BuildServiceProvider(); - // The ServiceProvidedUpbeatStack is the central data structure for an UpbeatUI app. One must be created for the life of the application and should be disposed at the end. Unlike the basic UpbeatStack, the ServiceProvidedUpbeatStack requires an IServiceProvider to resolve dependencies for ViewModels. - using var upbeatStack = new ServiceProvidedUpbeatStack(serviceProvider); + // The ServiceProvidedUpbeatStack is the central data structure for an UpbeatUI app. One must be created for the life of the application and should be disposed at the end. Unlike the basic UpbeatStack, the ServiceProvidedUpbeatStack requires an IServiceProvider to resolve dependencies for ViewModels. + using (var upbeatStack = new ServiceProvidedUpbeatStack(serviceProvider)) + { // Instead of manually mapping Parameters types to ViewModels and controls, the ServiceProvidedUpbeatStack can automatically map types based on naming convention. Use this method to enable the default naming convention, but other methods enable you to use your own naming conventions. upbeatStack.SetDefaultViewModelLocators(); @@ -112,7 +112,18 @@ void HandleApplicationException(object sender, DispatcherUnhandledExceptionEvent } if (_exception is not null) { - _ = MessageBox.Show($"Exception: {_exception.GetType().FullName} {_exception.Message}"); + if (MessageBox.Show( + $"Error message: {_exception.Message}. See stack trace?", + "Fatal Error", + MessageBoxButton.YesNo, + MessageBoxImage.Error) == MessageBoxResult.Yes) + { + _ = MessageBox.Show( + _exception.StackTrace, + "Fatal Error", + MessageBoxButton.OK, + MessageBoxImage.None); + } } } From 7fc03fbb19717fa340db0ffea55e9b8b893a7775 Mon Sep 17 00:00:00 2001 From: michaelpduda Date: Mon, 19 Aug 2024 11:52:10 -0700 Subject: [PATCH 7/9] Stops swallowing some exceptions when no FataErrorHandler set --- .../HostedUpbeatService.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/source/UpbeatUI.Extensions.Hosting/HostedUpbeatService.cs b/source/UpbeatUI.Extensions.Hosting/HostedUpbeatService.cs index 508e305..a012afb 100644 --- a/source/UpbeatUI.Extensions.Hosting/HostedUpbeatService.cs +++ b/source/UpbeatUI.Extensions.Hosting/HostedUpbeatService.cs @@ -81,7 +81,14 @@ private async Task ExecuteAsync() } catch (Exception e) { - _exception ??= e; + if (_upbeatHostBuilder.FatalErrorHandler != null) + { + _exception ??= e; + } + else + { + throw; + } } finally { @@ -92,7 +99,7 @@ private async Task ExecuteAsync() _upbeatApplicationService.CloseRequested -= HandleUpbeatApplicationServiceCloseRequested; } } - if (_exception != null && _upbeatHostBuilder.FatalErrorHandler != null) + if (_exception != null) { _upbeatHostBuilder.FatalErrorHandler(_serviceProvider, _exception); } From 6674c169aa38a20a05d7f9ab67fd49a6a17738bf Mon Sep 17 00:00:00 2001 From: michaelpduda Date: Mon, 19 Aug 2024 11:52:36 -0700 Subject: [PATCH 8/9] Fixes setting a null FatalErrorHandler --- .../UpbeatUI.Extensions.Hosting/HostedUpbeatBuilder.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/source/UpbeatUI.Extensions.Hosting/HostedUpbeatBuilder.cs b/source/UpbeatUI.Extensions.Hosting/HostedUpbeatBuilder.cs index ba65a9a..905512b 100644 --- a/source/UpbeatUI.Extensions.Hosting/HostedUpbeatBuilder.cs +++ b/source/UpbeatUI.Extensions.Hosting/HostedUpbeatBuilder.cs @@ -68,7 +68,14 @@ public IHostedUpbeatBuilder SetFatalErrorHandler(Action fatalErrorHandler) { - FatalErrorHandler = (_, e) => fatalErrorHandler(e); + if (fatalErrorHandler == null) + { + fatalErrorHandler = null; + } + else + { + return SetFatalErrorHandler((_, e) => fatalErrorHandler(e)); + } return this; } From db6c5cf3e289513cee36ac92b19255fe74682ab7 Mon Sep 17 00:00:00 2001 From: michaelpduda Date: Mon, 19 Aug 2024 11:54:18 -0700 Subject: [PATCH 9/9] Updates versions for release candidate --- .../UpbeatUI.Extensions.Hosting.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/UpbeatUI.Extensions.Hosting/UpbeatUI.Extensions.Hosting.csproj b/source/UpbeatUI.Extensions.Hosting/UpbeatUI.Extensions.Hosting.csproj index 619a48c..afc9ea1 100644 --- a/source/UpbeatUI.Extensions.Hosting/UpbeatUI.Extensions.Hosting.csproj +++ b/source/UpbeatUI.Extensions.Hosting/UpbeatUI.Extensions.Hosting.csproj @@ -22,7 +22,7 @@ © Michael P. Duda 2020-2024 MIT - 5.0.0-rc2 + 5.0.0-rc3