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>
This commit is contained in:
2026-06-02 15:09:12 -05:00
co-authored by Copilot
parent 50a07d5ce9
commit 594429bb7d
4 changed files with 60 additions and 17 deletions
@@ -332,7 +332,7 @@ public class RemoteWebServerTests
private static RemoteWebServer CreateServer(bool authEnabled, string userName, string password) private static RemoteWebServer CreateServer(bool authEnabled, string userName, string password)
{ {
return new RemoteWebServer(() => null, Mock.Of<IComputer>(), new object(), "localhost", 8085, authEnabled, userName, RemoteWebServer.ComputeSHA256(password)); return new RemoteWebServer(Mock.Of<IComputer>(), 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) private static ISensor CreateSensor(string name, SensorType type, Identifier identifier, float? value = null, float? min = null, float? max = null)
@@ -24,6 +24,8 @@ public interface IRemoteWebServer : IDisposable
string UserName { get; set; } string UserName { get; set; }
void SetRootProvider(Func<ViewModels.SensorTreeItemViewModel?> rootProvider);
void SetPassword(string plainPassword); void SetPassword(string plainPassword);
bool Start(); bool Start();
@@ -26,14 +26,14 @@ public sealed class RemoteWebServer : IRemoteWebServer
{ {
private readonly IComputer _computer; private readonly IComputer _computer;
private readonly object _sensorReadLock; private readonly object _sensorReadLock;
private readonly Func<SensorTreeItemViewModel?> _rootProvider; private Func<SensorTreeItemViewModel?> _rootProvider = () => null;
private readonly Version _version = typeof(RemoteWebServer).Assembly.GetName().Version ?? new Version(0, 0); private readonly Version _version = typeof(RemoteWebServer).Assembly.GetName().Version ?? new Version(0, 0);
private CancellationTokenSource? _cts; private CancellationTokenSource? _cts;
private HttpListener? _listener; private HttpListener? _listener;
private Task? _listenerTask; private Task? _listenerTask;
private bool _quit;
public RemoteWebServer( public RemoteWebServer(
Func<SensorTreeItemViewModel?> rootProvider,
IComputer computer, IComputer computer,
object sensorReadLock, object sensorReadLock,
string listenerIp, string listenerIp,
@@ -42,7 +42,6 @@ public sealed class RemoteWebServer : IRemoteWebServer
string userName, string userName,
string passwordHash) string passwordHash)
{ {
_rootProvider = rootProvider;
_computer = computer; _computer = computer;
_sensorReadLock = sensorReadLock; _sensorReadLock = sensorReadLock;
ListenerIp = listenerIp; ListenerIp = listenerIp;
@@ -61,6 +60,15 @@ public sealed class RemoteWebServer : IRemoteWebServer
} }
} }
/// <summary>
/// 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.
/// </summary>
public void SetRootProvider(Func<SensorTreeItemViewModel?> rootProvider)
{
_rootProvider = rootProvider;
}
public bool AuthEnabled { get; set; } public bool AuthEnabled { get; set; }
public bool IsRunning => _listener?.IsListening == true; public bool IsRunning => _listener?.IsListening == true;
@@ -137,9 +145,10 @@ public sealed class RemoteWebServer : IRemoteWebServer
public void Quit() public void Quit()
{ {
if (PlatformNotSupported) if (PlatformNotSupported || _quit)
return; return;
_quit = true;
Stop(); Stop();
try try
{ {
@@ -82,14 +82,15 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
TimeSpan.FromHours(24) TimeSpan.FromHours(24)
]; ];
private readonly HardwareMonitorService _hardwareMonitor; private readonly IHardwareMonitorService _hardwareMonitor;
private readonly DispatcherQueue _dispatcherQueue; private readonly DispatcherQueue _dispatcherQueue;
private readonly Logger _logger; private readonly ILogger _logger;
private readonly PlotTrackingService _plotTracking = new(); private readonly PlotTrackingService _plotTracking;
private readonly SensorSelectionService _sensorSelection; private readonly SensorSelectionService _sensorSelection;
private readonly RemoteWebServer _remoteWebServer; private readonly IRemoteWebServer _remoteWebServer;
private readonly StartupService _startupService = new(); private readonly StartupService _startupService;
private readonly IStartupTracer _startupTrace; private readonly IStartupTracer _startupTrace;
private readonly bool _ownsServices;
private AppThemeMode _themeMode; private AppThemeMode _themeMode;
private int _loggingIntervalIndex; private int _loggingIntervalIndex;
private PlotLocation _plotLocation; private PlotLocation _plotLocation;
@@ -116,28 +117,58 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
private TemperatureUnit _temperatureUnit; private TemperatureUnit _temperatureUnit;
private int _updateIntervalIndex; 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) internal MainWindowViewModel(AppSettings settings, IStartupTracer startupTrace)
{ {
Settings = settings; Settings = settings;
_startupTrace = startupTrace; _startupTrace = startupTrace;
_startupTrace.Mark("MainWindowViewModel.Constructor.Begin"); _startupTrace.Mark("MainWindowViewModel.Constructor.Begin");
_dispatcherQueue = DispatcherQueue.GetForCurrentThread(); _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_hardwareMonitor = new HardwareMonitorService(settings); HardwareMonitorService hardwareMonitor = new(settings);
_logger = new Logger(_hardwareMonitor.Computer); _hardwareMonitor = hardwareMonitor;
_logger = new Logger(hardwareMonitor.Computer);
_sensorSelection = new SensorSelectionService(settings); _sensorSelection = new SensorSelectionService(settings);
_remoteWebServer = new RemoteWebServer( _remoteWebServer = new RemoteWebServer(
() => RootItems.FirstOrDefault(), hardwareMonitor.Computer,
_hardwareMonitor.Computer, hardwareMonitor.SensorReadLock,
_hardwareMonitor.SensorReadLock,
settings.GetValue("listenerIp", "?"), settings.GetValue("listenerIp", "?"),
settings.GetValue("listenerPort", 8085), settings.GetValue("listenerPort", 8085),
settings.GetValue("authenticationEnabled", false), settings.GetValue("authenticationEnabled", false),
settings.GetValue("authenticationUserName", ""), settings.GetValue("authenticationUserName", ""),
settings.GetValue("authenticationPassword", "")); 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; _hardwareMonitor.TreeRebuilt += HardwareMonitor_TreeRebuilt;
_themeMode = ParseThemeMode(settings.GetValue("theme", "auto")); _themeMode = ParseThemeMode(settings.GetValue("theme", "auto"));
@@ -757,7 +788,8 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
Settings.SetValue("authenticationUserName", AuthWebServerUserName); Settings.SetValue("authenticationUserName", AuthWebServerUserName);
Settings.SetValue("authenticationPassword", _remoteWebServer.PasswordHash); Settings.SetValue("authenticationPassword", _remoteWebServer.PasswordHash);
_remoteWebServer.Quit(); _remoteWebServer.Quit();
_hardwareMonitor.Dispose(); if (_ownsServices)
_hardwareMonitor.Dispose();
Save(); Save();
} }