From 1e40ef32e18a8cab0e4b5b7d2b46c101388527c5 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:23:08 -0400 Subject: [PATCH] Fix legend duplication on tab switch: move cleanup from Unloaded to CleanupOnClose WPF fires Unloaded on tab switch, not just destruction. The old handler tore down legend tracking, chart helpers, and event subscriptions on every tab switch, causing the auto-refresh loop to lose track of legend panels and create duplicates. Now Unloaded is a no-op; full cleanup only runs via CleanupOnClose when a tab is actually closed/removed. Co-Authored-By: Claude Opus 4.6 (1M context) --- Dashboard/MainWindow.xaml.cs | 2 ++ Dashboard/ServerTab.xaml.cs | 22 ++++++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Dashboard/MainWindow.xaml.cs b/Dashboard/MainWindow.xaml.cs index 94aa06d..dfb032d 100644 --- a/Dashboard/MainWindow.xaml.cs +++ b/Dashboard/MainWindow.xaml.cs @@ -886,6 +886,7 @@ private void CloseTab_Click(object sender, RoutedEventArgs e) serverTab.AlertAcknowledged -= handler; _alertAcknowledgedHandlers.Remove(tabId); } + serverTab.CleanupOnClose(); } _openTabs.Remove(tabId); _tabBadges.Remove(tabId); @@ -1092,6 +1093,7 @@ private async void RemoveServer_Click(object sender, RoutedEventArgs e) if (_openTabs.TryGetValue(server.Id, out var tabItem)) { + if (tabItem.Content is ServerTab st) st.CleanupOnClose(); _openTabs.Remove(server.Id); ServerTabControl.Items.Remove(tabItem); } diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs index 4f990bf..4ba7629 100644 --- a/Dashboard/ServerTab.xaml.cs +++ b/Dashboard/ServerTab.xaml.cs @@ -377,6 +377,8 @@ private async void StartAutoRefreshLoop(int intervalSeconds) if (cts.Token.IsCancellationRequested) break; if (_isRefreshing) continue; + _isRefreshing = true; + _refreshStartedUtc = DateTime.UtcNow; try { var sw = System.Diagnostics.Stopwatch.StartNew(); @@ -387,7 +389,6 @@ private async void StartAutoRefreshLoop(int intervalSeconds) } catch (OperationCanceledException) when (!cts.Token.IsCancellationRequested) { - // SQL query cancelled or timed out, but our loop CTS is still alive — keep going Logger.Error($"Auto-refresh query cancelled for {_serverConnection.DisplayName}, continuing loop"); } catch (Exception ex) when (ex is not OperationCanceledException) @@ -395,6 +396,10 @@ private async void StartAutoRefreshLoop(int intervalSeconds) Logger.Error($"Auto-refresh error: {ex.Message}", ex); StatusText.Text = "Auto-refresh error"; } + finally + { + _isRefreshing = false; + } } } catch (OperationCanceledException) @@ -405,9 +410,18 @@ private async void StartAutoRefreshLoop(int intervalSeconds) private void ServerTab_Unloaded(object sender, RoutedEventArgs e) { - // Don't cancel auto-refresh on tab switch — WPF fires Unloaded when - // a TabItem is deselected, not just when the control is destroyed. - // The loop is lightweight and should keep ticking in the background. + // WPF fires Unloaded on tab switch, not just destruction. + // Don't tear down state here — the auto-refresh loop and chart + // state must survive tab switches. Cleanup happens when the tab + // is actually removed from the TabControl (via CleanupOnClose). + } + + /// + /// Full cleanup — call when the server tab is permanently removed, not on tab switch. + /// + public void CleanupOnClose() + { + _autoRefreshCts?.Cancel(); _autoRefreshTimer?.Stop(); _autoRefreshTimer = null;