Convert MenuBar to MainWindow.xaml (Phase 5b step 3)

Declare File/View/Options/Help menus and their toggle items in XAML with x:Bind (TwoWay) to ViewModel properties. Radio/indexed submenus (Temperature Unit, Plot Location, Theme, Stroke Thickness, File Rotation, Update Interval, Logging Interval, Sensor Values Time Window) are declared as named empty MenuFlyoutSubItems and populated by PopulateRadioSubMenu in code-behind so dynamic check-state and Tag-based selection are preserved.

Click handlers for non-toggle items (save report, reset, expand/collapse, web server dialogs, about, exit) live on the code-behind. Delete BuildMenuBar, CreateToggleItem, CreateToggleSettingItem, BuildRadioSubMenu, BuildIndexedSubMenu. CreateMenuItem is retained for the still-imperative sensor row context menu.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-06-02 13:28:34 -05:00
co-authored by Copilot
parent fa1b16a8f3
commit c6ef3edeff
2 changed files with 131 additions and 170 deletions
+65 -169
View File
@@ -95,7 +95,7 @@ public sealed partial class MainWindow : Window
RootGrid.LayoutUpdated += RootGrid_LayoutUpdated;
Bind(ContentGrid, UIElement.IsHitTestVisibleProperty, ViewModel, nameof(ViewModel.IsHardwareInteractionEnabled));
MeasureStartup("MainWindow.BuildMenuBar", () => MenuHost.Children.Add(BuildMenuBar()));
MeasureStartup("MainWindow.PopulateMenuSubmenus", PopulateMenuSubmenus);
TreeView? builtTree = null;
Grid sensorPane = MeasureStartup("MainWindow.BuildSensorPane", () =>
@@ -288,147 +288,89 @@ public sealed partial class MainWindow : Window
await _startupTrace.MeasureAsync(phase, action);
}
private MenuBar BuildMenuBar()
private void PopulateMenuSubmenus()
{
MenuBar menuBar = new();
Bind(menuBar, Control.IsEnabledProperty, ViewModel, nameof(ViewModel.IsHardwareInteractionEnabled));
MenuBarItem file = new() { Title = "File" };
file.Items.Add(CreateMenuItem("Save Report...", async (_, _) => await _dialogService.SaveReportAsync()));
file.Items.Add(new MenuFlyoutSeparator());
file.Items.Add(CreateMenuItem("Reset", (_, _) => ViewModel.ResetHardware()));
MenuFlyoutSubItem hardware = new() { Text = "Hardware" };
hardware.Items.Add(CreateToggleItem("Motherboard", nameof(ViewModel.IsMotherboardEnabled)));
hardware.Items.Add(CreateToggleItem("CPU", nameof(ViewModel.IsCpuEnabled)));
hardware.Items.Add(CreateToggleItem("Memory", nameof(ViewModel.IsMemoryEnabled)));
hardware.Items.Add(CreateToggleItem("GPU", nameof(ViewModel.IsGpuEnabled)));
hardware.Items.Add(CreateToggleItem("Power Monitors", nameof(ViewModel.IsPowerMonitorEnabled)));
hardware.Items.Add(CreateToggleItem("Fan Controllers", nameof(ViewModel.IsControllerEnabled)));
hardware.Items.Add(CreateToggleItem("Storage Devices", nameof(ViewModel.IsStorageEnabled)));
hardware.Items.Add(CreateToggleItem("Network", nameof(ViewModel.IsNetworkEnabled)));
hardware.Items.Add(CreateToggleItem("Power Supplies", nameof(ViewModel.IsPsuEnabled)));
hardware.Items.Add(CreateToggleItem("Battery", nameof(ViewModel.IsBatteryEnabled)));
file.Items.Add(hardware);
file.Items.Add(new MenuFlyoutSeparator());
file.Items.Add(CreateMenuItem("Exit", (_, _) => CloseApplication()));
menuBar.Items.Add(file);
MenuBarItem view = new() { Title = "View" };
view.Items.Add(CreateMenuItem("Reset Min/Max", (_, _) => ViewModel.ResetMinMax()));
view.Items.Add(CreateMenuItem("Expand All Nodes", (_, _) =>
{
ViewModel.SetAllExpanded(true);
RebuildSensorTree();
}));
view.Items.Add(CreateMenuItem("Collapse All Nodes", (_, _) =>
{
ViewModel.SetAllExpanded(false);
RebuildSensorTree();
}));
view.Items.Add(CreateMenuItem("Reset Plot", (_, _) => ResetPlot()));
view.Items.Add(new MenuFlyoutSeparator());
view.Items.Add(CreateToggleItem("Show Hidden Sensors", nameof(ViewModel.ShowHiddenSensors)));
view.Items.Add(CreateToggleItem("Show Plot", nameof(ViewModel.ShowPlot)));
view.Items.Add(CreateToggleItem("Gadget", nameof(ViewModel.ShowGadget)));
MenuFlyoutSubItem columns = new() { Text = "Columns" };
columns.Items.Add(CreateToggleItem("Value", nameof(ViewModel.ShowValueColumn)));
columns.Items.Add(CreateToggleItem("Min", nameof(ViewModel.ShowMinColumn)));
columns.Items.Add(CreateToggleItem("Max", nameof(ViewModel.ShowMaxColumn)));
view.Items.Add(columns);
menuBar.Items.Add(view);
MenuBarItem options = new() { Title = "Options" };
options.Items.Add(CreateToggleItem("Start Minimized", nameof(ViewModel.StartMinimized)));
options.Items.Add(CreateToggleItem("Minimize To Tray", nameof(ViewModel.MinimizeToTray)));
options.Items.Add(CreateToggleItem("Minimize On Close", nameof(ViewModel.MinimizeOnClose)));
options.Items.Add(CreateToggleItem("Run On Windows Startup", nameof(ViewModel.RunOnStartup)));
options.Items.Add(new MenuFlyoutSeparator());
options.Items.Add(BuildRadioSubMenu("Temperature Unit", [
PopulateRadioSubMenu(TemperatureUnitMenu, [
("Celsius", TemperatureUnit.Celsius),
("Fahrenheit", TemperatureUnit.Fahrenheit)
], () => ViewModel.TemperatureUnit, value => ViewModel.TemperatureUnit = value));
options.Items.Add(BuildRadioSubMenu("Plot Location", [
], () => ViewModel.TemperatureUnit, v => ViewModel.TemperatureUnit = v);
PopulateRadioSubMenu(PlotLocationMenu, [
("Window", PlotLocation.Window),
("Bottom", PlotLocation.Bottom),
("Right", PlotLocation.Right)
], () => ViewModel.PlotLocation, value => ViewModel.PlotLocation = value));
options.Items.Add(BuildRadioSubMenu("Theme", [
], () => ViewModel.PlotLocation, v => ViewModel.PlotLocation = v);
PopulateRadioSubMenu(ThemeMenu, [
("Auto", AppThemeMode.Auto),
("Light", AppThemeMode.Light),
("Dark", AppThemeMode.Dark),
("Black", AppThemeMode.Black)
], () => ViewModel.ThemeMode, value => ViewModel.ThemeMode = value));
options.Items.Add(BuildIndexedSubMenu("Stroke Thickness", [
], () => ViewModel.ThemeMode, v => ViewModel.ThemeMode = v);
PopulateRadioSubMenu(StrokeThicknessMenu, [
("1pt", 1),
("2pt", 2),
("3pt", 3),
("4pt", 4)
], () => (int)ViewModel.PlotStrokeThickness, value => ViewModel.PlotStrokeThickness = value));
options.Items.Add(new MenuFlyoutSeparator());
options.Items.Add(CreateToggleItem("Log Sensors", nameof(ViewModel.LogSensors)));
options.Items.Add(CreateToggleItem("Force Drive Wakeup", nameof(ViewModel.ForceDriveWakeup)));
options.Items.Add(CreateToggleItem("Throttle ATA Storage", nameof(ViewModel.ThrottleAtaUpdate)));
options.Items.Add(BuildRadioSubMenu("File Rotation Method", [
], () => (int)ViewModel.PlotStrokeThickness, v => ViewModel.PlotStrokeThickness = v);
PopulateRadioSubMenu(FileRotationMenu, [
("Per Session", LoggerFileRotation.PerSession),
("Daily", LoggerFileRotation.Daily)
], () => ViewModel.FileRotationMethod, value => ViewModel.FileRotationMethod = value));
options.Items.Add(BuildIndexedSubMenu("Update Interval", [
("250ms", 0),
("500ms", 1),
("1s", 2),
("2s", 3),
("5s", 4),
("10s", 5)
], () => ViewModel.UpdateIntervalIndex, value => ViewModel.UpdateIntervalIndex = value));
options.Items.Add(BuildIndexedSubMenu("Logging Interval", [
("1s", 0),
("2s", 1),
("5s", 2),
("10s", 3),
("30s", 4),
("1min", 5),
("2min", 6),
("5min", 7),
("10min", 8),
("30min", 9),
("1h", 10),
("2h", 11),
("6h", 12)
], () => ViewModel.LoggingIntervalIndex, value => ViewModel.LoggingIntervalIndex = value));
options.Items.Add(BuildIndexedSubMenu("Sensor Values Time Window", [
("30s", 0),
("1min", 1),
("2min", 2),
("5min", 3),
("10min", 4),
("30min", 5),
("1h", 6),
("2h", 7),
("6h", 8),
("12h", 9),
("24h", 10)
], () => ViewModel.SensorValuesTimeWindowIndex, value => ViewModel.SensorValuesTimeWindowIndex = value));
options.Items.Add(new MenuFlyoutSeparator());
MenuFlyoutSubItem webServer = new() { Text = "Remote Web Server" };
ToggleMenuFlyoutItem runWebServer = CreateToggleItem("Run", nameof(ViewModel.RunWebServer));
runWebServer.IsEnabled = !ViewModel.IsWebServerUnavailable;
webServer.Items.Add(runWebServer);
MenuFlyoutItem interfacePort = CreateMenuItem("Interface / Port", async (_, _) => await _dialogService.ShowWebServerSettingsAsync());
interfacePort.IsEnabled = !ViewModel.IsWebServerUnavailable;
webServer.Items.Add(interfacePort);
MenuFlyoutItem authentication = CreateMenuItem("Authentication", async (_, _) => await _dialogService.ShowWebServerAuthenticationAsync());
authentication.IsEnabled = !ViewModel.IsWebServerUnavailable;
webServer.Items.Add(authentication);
options.Items.Add(webServer);
menuBar.Items.Add(options);
], () => ViewModel.FileRotationMethod, v => ViewModel.FileRotationMethod = v);
PopulateRadioSubMenu(UpdateIntervalMenu, [
("250ms", 0), ("500ms", 1), ("1s", 2), ("2s", 3), ("5s", 4), ("10s", 5)
], () => ViewModel.UpdateIntervalIndex, v => ViewModel.UpdateIntervalIndex = v);
PopulateRadioSubMenu(LoggingIntervalMenu, [
("1s", 0), ("2s", 1), ("5s", 2), ("10s", 3), ("30s", 4), ("1min", 5),
("2min", 6), ("5min", 7), ("10min", 8), ("30min", 9), ("1h", 10), ("2h", 11), ("6h", 12)
], () => ViewModel.LoggingIntervalIndex, v => ViewModel.LoggingIntervalIndex = v);
PopulateRadioSubMenu(SensorTimeWindowMenu, [
("30s", 0), ("1min", 1), ("2min", 2), ("5min", 3), ("10min", 4),
("30min", 5), ("1h", 6), ("2h", 7), ("6h", 8), ("12h", 9), ("24h", 10)
], () => ViewModel.SensorValuesTimeWindowIndex, v => ViewModel.SensorValuesTimeWindowIndex = v);
MenuBarItem help = new() { Title = "Help" };
help.Items.Add(CreateMenuItem("About", async (_, _) => await _dialogService.ShowAboutAsync()));
menuBar.Items.Add(help);
return menuBar;
RunWebServerItem.IsEnabled = !ViewModel.IsWebServerUnavailable;
WebServerInterfaceItem.IsEnabled = !ViewModel.IsWebServerUnavailable;
WebServerAuthItem.IsEnabled = !ViewModel.IsWebServerUnavailable;
}
private static void PopulateRadioSubMenu<T>(MenuFlyoutSubItem subItem, (string Label, T Value)[] items, Func<T> getter, Action<T> setter)
{
foreach ((string label, T value) in items)
{
ToggleMenuFlyoutItem item = new()
{
Text = label,
IsChecked = Equals(getter(), value),
Tag = value
};
item.Click += (_, _) =>
{
setter(value);
foreach (ToggleMenuFlyoutItem sibling in subItem.Items.OfType<ToggleMenuFlyoutItem>())
sibling.IsChecked = Equals(sibling.Tag, value);
};
subItem.Items.Add(item);
}
}
private async void OnSaveReport(object sender, RoutedEventArgs e) => await _dialogService.SaveReportAsync();
private void OnResetHardware(object sender, RoutedEventArgs e) => ViewModel.ResetHardware();
private void OnExit(object sender, RoutedEventArgs e) => CloseApplication();
private void OnResetMinMax(object sender, RoutedEventArgs e) => ViewModel.ResetMinMax();
private void OnExpandAll(object sender, RoutedEventArgs e)
{
ViewModel.SetAllExpanded(true);
RebuildSensorTree();
}
private void OnCollapseAll(object sender, RoutedEventArgs e)
{
ViewModel.SetAllExpanded(false);
RebuildSensorTree();
}
private void OnResetPlot(object sender, RoutedEventArgs e) => ResetPlot();
private async void OnWebServerSettings(object sender, RoutedEventArgs e) => await _dialogService.ShowWebServerSettingsAsync();
private async void OnWebServerAuthentication(object sender, RoutedEventArgs e) => await _dialogService.ShowWebServerAuthenticationAsync();
private async void OnAbout(object sender, RoutedEventArgs e) => await _dialogService.ShowAboutAsync();
private Grid BuildSensorPane(out TreeView tree)
{
Grid pane = new()
@@ -993,34 +935,6 @@ public sealed partial class MainWindow : Window
return textBlock;
}
private static MenuFlyoutSubItem BuildIndexedSubMenu(string text, (string Label, int Value)[] items, Func<int> getter, Action<int> setter)
{
return BuildRadioSubMenu(text, items, getter, setter);
}
private static MenuFlyoutSubItem BuildRadioSubMenu<T>(string text, (string Label, T Value)[] items, Func<T> getter, Action<T> setter)
{
MenuFlyoutSubItem subItem = new() { Text = text };
foreach ((string label, T value) in items)
{
ToggleMenuFlyoutItem item = new()
{
Text = label,
IsChecked = Equals(getter(), value)
};
item.Click += (_, _) =>
{
setter(value);
foreach (ToggleMenuFlyoutItem sibling in subItem.Items.OfType<ToggleMenuFlyoutItem>())
sibling.IsChecked = Equals(sibling.Tag, value);
};
item.Tag = value;
subItem.Items.Add(item);
}
return subItem;
}
private static MenuFlyoutItem CreateMenuItem(string text, RoutedEventHandler handler)
{
MenuFlyoutItem item = new() { Text = text };
@@ -1028,24 +942,6 @@ public sealed partial class MainWindow : Window
return item;
}
private ToggleMenuFlyoutItem CreateToggleItem(string text, string bindingPath)
{
ToggleMenuFlyoutItem item = new() { Text = text };
Bind(item, ToggleMenuFlyoutItem.IsCheckedProperty, ViewModel, bindingPath, BindingMode.TwoWay);
return item;
}
private static ToggleMenuFlyoutItem CreateToggleSettingItem(string text, Func<bool> getter, Action<bool> setter)
{
ToggleMenuFlyoutItem item = new()
{
Text = text,
IsChecked = getter()
};
item.Click += (_, _) => setter(item.IsChecked);
return item;
}
private static void Bind(DependencyObject target, DependencyProperty property, object source, string path, BindingMode mode = BindingMode.OneWay)
{
BindingOperations.SetBinding(target, property, new Binding
@@ -12,7 +12,72 @@
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid x:Name="MenuHost" Grid.Row="0" />
<MenuBar x:Name="AppMenuBar"
Grid.Row="0"
IsEnabled="{x:Bind ViewModel.IsHardwareInteractionEnabled, Mode=OneWay}">
<MenuBarItem Title="File">
<MenuFlyoutItem Text="Save Report..." Click="OnSaveReport" />
<MenuFlyoutSeparator />
<MenuFlyoutItem Text="Reset" Click="OnResetHardware" />
<MenuFlyoutSubItem Text="Hardware">
<ToggleMenuFlyoutItem Text="Motherboard" IsChecked="{x:Bind ViewModel.IsMotherboardEnabled, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="CPU" IsChecked="{x:Bind ViewModel.IsCpuEnabled, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Memory" IsChecked="{x:Bind ViewModel.IsMemoryEnabled, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="GPU" IsChecked="{x:Bind ViewModel.IsGpuEnabled, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Power Monitors" IsChecked="{x:Bind ViewModel.IsPowerMonitorEnabled, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Fan Controllers" IsChecked="{x:Bind ViewModel.IsControllerEnabled, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Storage Devices" IsChecked="{x:Bind ViewModel.IsStorageEnabled, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Network" IsChecked="{x:Bind ViewModel.IsNetworkEnabled, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Power Supplies" IsChecked="{x:Bind ViewModel.IsPsuEnabled, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Battery" IsChecked="{x:Bind ViewModel.IsBatteryEnabled, Mode=TwoWay}" />
</MenuFlyoutSubItem>
<MenuFlyoutSeparator />
<MenuFlyoutItem Text="Exit" Click="OnExit" />
</MenuBarItem>
<MenuBarItem Title="View">
<MenuFlyoutItem Text="Reset Min/Max" Click="OnResetMinMax" />
<MenuFlyoutItem Text="Expand All Nodes" Click="OnExpandAll" />
<MenuFlyoutItem Text="Collapse All Nodes" Click="OnCollapseAll" />
<MenuFlyoutItem Text="Reset Plot" Click="OnResetPlot" />
<MenuFlyoutSeparator />
<ToggleMenuFlyoutItem Text="Show Hidden Sensors" IsChecked="{x:Bind ViewModel.ShowHiddenSensors, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Show Plot" IsChecked="{x:Bind ViewModel.ShowPlot, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Gadget" IsChecked="{x:Bind ViewModel.ShowGadget, Mode=TwoWay}" />
<MenuFlyoutSubItem Text="Columns">
<ToggleMenuFlyoutItem Text="Value" IsChecked="{x:Bind ViewModel.ShowValueColumn, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Min" IsChecked="{x:Bind ViewModel.ShowMinColumn, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Max" IsChecked="{x:Bind ViewModel.ShowMaxColumn, Mode=TwoWay}" />
</MenuFlyoutSubItem>
</MenuBarItem>
<MenuBarItem Title="Options">
<ToggleMenuFlyoutItem Text="Start Minimized" IsChecked="{x:Bind ViewModel.StartMinimized, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Minimize To Tray" IsChecked="{x:Bind ViewModel.MinimizeToTray, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Minimize On Close" IsChecked="{x:Bind ViewModel.MinimizeOnClose, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Run On Windows Startup" IsChecked="{x:Bind ViewModel.RunOnStartup, Mode=TwoWay}" />
<MenuFlyoutSeparator />
<MenuFlyoutSubItem x:Name="TemperatureUnitMenu" Text="Temperature Unit" />
<MenuFlyoutSubItem x:Name="PlotLocationMenu" Text="Plot Location" />
<MenuFlyoutSubItem x:Name="ThemeMenu" Text="Theme" />
<MenuFlyoutSubItem x:Name="StrokeThicknessMenu" Text="Stroke Thickness" />
<MenuFlyoutSeparator />
<ToggleMenuFlyoutItem Text="Log Sensors" IsChecked="{x:Bind ViewModel.LogSensors, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Force Drive Wakeup" IsChecked="{x:Bind ViewModel.ForceDriveWakeup, Mode=TwoWay}" />
<ToggleMenuFlyoutItem Text="Throttle ATA Storage" IsChecked="{x:Bind ViewModel.ThrottleAtaUpdate, Mode=TwoWay}" />
<MenuFlyoutSubItem x:Name="FileRotationMenu" Text="File Rotation Method" />
<MenuFlyoutSubItem x:Name="UpdateIntervalMenu" Text="Update Interval" />
<MenuFlyoutSubItem x:Name="LoggingIntervalMenu" Text="Logging Interval" />
<MenuFlyoutSubItem x:Name="SensorTimeWindowMenu" Text="Sensor Values Time Window" />
<MenuFlyoutSeparator />
<MenuFlyoutSubItem Text="Remote Web Server">
<ToggleMenuFlyoutItem x:Name="RunWebServerItem" Text="Run" IsChecked="{x:Bind ViewModel.RunWebServer, Mode=TwoWay}" />
<MenuFlyoutItem x:Name="WebServerInterfaceItem" Text="Interface / Port" Click="OnWebServerSettings" />
<MenuFlyoutItem x:Name="WebServerAuthItem" Text="Authentication" Click="OnWebServerAuthentication" />
</MenuFlyoutSubItem>
</MenuBarItem>
<MenuBarItem Title="Help">
<MenuFlyoutItem Text="About" Click="OnAbout" />
</MenuBarItem>
</MenuBar>
<Grid x:Name="ContentGrid" Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />