diff --git a/LibreHardwareMonitor.Windows.WinUI/App.xaml.cs b/LibreHardwareMonitor.Windows.WinUI/App.xaml.cs index 77bb031..14a1625 100644 --- a/LibreHardwareMonitor.Windows.WinUI/App.xaml.cs +++ b/LibreHardwareMonitor.Windows.WinUI/App.xaml.cs @@ -5,7 +5,10 @@ using System; using System.IO; using System.Threading.Tasks; +using LibreHardwareMonitor.Windows.WinUI.Composition; using LibreHardwareMonitor.Windows.WinUI.Services.Tracing; +using LibreHardwareMonitor.Windows.WinUI.ViewModels; +using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Xaml; using IOPath = System.IO.Path; @@ -15,6 +18,7 @@ public partial class App : Application { private bool _launchStarted; private readonly IStartupTracer _startupTrace; + private ServiceProvider? _serviceProvider; private Window? _window; public App() @@ -46,7 +50,13 @@ public partial class App : Application try { _startupTrace.Mark("App.LaunchMainWindow.Begin"); - MainWindow mainWindow = MeasureStartup("App.CreateMainWindow", () => new MainWindow(_startupTrace)); + _serviceProvider = MeasureStartup("App.BuildServiceProvider", () => + new ServiceCollection().AddAppServices(_startupTrace).BuildServiceProvider()); + MainWindow mainWindow = MeasureStartup("App.CreateMainWindow", () => new MainWindow( + _serviceProvider.GetRequiredService(), + _startupTrace, + _serviceProvider.GetRequiredService(), + _serviceProvider)); _window = mainWindow; MeasureStartup("App.ActivateWindow", mainWindow.Activate); MeasureStartup("App.StartMonitoringAfterActivation", mainWindow.StartMonitoringAfterActivation); diff --git a/LibreHardwareMonitor.Windows.WinUI/Composition/MainWindowRuntimeFactory.cs b/LibreHardwareMonitor.Windows.WinUI/Composition/MainWindowRuntimeFactory.cs new file mode 100644 index 0000000..ac0e135 --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/Composition/MainWindowRuntimeFactory.cs @@ -0,0 +1,82 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using System; +using LibreHardwareMonitor.Windows.WinUI.Services; +using LibreHardwareMonitor.Windows.WinUI.Services.Tracing; +using LibreHardwareMonitor.Windows.WinUI.ViewModels; +using Microsoft.UI; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using WinRT.Interop; + +namespace LibreHardwareMonitor.Windows.WinUI.Composition; + +/// +/// The runtime/window-tied services that can only be built once the window exists (their dependencies +/// — HWND, AppWindow, dispatcher, XamlRoot, and window-method closures — are not available at the time +/// the container graph is built). +/// +internal sealed record MainWindowRuntime( + AppWindow AppWindow, + WindowChromeManager ChromeManager, + WindowPlacementService PlacementService, + TrayIconService TrayIconService, + DialogService DialogService, + SecondaryWindowCoordinator SecondaryWindows, + SensorColumnMeasurer ColumnMeasurer); + +/// +/// Builds the runtime/window-tied services for a constructed . This is the explicit +/// two-phase boundary between the container-resolved graph and the values that only exist after the +/// window is created. +/// +internal interface IMainWindowRuntimeFactory +{ + MainWindowRuntime Create( + Window window, + MainWindowViewModel viewModel, + Func xamlRootProvider, + Action hideShowMainWindow); +} + +internal sealed class MainWindowRuntimeFactory : IMainWindowRuntimeFactory +{ + private readonly AppSettings _settings; + private readonly IStartupTracer _startupTrace; + + public MainWindowRuntimeFactory(AppSettings settings, IStartupTracer startupTrace) + { + _settings = settings; + _startupTrace = startupTrace; + } + + public MainWindowRuntime Create( + Window window, + MainWindowViewModel viewModel, + Func xamlRootProvider, + Action hideShowMainWindow) + { + IntPtr hwnd = WindowNative.GetWindowHandle(window); + WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd); + AppWindow appWindow = AppWindow.GetFromWindowId(windowId); + appWindow.Title = "Libre Hardware Monitor"; + + WindowChromeManager chromeManager = new(hwnd); + WindowPlacementService placementService = new(appWindow, _settings, hwnd); + TrayIconService trayIconService = new(hwnd, _settings, () => viewModel.TemperatureUnit); + DialogService dialogService = new(xamlRootProvider, viewModel, hwnd); + SecondaryWindowCoordinator secondaryWindows = new(viewModel, hideShowMainWindow); + SensorColumnMeasurer columnMeasurer = new(window.DispatcherQueue, viewModel, _startupTrace); + + return new MainWindowRuntime( + appWindow, + chromeManager, + placementService, + trayIconService, + dialogService, + secondaryWindows, + columnMeasurer); + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/Composition/ServiceCollectionExtensions.cs b/LibreHardwareMonitor.Windows.WinUI/Composition/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..14d9e0b --- /dev/null +++ b/LibreHardwareMonitor.Windows.WinUI/Composition/ServiceCollectionExtensions.cs @@ -0,0 +1,65 @@ +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// Copyright (C) LibreHardwareMonitor and Contributors. + +using LibreHardwareMonitor.Hardware; +using LibreHardwareMonitor.Windows.WinUI.Services; +using LibreHardwareMonitor.Windows.WinUI.Services.Tracing; +using LibreHardwareMonitor.Windows.WinUI.ViewModels; +using Microsoft.Extensions.DependencyInjection; + +namespace LibreHardwareMonitor.Windows.WinUI.Composition; + +/// +/// Single composition root for the application's service graph. Registers configuration, startup +/// tracing, the domain-tier services, and the main view model. Runtime/window-tied services are +/// produced separately by IMainWindowRuntimeFactory once the window exists. +/// +internal static class ServiceCollectionExtensions +{ + public static IServiceCollection AddAppServices(this IServiceCollection services, IStartupTracer startupTracer) + { + // Configuration root and the (already-created) startup tracer instance. + services.AddSingleton(startupTracer); + services.AddSingleton(_ => AppSettings.LoadDefault()); + + // Domain tier. + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService().Computer); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => + { + AppSettings settings = sp.GetRequiredService(); + IHardwareMonitorService hardwareMonitor = sp.GetRequiredService(); + return new RemoteWebServer( + hardwareMonitor.Computer, + hardwareMonitor.SensorReadLock, + settings.GetValue("listenerIp", "?"), + settings.GetValue("listenerPort", 8085), + settings.GetValue("authenticationEnabled", false), + settings.GetValue("authenticationUserName", ""), + settings.GetValue("authenticationPassword", "")); + }); + + // View model (explicit factory so the injected internal constructor is used, never the + // transitional convenience constructor). + services.AddSingleton(sp => new MainWindowViewModel( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService())); + + // Runtime/window-tied service factory. The window itself is constructed by App (kept out of the + // container so the XAML-tied Window is not a container-managed singleton). + services.AddSingleton(); + + return services; + } +} diff --git a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs index 5d31ed0..26f2d47 100644 --- a/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs +++ b/LibreHardwareMonitor.Windows.WinUI/MainWindow.cs @@ -10,6 +10,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; using LibreHardwareMonitor.Hardware; +using LibreHardwareMonitor.Windows.WinUI.Composition; using LibreHardwareMonitor.Windows.WinUI.Controls; using LibreHardwareMonitor.Windows.WinUI.Services; using LibreHardwareMonitor.Windows.WinUI.Services.Tracing; @@ -35,6 +36,7 @@ public sealed partial class MainWindow : Window private readonly WindowPlacementService _placementService; private readonly DispatcherQueueTimer _timer; private readonly IStartupTracer _startupTrace; + private readonly IServiceProvider _serviceProvider; private readonly TrayIconService _trayIconService; private readonly DialogService _dialogService; private readonly SensorColumnMeasurer _columnMeasurer; @@ -51,41 +53,34 @@ public sealed partial class MainWindow : Window private bool _sensorTreeRebuildQueued; private bool _sensorColumnWidthUpdateQueued; - public MainWindow() : this(NoOpStartupTracer.Instance) - { - } - - internal MainWindow(IStartupTracer startupTrace) + internal MainWindow( + MainWindowViewModel viewModel, + IStartupTracer startupTrace, + IMainWindowRuntimeFactory runtimeFactory, + IServiceProvider serviceProvider) { + ViewModel = viewModel; _startupTrace = startupTrace; + _serviceProvider = serviceProvider; _startupTrace.Mark("MainWindow.Constructor.Begin"); - AppSettings settings = MeasureStartup("MainWindow.LoadSettings", AppSettings.LoadDefault); - ViewModel = MeasureStartup("MainWindow.CreateViewModel", () => new MainWindowViewModel(settings, _startupTrace)); - _appWindow = MeasureStartup("MainWindow.GetAppWindow", () => - { - IntPtr hwnd = WindowNative.GetWindowHandle(this); - WindowId windowId = Win32Interop.GetWindowIdFromWindow(hwnd); - AppWindow appWindow = AppWindow.GetFromWindowId(windowId); - appWindow.Title = "Libre Hardware Monitor"; - return appWindow; - }); - IntPtr mainHwnd = WindowNative.GetWindowHandle(this); - _chromeManager = new WindowChromeManager(mainHwnd); - _placementService = new WindowPlacementService(_appWindow, settings, mainHwnd); + MeasureStartup("MainWindow.InitializeComponent", InitializeComponent); + + MainWindowRuntime runtime = MeasureStartup( + "MainWindow.CreateRuntimeServices", + () => runtimeFactory.Create(this, ViewModel, () => Content.XamlRoot, HideShowMainWindow)); + _appWindow = runtime.AppWindow; + _chromeManager = runtime.ChromeManager; + _placementService = runtime.PlacementService; + _trayIconService = runtime.TrayIconService; + _dialogService = runtime.DialogService; + _secondaryWindows = runtime.SecondaryWindows; + _columnMeasurer = runtime.ColumnMeasurer; - _trayIconService = MeasureStartup("MainWindow.CreateTrayIconService", () => new TrayIconService( - WindowNative.GetWindowHandle(this), - settings, - () => ViewModel.TemperatureUnit)); _trayIconService.IsMainIconEnabled = ViewModel.MinimizeToTray; - _dialogService = new DialogService(() => Content.XamlRoot, ViewModel, WindowNative.GetWindowHandle(this)); - _secondaryWindows = new SecondaryWindowCoordinator(ViewModel, HideShowMainWindow); - _columnMeasurer = new SensorColumnMeasurer(DispatcherQueue, ViewModel, _startupTrace); MeasureStartup("MainWindow.ApplySavedDeviceColumnWidth", _columnMeasurer.ApplySavedWidth, () => FormattableString.Invariant($"width={_columnMeasurer.DeviceColumnWidth:F0}")); _columnMeasurer.SettleTriggered += (_, _) => UpdateSensorColumnWidths(); - MeasureStartup("MainWindow.InitializeComponent", InitializeComponent); TryApplyMicaBackdrop(); RootGrid.Loaded += RootGrid_Loaded; RootGrid.LayoutUpdated += RootGrid_LayoutUpdated; @@ -466,8 +461,11 @@ public sealed partial class MainWindow : Window _trayIconService.Dispose(); _secondaryWindows.CloseAll(); _placementService.Save(); - ViewModel.Dispose(); - _startupTrace.Dispose(); + + // The container owns the view model and domain services. Disposing the provider runs + // MainWindowViewModel.Dispose first (reverse construction order) to persist settings while the + // hardware monitor and web server are still alive, then disposes those services and the tracer. + (_serviceProvider as IDisposable)?.Dispose(); } private void AppWindow_Changed(AppWindow sender, AppWindowChangedEventArgs args)