From 5e31f6f67e04049c362f5afe48ad822a165c55aa Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Wed, 6 May 2026 05:29:14 -0400 Subject: [PATCH] =?UTF-8?q?Fix=20#916=20=E2=80=94=20chart=20tooltips=20bre?= =?UTF-8?q?ak=20after=20tab=20switch=20(real=20root=20cause)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #919 / #921 / #922 addressed the WPF Popup wedge but missed the underlying bug: MemoryContent, ResourceMetricsContent, and QueryPerformanceContent all called DisposeChartHelpers() from their per-control Unloaded event handler. WPF fires Unloaded on every TabControl tab switch, not just on destruction, so switching away from the Memory tab unsubscribed every chart's MouseMove handlers — which were never re-registered when the user came back. The popup-wedge fixes were running inside helpers that had already torn themselves down. ServerTab_Unloaded had the same comment warning future maintainers, but the inner UserControls didn't follow that rule. Changes: - MemoryContent / ResourceMetricsContent / QueryPerformanceContent: drop DisposeChartHelpers() (and ThemeManager unsubscribe) from the tab-switch Unloaded handler. Move the ThemeManager unsubscribe into DisposeChartHelpers() so it runs only on real cleanup. - SystemEventsContent: same pattern — add a DisposeChartHelpers() method that disposes the 19 hover helpers, unsubscribes filter-popup events, and unsubscribes ThemeManager. Empty out OnUnloaded. - ServerTab.CleanupOnClose: add SystemEventsContent.DisposeChartHelpers() to the cleanup chain (its hovers leaked on tab close before this). Final disposal still happens correctly via ServerTab.CleanupOnClose, which only fires when a server tab is actually removed. Lite is unaffected — its Unloaded handler never disposed hovers. Co-Authored-By: Claude Opus 4.7 (1M context) --- Dashboard/Controls/MemoryContent.xaml.cs | 10 +++--- .../Controls/QueryPerformanceContent.xaml.cs | 7 ++-- .../Controls/ResourceMetricsContent.xaml.cs | 10 +++--- .../Controls/SystemEventsContent.xaml.cs | 36 ++++++++++++++----- Dashboard/ServerTab.xaml.cs | 1 + 5 files changed, 44 insertions(+), 20 deletions(-) diff --git a/Dashboard/Controls/MemoryContent.xaml.cs b/Dashboard/Controls/MemoryContent.xaml.cs index 1ce108c..691cbee 100644 --- a/Dashboard/Controls/MemoryContent.xaml.cs +++ b/Dashboard/Controls/MemoryContent.xaml.cs @@ -106,11 +106,10 @@ public MemoryContent() SetupChartContextMenus(); Loaded += OnLoaded; Helpers.ThemeManager.ThemeChanged += OnThemeChanged; - Unloaded += (_, _) => - { - Helpers.ThemeManager.ThemeChanged -= OnThemeChanged; - DisposeChartHelpers(); - }; + /* WPF fires Unloaded on every TabControl tab switch, not just on destruction. + Tearing down chart hover helpers here unsubscribes their MouseMove handlers + and they are never re-registered when the user returns — this is the + root cause of #916. Final disposal happens via ServerTab.CleanupOnClose. */ // Apply dark theme immediately so charts don't flash white before data loads TabHelpers.ApplyThemeToChart(MemoryStatsOverviewChart); @@ -136,6 +135,7 @@ public void DisposeChartHelpers() _memoryClerksHover?.Dispose(); _planCacheHover?.Dispose(); _memoryPressureEventsHover?.Dispose(); + Helpers.ThemeManager.ThemeChanged -= OnThemeChanged; } private void OnLoaded(object sender, RoutedEventArgs e) diff --git a/Dashboard/Controls/QueryPerformanceContent.xaml.cs b/Dashboard/Controls/QueryPerformanceContent.xaml.cs index 3d998fd..e500bf8 100644 --- a/Dashboard/Controls/QueryPerformanceContent.xaml.cs +++ b/Dashboard/Controls/QueryPerformanceContent.xaml.cs @@ -241,8 +241,10 @@ private void OnUnloaded(object sender, RoutedEventArgs e) _qsRegressionsUnfilteredData = null; _lrqPatternsUnfilteredData = null; - DisposeChartHelpers(); - Helpers.ThemeManager.ThemeChanged -= OnThemeChanged; + /* WPF fires Unloaded on every TabControl tab switch, not just on destruction. + Tearing down chart hover helpers or unsubscribing ThemeManager here breaks + tooltips and theme refresh after a tab switch (#916). Final cleanup happens + via ServerTab.CleanupOnClose → DisposeChartHelpers. */ } public void DisposeChartHelpers() @@ -251,6 +253,7 @@ public void DisposeChartHelpers() _procDurationHover?.Dispose(); _qsDurationHover?.Dispose(); _execTrendsHover?.Dispose(); + Helpers.ThemeManager.ThemeChanged -= OnThemeChanged; } private void OnThemeChanged(string _) diff --git a/Dashboard/Controls/ResourceMetricsContent.xaml.cs b/Dashboard/Controls/ResourceMetricsContent.xaml.cs index 3d4c3af..45dc3ca 100644 --- a/Dashboard/Controls/ResourceMetricsContent.xaml.cs +++ b/Dashboard/Controls/ResourceMetricsContent.xaml.cs @@ -130,11 +130,10 @@ public ResourceMetricsContent() SetupChartContextMenus(); Loaded += OnLoaded; Helpers.ThemeManager.ThemeChanged += OnThemeChanged; - Unloaded += (_, _) => - { - Helpers.ThemeManager.ThemeChanged -= OnThemeChanged; - DisposeChartHelpers(); - }; + /* WPF fires Unloaded on every TabControl tab switch, not just on destruction. + Tearing down chart hover helpers here unsubscribes their MouseMove handlers + and they are never re-registered when the user returns — this is the + root cause of #916. Final disposal happens via ServerTab.CleanupOnClose. */ // Apply dark theme immediately so charts don't flash white before data loads TabHelpers.ApplyThemeToChart(LatchStatsChart); @@ -175,6 +174,7 @@ public void DisposeChartHelpers() _waitStatsHover?.Dispose(); _tempdbStatsHover?.Dispose(); _tempDbLatencyHover?.Dispose(); + Helpers.ThemeManager.ThemeChanged -= OnThemeChanged; } private void OnLoaded(object sender, RoutedEventArgs e) diff --git a/Dashboard/Controls/SystemEventsContent.xaml.cs b/Dashboard/Controls/SystemEventsContent.xaml.cs index 726f3d2..2ddb8af 100644 --- a/Dashboard/Controls/SystemEventsContent.xaml.cs +++ b/Dashboard/Controls/SystemEventsContent.xaml.cs @@ -175,20 +175,40 @@ public SystemEventsContent() private void OnUnloaded(object sender, RoutedEventArgs e) { - /* Unsubscribe from filter popup events to prevent memory leaks */ + /* WPF fires Unloaded on every TabControl tab switch, not just on destruction. + Unsubscribing ThemeManager or filter-popup events here breaks them on + return to the tab (#916 family). Final cleanup happens via + ServerTab.CleanupOnClose → DisposeChartHelpers. */ + } + + public void DisposeChartHelpers() + { + _badPagesHover?.Dispose(); + _dumpRequestsHover?.Dispose(); + _accessViolationsHover?.Dispose(); + _writeAccessViolationsHover?.Dispose(); + _nonYieldingTasksHover?.Dispose(); + _latchWarningsHover?.Dispose(); + _sickSpinlocksHover?.Dispose(); + _cpuComparisonHover?.Dispose(); + _severeErrorsHover?.Dispose(); + _ioIssuesHover?.Dispose(); + _longestPendingIoHover?.Dispose(); + _schedulerIssuesHover?.Dispose(); + _memoryConditionsHover?.Dispose(); + _cpuTasksHover?.Dispose(); + _memoryBrokerHover?.Dispose(); + _memoryBrokerRatioHover?.Dispose(); + _memoryNodeOomHover?.Dispose(); + _memoryNodeOomUtilHover?.Dispose(); + _memoryNodeOomMemoryHover?.Dispose(); + if (_filterPopupContent != null) { _filterPopupContent.FilterApplied -= FilterPopup_FilterApplied; _filterPopupContent.FilterCleared -= FilterPopup_FilterCleared; } - /* Clear large data collections to free memory */ - _systemHealthUnfilteredData = null; - _severeErrorsUnfilteredData = null; - _ioIssuesUnfilteredData = null; - _memoryBrokerUnfilteredData = null; - _memoryNodeOOMUnfilteredData = null; - Helpers.ThemeManager.ThemeChanged -= OnThemeChanged; } diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs index 335954b..3c6974f 100644 --- a/Dashboard/ServerTab.xaml.cs +++ b/Dashboard/ServerTab.xaml.cs @@ -275,6 +275,7 @@ public void DisposeChartHelpers() MemoryTab.DisposeChartHelpers(); ResourceMetricsContent.DisposeChartHelpers(); PerformanceTab.DisposeChartHelpers(); + SystemEventsContent.DisposeChartHelpers(); } public void RefreshAutoRefreshSettings()