Invert composition root to DI container
Build the Microsoft.Extensions.DependencyInjection provider in App and resolve the view model and runtime factory from it, replacing the manual new MainWindow(trace) path. MainWindow now takes an injected ctor (view model, tracer, runtime factory, provider); window-tied services are produced by MainWindowRuntimeFactory once the window exists. On real shutdown MainWindow_Closed disposes the provider (which disposes the view model first, then domain services and tracer), while the minimize-to-tray branch returns before any teardown. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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<MainWindowViewModel>(),
|
||||
_startupTrace,
|
||||
_serviceProvider.GetRequiredService<IMainWindowRuntimeFactory>(),
|
||||
_serviceProvider));
|
||||
_window = mainWindow;
|
||||
MeasureStartup("App.ActivateWindow", mainWindow.Activate);
|
||||
MeasureStartup("App.StartMonitoringAfterActivation", mainWindow.StartMonitoringAfterActivation);
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
internal sealed record MainWindowRuntime(
|
||||
AppWindow AppWindow,
|
||||
WindowChromeManager ChromeManager,
|
||||
WindowPlacementService PlacementService,
|
||||
TrayIconService TrayIconService,
|
||||
DialogService DialogService,
|
||||
SecondaryWindowCoordinator SecondaryWindows,
|
||||
SensorColumnMeasurer ColumnMeasurer);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the runtime/window-tied services for a constructed <see cref="Window" />. This is the explicit
|
||||
/// two-phase boundary between the container-resolved graph and the values that only exist after the
|
||||
/// window is created.
|
||||
/// </summary>
|
||||
internal interface IMainWindowRuntimeFactory
|
||||
{
|
||||
MainWindowRuntime Create(
|
||||
Window window,
|
||||
MainWindowViewModel viewModel,
|
||||
Func<XamlRoot?> 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<XamlRoot?> 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>IMainWindowRuntimeFactory</c> once the window exists.
|
||||
/// </summary>
|
||||
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<IHardwareMonitorService, HardwareMonitorService>();
|
||||
services.AddSingleton<IComputer>(sp => sp.GetRequiredService<IHardwareMonitorService>().Computer);
|
||||
services.AddSingleton<ILogger, Logger>();
|
||||
services.AddSingleton<SensorSelectionService>();
|
||||
services.AddSingleton<PlotTrackingService>();
|
||||
services.AddSingleton<StartupService>();
|
||||
services.AddSingleton<IRemoteWebServer>(sp =>
|
||||
{
|
||||
AppSettings settings = sp.GetRequiredService<AppSettings>();
|
||||
IHardwareMonitorService hardwareMonitor = sp.GetRequiredService<IHardwareMonitorService>();
|
||||
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<AppSettings>(),
|
||||
sp.GetRequiredService<IHardwareMonitorService>(),
|
||||
sp.GetRequiredService<ILogger>(),
|
||||
sp.GetRequiredService<SensorSelectionService>(),
|
||||
sp.GetRequiredService<IRemoteWebServer>(),
|
||||
sp.GetRequiredService<PlotTrackingService>(),
|
||||
sp.GetRequiredService<StartupService>(),
|
||||
sp.GetRequiredService<IStartupTracer>()));
|
||||
|
||||
// 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<IMainWindowRuntimeFactory, MainWindowRuntimeFactory>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user