diff --git a/Daqifi.Desktop/MainWindow.xaml b/Daqifi.Desktop/MainWindow.xaml
index 19b1cbc6..060a7afe 100644
--- a/Daqifi.Desktop/MainWindow.xaml
+++ b/Daqifi.Desktop/MainWindow.xaml
@@ -139,9 +139,6 @@
-
-
-
@@ -978,103 +975,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/Daqifi.Desktop/View/Flyouts/DevicesFlyout.xaml b/Daqifi.Desktop/View/Flyouts/DevicesFlyout.xaml
deleted file mode 100644
index fe9ce309..00000000
--- a/Daqifi.Desktop/View/Flyouts/DevicesFlyout.xaml
+++ /dev/null
@@ -1,534 +0,0 @@
-
-
-
-
- #4B8FE2
- #333333
- #2D2D30
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Daqifi.Desktop/View/Flyouts/DevicesFlyout.xaml.cs b/Daqifi.Desktop/View/Flyouts/DevicesFlyout.xaml.cs
deleted file mode 100644
index 17d9bfe5..00000000
--- a/Daqifi.Desktop/View/Flyouts/DevicesFlyout.xaml.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Daqifi.Desktop.View.Flyouts;
-
-///
-/// Interaction logic for DevicesFlyout.xaml
-///
-public partial class DevicesFlyout
-{
- public DevicesFlyout()
- {
- InitializeComponent();
- }
-}
\ No newline at end of file
diff --git a/Daqifi.Desktop/View/Prototype/DevicesPanePrototype.xaml b/Daqifi.Desktop/View/Prototype/DevicesPanePrototype.xaml
new file mode 100644
index 00000000..6c2062df
--- /dev/null
+++ b/Daqifi.Desktop/View/Prototype/DevicesPanePrototype.xaml
@@ -0,0 +1,863 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Daqifi.Desktop/View/Prototype/DevicesPanePrototype.xaml.cs b/Daqifi.Desktop/View/Prototype/DevicesPanePrototype.xaml.cs
new file mode 100644
index 00000000..9c789db7
--- /dev/null
+++ b/Daqifi.Desktop/View/Prototype/DevicesPanePrototype.xaml.cs
@@ -0,0 +1,45 @@
+using System.Windows;
+using Daqifi.Desktop.ViewModels;
+using UserControl = System.Windows.Controls.UserControl;
+
+namespace Daqifi.Desktop.View.Prototype;
+
+///
+/// Host UserControl for the unified Devices pane. Owns the
+/// lifecycle — recreates the VM on
+/// Loaded (since TabControl switches trigger Unloaded → Loaded) so a
+/// returning tab picks up devices connected while it was detached, and
+/// disposes the VM on Unloaded to detach the singleton subscription.
+///
+public partial class DevicesPanePrototype : UserControl
+{
+ /// Creates the pane and wires the Loaded/Unloaded VM lifecycle.
+ public DevicesPanePrototype()
+ {
+ InitializeComponent();
+ Loaded += OnLoaded;
+ Unloaded += OnUnloaded;
+ }
+
+ private void OnLoaded(object sender, RoutedEventArgs e)
+ {
+ // Tab-switches on the host TabControl trigger Unloaded (which disposes
+ // the VM) and then Loaded when the tab comes back. Recreate the VM so
+ // a returning tab gets a fresh Rebuild and picks up devices connected
+ // while the pane was detached.
+ if (DataContext is not DevicesPaneViewModel)
+ {
+ var shell = Window.GetWindow(this)?.DataContext as DaqifiViewModel;
+ DataContext = new DevicesPaneViewModel(shell);
+ }
+ }
+
+ private void OnUnloaded(object sender, RoutedEventArgs e)
+ {
+ if (DataContext is IDisposable disposable)
+ {
+ disposable.Dispose();
+ DataContext = null;
+ }
+ }
+}
diff --git a/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs b/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs
index 835f7f49..2fe123d8 100644
--- a/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs
+++ b/Daqifi.Desktop/ViewModels/DaqifiViewModel.cs
@@ -53,8 +53,6 @@ public partial class DaqifiViewModel : ObservableObject
[ObservableProperty]
private bool _isLoggedDataBusy;
[ObservableProperty]
- private bool _isDeviceSettingsOpen;
- [ObservableProperty]
private bool _isProfileSettingsOpen;
[ObservableProperty]
private bool _isNotificationsOpen;
@@ -1079,8 +1077,34 @@ public void Shutdown()
[RelayCommand]
public async Task UpdateNetworkConfiguration()
{
- await SelectedDevice.UpdateNetworkConfiguration();
- _dialogService.ShowDialog(this, new SuccessDialogViewModel("WiFi settings updated."));
+ // Guard the happy-path below: a device can disappear while the drawer
+ // is open (disconnect, tab switch, etc.), and the underlying
+ // UpdateNetworkConfiguration() throws when the connection is gone.
+ var device = SelectedDevice;
+ if (device == null)
+ {
+ _dialogService.ShowDialog(this,
+ new ErrorDialogViewModel("Select a device before applying WiFi settings."));
+ return;
+ }
+ if (!device.IsConnected)
+ {
+ _dialogService.ShowDialog(this,
+ new ErrorDialogViewModel("Cannot apply WiFi settings — the device is not connected."));
+ return;
+ }
+
+ try
+ {
+ await device.UpdateNetworkConfiguration();
+ _dialogService.ShowDialog(this, new SuccessDialogViewModel("WiFi settings updated."));
+ }
+ catch (Exception ex)
+ {
+ _appLogger.Error(ex, "Failed to update network configuration");
+ _dialogService.ShowDialog(this,
+ new ErrorDialogViewModel($"Failed to apply WiFi settings: {ex.Message}"));
+ }
}
[RelayCommand]
@@ -1103,21 +1127,6 @@ private void OpenLiveGraphSettings()
IsLiveGraphSettingsOpen = true;
}
- [RelayCommand]
- private void OpenDeviceSettings(IStreamingDevice? device)
- {
- if (device == null)
- {
- return;
- }
-
- SelectedDeviceSupportsFirmwareUpdate = device.ConnectionType == Device.ConnectionType.Usb;
-
- CloseFlyouts();
- SelectedDevice = device;
- IsDeviceSettingsOpen = true;
- }
-
[RelayCommand]
private void OpenFirmwareUpdateSettings(IStreamingDevice? device)
{
@@ -2204,7 +2213,6 @@ public async Task ShowMessage(string title, string message,
public void CloseFlyouts()
{
IsProfileSettingsOpen = false;
- IsDeviceSettingsOpen = false;
IsLoggingSessionSettingsOpen = false;
IsLiveGraphSettingsOpen = false;
IsLogSummaryOpen = false;
diff --git a/Daqifi.Desktop/ViewModels/DeviceSettingsViewModel.cs b/Daqifi.Desktop/ViewModels/DeviceSettingsViewModel.cs
deleted file mode 100644
index cf28643e..00000000
--- a/Daqifi.Desktop/ViewModels/DeviceSettingsViewModel.cs
+++ /dev/null
@@ -1,52 +0,0 @@
-using CommunityToolkit.Mvvm.ComponentModel;
-using Daqifi.Desktop.Device;
-
-namespace Daqifi.Desktop.ViewModels;
-
-[ObservableObject]
-public partial class DeviceSettingsViewModel
-{
- [ObservableProperty]
- private IStreamingDevice _selectedDevice;
-
- [ObservableProperty]
- private bool _isLoggingToDevice;
-
- [ObservableProperty]
- private string _logFileName;
-
- public bool CanAccessSdCard => SelectedDevice?.ConnectionType == ConnectionType.Usb;
-
- public string SdCardMessage => GetSdCardMessage();
-
- partial void OnSelectedDeviceChanged(IStreamingDevice value)
- {
- // If switching to WiFi or no device, ensure logging is disabled
- if (value == null || value.ConnectionType != ConnectionType.Usb)
- {
- IsLoggingToDevice = false;
- LogFileName = string.Empty;
- }
- }
-
- partial void OnIsLoggingToDeviceChanging(bool value)
- {
- // Prevent enabling logging if not on USB
- if (value && (SelectedDevice == null || SelectedDevice.ConnectionType != ConnectionType.Usb))
- {
- throw new InvalidOperationException("Cannot enable logging when not connected via USB");
- }
- }
-
- private string GetSdCardMessage()
- {
- if (SelectedDevice == null)
- {
- return string.Empty;
- }
-
- return SelectedDevice.ConnectionType == ConnectionType.Usb
- ? "SD Card logging available"
- : "SD Card logging is not available on WiFi devices";
- }
-}
\ No newline at end of file
diff --git a/Daqifi.Desktop/ViewModels/DeviceTileViewModel.cs b/Daqifi.Desktop/ViewModels/DeviceTileViewModel.cs
new file mode 100644
index 00000000..da63bcc3
--- /dev/null
+++ b/Daqifi.Desktop/ViewModels/DeviceTileViewModel.cs
@@ -0,0 +1,116 @@
+using System.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Daqifi.Desktop.Device;
+using Brush = System.Windows.Media.Brush;
+using Color = System.Windows.Media.Color;
+using ColorConverter = System.Windows.Media.ColorConverter;
+using SolidColorBrush = System.Windows.Media.SolidColorBrush;
+
+namespace Daqifi.Desktop.ViewModels;
+
+///
+/// View-model for a single device tile. All presentation (stripe, labels,
+/// tile colors) is computed from the underlying device's current state so a
+/// device that reconnects or updates its firmware updates in place.
+///
+public sealed class DeviceTileViewModel : ObservableObject, IDisposable
+{
+ private readonly INotifyPropertyChanged? _deviceNotifier;
+
+ /// The underlying streaming device this tile represents.
+ public IStreamingDevice Device { get; }
+
+ /// Primary display name — the device's part number.
+ public string Name => Device.DevicePartNumber;
+
+ /// Serial number as shown on the tile.
+ public string SerialNumber => Device.DeviceSerialNo;
+
+ /// Firmware version as shown on the tile.
+ public string Version => Device.DeviceVersion;
+
+ /// COM port (USB) or IP address (WiFi).
+ public string Identifier => Device.DisplayIdentifier;
+
+ /// "USB" or "WIFI" label for the connection chip.
+ public string ConnectionLabel => Device.ConnectionType == ConnectionType.Usb ? "USB" : "WIFI";
+
+ /// Whether the device is on a USB connection.
+ public bool IsUsb => Device.ConnectionType == ConnectionType.Usb;
+
+ /// Whether the device needs a firmware update.
+ public bool IsFirmwareOutdated => Device.IsFirmwareOutdated;
+
+ /// Whether the device is currently connected.
+ public bool IsConnected => Device.IsConnected;
+
+ ///
+ /// Type-coded stripe color — cyan for USB, purple for WiFi. Connection
+ /// type is a meaningful dimension operators sort by, so it earns a
+ /// dedicated hue.
+ ///
+ public Brush StripeBrush => IsUsb ? UsbAccent : WifiAccent;
+
+ /// Background color for the tile.
+ public Brush TileBackground => SurfaceRaised;
+
+ /// Border color — stripe color when connected, dim otherwise.
+ public Brush TileBorderBrush => IsConnected ? StripeBrush : BorderDim;
+
+ /// Creates a tile bound to the given device.
+ public DeviceTileViewModel(IStreamingDevice device)
+ {
+ Device = device;
+ _deviceNotifier = device as INotifyPropertyChanged;
+ if (_deviceNotifier != null)
+ {
+ _deviceNotifier.PropertyChanged += OnDevicePropertyChanged;
+ }
+ }
+
+ private void OnDevicePropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ switch (e.PropertyName)
+ {
+ case nameof(IStreamingDevice.IsConnected):
+ OnPropertyChanged(nameof(IsConnected));
+ OnPropertyChanged(nameof(TileBorderBrush));
+ break;
+ case nameof(IStreamingDevice.IsFirmwareOutdated):
+ OnPropertyChanged(nameof(IsFirmwareOutdated));
+ break;
+ case nameof(IStreamingDevice.DeviceSerialNo):
+ OnPropertyChanged(nameof(SerialNumber));
+ break;
+ case nameof(IStreamingDevice.DeviceVersion):
+ OnPropertyChanged(nameof(Version));
+ break;
+ case nameof(IStreamingDevice.IpAddress):
+ case nameof(IStreamingDevice.DisplayIdentifier):
+ OnPropertyChanged(nameof(Identifier));
+ break;
+ }
+ }
+
+ /// Detaches the device subscription.
+ public void Dispose()
+ {
+ if (_deviceNotifier != null)
+ {
+ _deviceNotifier.PropertyChanged -= OnDevicePropertyChanged;
+ }
+ }
+
+ private static readonly Brush SurfaceRaised = MakeBrush("#171A20");
+ private static readonly Brush BorderDim = MakeBrush("#2A2F38");
+ private static readonly Brush UsbAccent = MakeBrush("#06B6D4");
+ private static readonly Brush WifiAccent = MakeBrush("#A855F7");
+
+ private static SolidColorBrush MakeBrush(string hex)
+ {
+ var color = (Color)ColorConverter.ConvertFromString(hex)!;
+ var brush = new SolidColorBrush(color);
+ brush.Freeze();
+ return brush;
+ }
+}
diff --git a/Daqifi.Desktop/ViewModels/DevicesPaneViewModel.cs b/Daqifi.Desktop/ViewModels/DevicesPaneViewModel.cs
new file mode 100644
index 00000000..48202484
--- /dev/null
+++ b/Daqifi.Desktop/ViewModels/DevicesPaneViewModel.cs
@@ -0,0 +1,233 @@
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Windows.Threading;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Daqifi.Desktop.Device;
+
+namespace Daqifi.Desktop.ViewModels;
+
+///
+/// Backs the unified Devices pane. Aggregates connected devices into a tile
+/// grid and owns the inline settings drawer state. Delegates side-effecting
+/// commands (connect, disconnect, reboot, firmware, network) to the shell
+/// view-model so the existing service wiring is reused unchanged.
+///
+public partial class DevicesPaneViewModel : ObservableObject, IDisposable
+{
+ private readonly DaqifiViewModel? _shell;
+ private readonly Dispatcher _dispatcher;
+ private bool _disposed;
+
+ /// Device tiles in the order ConnectionManager returns them.
+ public ObservableCollection Devices { get; } = [];
+
+ ///
+ /// The shell view-model, exposed so the drawer can bind directly to shell-owned
+ /// state (firmware path/progress, logging mode, SD format) and commands
+ /// (firmware upload, network apply) without a second DataContext hop.
+ ///
+ public DaqifiViewModel? Shell => _shell;
+
+ [ObservableProperty] private bool _hasConnectedDevice;
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(SelectedDevice))]
+ [NotifyPropertyChangedFor(nameof(SelectedDeviceSupportsFirmwareUpdate))]
+ [NotifyPropertyChangedFor(nameof(FrequencyHz))]
+ private DeviceTileViewModel? _selectedTile;
+
+ [ObservableProperty] private bool _isSettingsOpen;
+
+ /// The underlying device of the tile shown in the settings drawer.
+ public IStreamingDevice? SelectedDevice => SelectedTile?.Device;
+
+ /// True when the selected device is USB-connected (firmware update path).
+ public bool SelectedDeviceSupportsFirmwareUpdate =>
+ SelectedDevice?.ConnectionType == ConnectionType.Usb;
+
+ ///
+ /// Sampling frequency shown on the drawer's FREQUENCY slider.
+ /// Reads straight off the device so the slider reflects the real value,
+ /// but writes go through the shell's guarded SelectedStreamingFrequency
+ /// setter — which blocks changes and shows an error dialog while a
+ /// logging session is active.
+ ///
+ public int FrequencyHz
+ {
+ get => SelectedDevice?.StreamingFrequency ?? 0;
+ set
+ {
+ if (_shell == null || SelectedDevice == null) return;
+ _shell.SelectedStreamingFrequency = value;
+ OnPropertyChanged();
+ }
+ }
+
+ /// Opens the inline settings drawer for a device tile.
+ public IRelayCommand OpenSettingsCommand { get; }
+
+ /// Closes the inline settings drawer.
+ public IRelayCommand CloseSettingsCommand { get; }
+
+ /// Shows the Add-Device dialog via the shell view-model.
+ public IRelayCommand AddDeviceCommand { get; }
+
+ /// Disconnects the currently selected device and closes the drawer.
+ public IRelayCommand DisconnectSelectedCommand { get; }
+
+ /// Reboots the currently selected device.
+ public IRelayCommand RebootSelectedCommand { get; }
+
+ ///
+ /// Writes the chosen logging-mode label ("Stream to App" or "Log to
+ /// Device") to the shell. Parameterized so the XAML can wire both
+ /// segmented-toggle RadioButtons to the same command.
+ ///
+ public IRelayCommand SetLoggingModeCommand { get; }
+
+ /// Creates the view-model bound to the shell view-model.
+ public DevicesPaneViewModel(DaqifiViewModel? shell)
+ {
+ _shell = shell;
+ _dispatcher = Dispatcher.CurrentDispatcher;
+
+ OpenSettingsCommand = new RelayCommand(OpenSettings);
+ CloseSettingsCommand = new RelayCommand(CloseSettings);
+ AddDeviceCommand = new RelayCommand(AddDevice);
+ DisconnectSelectedCommand = new RelayCommand(DisconnectSelected, () => SelectedDevice != null);
+ RebootSelectedCommand = new RelayCommand(RebootSelected, () => SelectedDevice != null);
+ SetLoggingModeCommand = new RelayCommand(SetLoggingMode);
+
+ ConnectionManager.Instance.PropertyChanged += OnConnectionManagerPropertyChanged;
+ Rebuild();
+ }
+
+ partial void OnSelectedTileChanged(DeviceTileViewModel? value)
+ {
+ // Derived-property change notifications are declared via
+ // [NotifyPropertyChangedFor] attributes above; the partial is only
+ // for imperative work the source generator can't do.
+ DisconnectSelectedCommand.NotifyCanExecuteChanged();
+ RebootSelectedCommand.NotifyCanExecuteChanged();
+ }
+
+ private void OnConnectionManagerPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName != nameof(ConnectionManager.ConnectedDevices)) return;
+
+ if (_dispatcher.CheckAccess())
+ {
+ Rebuild();
+ }
+ else
+ {
+ _dispatcher.BeginInvoke((Action)Rebuild);
+ }
+ }
+
+ private void Rebuild()
+ {
+ if (_disposed) return;
+
+ var openSerial = SelectedDevice?.DeviceSerialNo;
+
+ foreach (var tile in Devices) tile.Dispose();
+ Devices.Clear();
+
+ foreach (var device in ConnectionManager.Instance.ConnectedDevices)
+ {
+ Devices.Add(new DeviceTileViewModel(device));
+ }
+
+ HasConnectedDevice = Devices.Count > 0;
+
+ // Preserve drawer state across a rebuild if the device is still there;
+ // otherwise close the drawer so it doesn't point at a removed device.
+ if (IsSettingsOpen && openSerial != null)
+ {
+ var match = Devices.FirstOrDefault(d => d.Device.DeviceSerialNo == openSerial);
+ if (match != null)
+ {
+ SelectedTile = match;
+ }
+ else
+ {
+ CloseSettings();
+ }
+ }
+ }
+
+ private void OpenSettings(DeviceTileViewModel? tile)
+ {
+ if (tile == null) return;
+ SelectedTile = tile;
+ // Keep the shell's SelectedDevice in sync so existing commands
+ // (UploadFirmware, UpdateNetworkConfiguration) operate on the
+ // device the drawer is showing.
+ if (_shell != null)
+ {
+ _shell.SelectedDevice = tile.Device;
+ _shell.SelectedDeviceSupportsFirmwareUpdate =
+ tile.Device.ConnectionType == ConnectionType.Usb;
+ }
+ IsSettingsOpen = true;
+ }
+
+ private void CloseSettings()
+ {
+ IsSettingsOpen = false;
+ SelectedTile = null;
+ }
+
+ private void AddDevice()
+ {
+ if (_shell?.ShowConnectionDialogCommand.CanExecute(null) == true)
+ {
+ _shell.ShowConnectionDialogCommand.Execute(null);
+ }
+ }
+
+ private void DisconnectSelected()
+ {
+ var device = SelectedDevice;
+ if (device == null || _shell == null) return;
+ if (_shell.DisconnectDeviceCommand.CanExecute(device))
+ {
+ _shell.DisconnectDeviceCommand.Execute(device);
+ }
+ CloseSettings();
+ }
+
+ private void SetLoggingMode(string? mode)
+ {
+ // The shell setter takes the label, drives device SwitchMode, and
+ // refuses changes mid-session — we just forward the click.
+ if (string.IsNullOrEmpty(mode) || _shell == null) return;
+ _shell.SelectedLoggingMode = mode;
+ }
+
+ private void RebootSelected()
+ {
+ var device = SelectedDevice;
+ if (device == null || _shell == null) return;
+ if (_shell.RebootDeviceCommand.CanExecute(device))
+ {
+ _shell.RebootDeviceCommand.Execute(device);
+ }
+ }
+
+ /// Detaches the singleton subscription and disposes tiles.
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ ConnectionManager.Instance.PropertyChanged -= OnConnectionManagerPropertyChanged;
+
+ foreach (var tile in Devices) tile.Dispose();
+ Devices.Clear();
+
+ GC.SuppressFinalize(this);
+ }
+}