From 594429bb7d89ec7dc6ff82d92a1f3bfcdfe25cac Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Tue, 2 Jun 2026 15:09:12 -0500 Subject: [PATCH] Add fully-injected MainWindowViewModel constructor Introduce an injected constructor taking IHardwareMonitorService, ILogger, IRemoteWebServer (+ the concrete selection/plot/startup services) and IStartupTracer, with shared settings initialization. A transitional 2-arg constructor still builds the legacy graph for MainWindow until it is DI-resolved; an _ownsServices flag keeps disposal correct across both paths. Break the VM<->RemoteWebServer construction cycle by moving the root provider to RemoteWebServer.SetRootProvider (removing it from the constructor), and make RemoteWebServer.Quit idempotent. Update RemoteWebServer tests for the new ctor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Services/RemoteWebServerTests.cs | 2 +- .../Services/IRemoteWebServer.cs | 2 + .../Services/RemoteWebServer.cs | 17 ++++-- .../ViewModels/MainWindowViewModel.cs | 56 +++++++++++++++---- 4 files changed, 60 insertions(+), 17 deletions(-) diff --git a/LibreHardwareMonitor.Windows.WinUI.Tests/Services/RemoteWebServerTests.cs b/LibreHardwareMonitor.Windows.WinUI.Tests/Services/RemoteWebServerTests.cs index 938acfa..9d8421b 100644 --- a/LibreHardwareMonitor.Windows.WinUI.Tests/Services/RemoteWebServerTests.cs +++ b/LibreHardwareMonitor.Windows.WinUI.Tests/Services/RemoteWebServerTests.cs @@ -332,7 +332,7 @@ public class RemoteWebServerTests private static RemoteWebServer CreateServer(bool authEnabled, string userName, string password) { - return new RemoteWebServer(() => null, Mock.Of(), new object(), "localhost", 8085, authEnabled, userName, RemoteWebServer.ComputeSHA256(password)); + return new RemoteWebServer(Mock.Of(), new object(), "localhost", 8085, authEnabled, userName, RemoteWebServer.ComputeSHA256(password)); } private static ISensor CreateSensor(string name, SensorType type, Identifier identifier, float? value = null, float? min = null, float? max = null) diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/IRemoteWebServer.cs b/LibreHardwareMonitor.Windows.WinUI/Services/IRemoteWebServer.cs index 8c72254..f42853c 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Services/IRemoteWebServer.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Services/IRemoteWebServer.cs @@ -24,6 +24,8 @@ public interface IRemoteWebServer : IDisposable string UserName { get; set; } + void SetRootProvider(Func rootProvider); + void SetPassword(string plainPassword); bool Start(); diff --git a/LibreHardwareMonitor.Windows.WinUI/Services/RemoteWebServer.cs b/LibreHardwareMonitor.Windows.WinUI/Services/RemoteWebServer.cs index 49e8a86..5be9906 100644 --- a/LibreHardwareMonitor.Windows.WinUI/Services/RemoteWebServer.cs +++ b/LibreHardwareMonitor.Windows.WinUI/Services/RemoteWebServer.cs @@ -26,14 +26,14 @@ public sealed class RemoteWebServer : IRemoteWebServer { private readonly IComputer _computer; private readonly object _sensorReadLock; - private readonly Func _rootProvider; + private Func _rootProvider = () => null; private readonly Version _version = typeof(RemoteWebServer).Assembly.GetName().Version ?? new Version(0, 0); private CancellationTokenSource? _cts; private HttpListener? _listener; private Task? _listenerTask; + private bool _quit; public RemoteWebServer( - Func rootProvider, IComputer computer, object sensorReadLock, string listenerIp, @@ -42,7 +42,6 @@ public sealed class RemoteWebServer : IRemoteWebServer string userName, string passwordHash) { - _rootProvider = rootProvider; _computer = computer; _sensorReadLock = sensorReadLock; ListenerIp = listenerIp; @@ -61,6 +60,15 @@ public sealed class RemoteWebServer : IRemoteWebServer } } + /// + /// Sets the accessor used to read the current sensor tree root. Supplied after construction so the + /// server can be created by the container without a constructor dependency on the view model. + /// + public void SetRootProvider(Func rootProvider) + { + _rootProvider = rootProvider; + } + public bool AuthEnabled { get; set; } public bool IsRunning => _listener?.IsListening == true; @@ -137,9 +145,10 @@ public sealed class RemoteWebServer : IRemoteWebServer public void Quit() { - if (PlatformNotSupported) + if (PlatformNotSupported || _quit) return; + _quit = true; Stop(); try { diff --git a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs index 1ac672a..a67bafa 100644 --- a/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs +++ b/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs @@ -82,14 +82,15 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable TimeSpan.FromHours(24) ]; - private readonly HardwareMonitorService _hardwareMonitor; + private readonly IHardwareMonitorService _hardwareMonitor; private readonly DispatcherQueue _dispatcherQueue; - private readonly Logger _logger; - private readonly PlotTrackingService _plotTracking = new(); + private readonly ILogger _logger; + private readonly PlotTrackingService _plotTracking; private readonly SensorSelectionService _sensorSelection; - private readonly RemoteWebServer _remoteWebServer; - private readonly StartupService _startupService = new(); + private readonly IRemoteWebServer _remoteWebServer; + private readonly StartupService _startupService; private readonly IStartupTracer _startupTrace; + private readonly bool _ownsServices; private AppThemeMode _themeMode; private int _loggingIntervalIndex; private PlotLocation _plotLocation; @@ -116,28 +117,58 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable private TemperatureUnit _temperatureUnit; private int _updateIntervalIndex; - public MainWindowViewModel(AppSettings settings) : this(settings, NoOpStartupTracer.Instance) + internal MainWindowViewModel( + AppSettings settings, + IHardwareMonitorService hardwareMonitor, + ILogger logger, + SensorSelectionService sensorSelection, + IRemoteWebServer remoteWebServer, + PlotTrackingService plotTracking, + StartupService startupService, + IStartupTracer startupTrace) { + Settings = settings; + _startupTrace = startupTrace; + _startupTrace.Mark("MainWindowViewModel.Constructor.Begin"); + _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + _hardwareMonitor = hardwareMonitor; + _logger = logger; + _sensorSelection = sensorSelection; + _remoteWebServer = remoteWebServer; + _plotTracking = plotTracking; + _startupService = startupService; + InitializeFromSettings(settings); } + // Transitional constructor that builds the domain graph the legacy way for callers that do not yet + // resolve the view model from the container. Removed once MainWindow is DI-resolved (di-app-invert). internal MainWindowViewModel(AppSettings settings, IStartupTracer startupTrace) { Settings = settings; _startupTrace = startupTrace; _startupTrace.Mark("MainWindowViewModel.Constructor.Begin"); _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - _hardwareMonitor = new HardwareMonitorService(settings); - _logger = new Logger(_hardwareMonitor.Computer); + HardwareMonitorService hardwareMonitor = new(settings); + _hardwareMonitor = hardwareMonitor; + _logger = new Logger(hardwareMonitor.Computer); _sensorSelection = new SensorSelectionService(settings); _remoteWebServer = new RemoteWebServer( - () => RootItems.FirstOrDefault(), - _hardwareMonitor.Computer, - _hardwareMonitor.SensorReadLock, + hardwareMonitor.Computer, + hardwareMonitor.SensorReadLock, settings.GetValue("listenerIp", "?"), settings.GetValue("listenerPort", 8085), settings.GetValue("authenticationEnabled", false), settings.GetValue("authenticationUserName", ""), settings.GetValue("authenticationPassword", "")); + _plotTracking = new PlotTrackingService(); + _startupService = new StartupService(); + _ownsServices = true; + InitializeFromSettings(settings); + } + + private void InitializeFromSettings(AppSettings settings) + { + _remoteWebServer.SetRootProvider(() => RootItems.FirstOrDefault()); _hardwareMonitor.TreeRebuilt += HardwareMonitor_TreeRebuilt; _themeMode = ParseThemeMode(settings.GetValue("theme", "auto")); @@ -757,7 +788,8 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable Settings.SetValue("authenticationUserName", AuthWebServerUserName); Settings.SetValue("authenticationPassword", _remoteWebServer.PasswordHash); _remoteWebServer.Quit(); - _hardwareMonitor.Dispose(); + if (_ownsServices) + _hardwareMonitor.Dispose(); Save(); }