From b6c5efc05aff5747ff52aee20c0db69a5884f0ee Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:21:22 -0400 Subject: [PATCH] Fix #659: Preserve DataGrid sort order across auto-refresh Save SortDescriptions before setting ItemsSource and restore them after, so user-applied sort order survives refresh cycles in both Dashboard and Lite. Dashboard: Replace SetInitialSort with SetItemsSourcePreservingSort across all 7 QueryPerformanceContent DataGrids. Lite: Add sort preservation to DataGridFilterManager (covers refresh and filter paths), rename SetInitialSort to SetDefaultSortIfNone which skips when user sort is already active. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Controls/QueryPerformanceContent.xaml.cs | 59 +++++++++++++------ Lite/Controls/ServerTab.xaml.cs | 23 ++++---- Lite/Services/DataGridFilterManager.cs | 33 +++++++++-- 3 files changed, 82 insertions(+), 33 deletions(-) diff --git a/Dashboard/Controls/QueryPerformanceContent.xaml.cs b/Dashboard/Controls/QueryPerformanceContent.xaml.cs index 4e6a0c20..cd6e293b 100644 --- a/Dashboard/Controls/QueryPerformanceContent.xaml.cs +++ b/Dashboard/Controls/QueryPerformanceContent.xaml.cs @@ -693,23 +693,20 @@ private async Task RefreshQueryStoreGridAsync() private void PopulateQueryStatsGrid(List data) { - QueryStatsDataGrid.ItemsSource = data; + SetItemsSourcePreservingSort(QueryStatsDataGrid, data, "AvgCpuTimeMs", ListSortDirection.Descending); QueryStatsNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed; - SetInitialSort(QueryStatsDataGrid, "AvgCpuTimeMs", ListSortDirection.Descending); } private void PopulateProcStatsGrid(List data) { - ProcStatsDataGrid.ItemsSource = data; + SetItemsSourcePreservingSort(ProcStatsDataGrid, data, "AvgCpuTimeMs", ListSortDirection.Descending); ProcStatsNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed; - SetInitialSort(ProcStatsDataGrid, "AvgCpuTimeMs", ListSortDirection.Descending); } private void PopulateQueryStoreGrid(List data) { - QueryStoreDataGrid.ItemsSource = data; + SetItemsSourcePreservingSort(QueryStoreDataGrid, data, "AvgCpuTimeMs", ListSortDirection.Descending); QueryStoreNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed; - SetInitialSort(QueryStoreDataGrid, "AvgCpuTimeMs", ListSortDirection.Descending); } private void SetStatus(string message) @@ -717,16 +714,42 @@ private void SetStatus(string message) _statusCallback?.Invoke(message); } - private static void SetInitialSort(DataGrid grid, string bindingPath, ListSortDirection direction) + private static void SetItemsSourcePreservingSort( + DataGrid grid, System.Collections.IEnumerable? newSource, + string? defaultSortProperty = null, + ListSortDirection defaultDirection = ListSortDirection.Descending) { - foreach (var column in grid.Columns) + var savedSorts = grid.Items.SortDescriptions.ToList(); + + grid.ItemsSource = newSource; + + if (savedSorts.Count > 0) { - if (column is DataGridBoundColumn bc && - bc.Binding is Binding b && - b.Path.Path == bindingPath) + foreach (var sort in savedSorts) + grid.Items.SortDescriptions.Add(sort); + + foreach (var column in grid.Columns) { - column.SortDirection = direction; - return; + if (column is DataGridBoundColumn bc && + bc.Binding is Binding b) + { + var match = savedSorts.FirstOrDefault(s => s.PropertyName == b.Path.Path); + column.SortDirection = match.PropertyName != null ? match.Direction : null; + } + } + } + else if (defaultSortProperty != null) + { + grid.Items.SortDescriptions.Add(new SortDescription(defaultSortProperty, defaultDirection)); + foreach (var column in grid.Columns) + { + if (column is DataGridBoundColumn bc && + bc.Binding is Binding b && + b.Path.Path == defaultSortProperty) + { + column.SortDirection = defaultDirection; + return; + } } } } @@ -851,7 +874,7 @@ private async Task RefreshActiveQueriesAsync() data = await _databaseService.GetQuerySnapshotsAsync(_activeQueriesHoursBack, _activeQueriesFromDate, _activeQueriesToDate); } - ActiveQueriesDataGrid.ItemsSource = data; + SetItemsSourcePreservingSort(ActiveQueriesDataGrid, data); ActiveQueriesNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed; SetStatus($"Loaded {data.Count} query snapshots"); LoadActiveQueriesSlicerAsync().ConfigureAwait(false); @@ -1015,7 +1038,7 @@ private async Task RefreshCurrentActiveQueriesAsync() var data = await _databaseService.GetCurrentActiveQueriesAsync(); _currentActiveUnfilteredData = data; - CurrentActiveQueriesDataGrid.ItemsSource = data; + SetItemsSourcePreservingSort(CurrentActiveQueriesDataGrid, data); CurrentActiveNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed; CurrentActiveTimestamp.Text = $"Last refreshed: {DateTime.Now:HH:mm:ss} — {data.Count} queries"; @@ -1757,9 +1780,8 @@ private async Task RefreshQueryStoreRegressionsAsync() { SetStatus("Loading query store regressions..."); var data = await _databaseService.GetQueryStoreRegressionsAsync(_qsRegressionsHoursBack, _qsRegressionsFromDate, _qsRegressionsToDate); - QueryStoreRegressionsDataGrid.ItemsSource = data; + SetItemsSourcePreservingSort(QueryStoreRegressionsDataGrid, data, "DurationRegressionPercent", ListSortDirection.Descending); QueryStoreRegressionsNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed; - SetInitialSort(QueryStoreRegressionsDataGrid, "DurationRegressionPercent", ListSortDirection.Descending); SetStatus($"Loaded {data.Count} query store regression records"); } catch (Exception ex) @@ -1858,9 +1880,8 @@ private async Task RefreshLongRunningPatternsAsync() { SetStatus("Loading long running query patterns..."); var data = await _databaseService.GetLongRunningQueryPatternsAsync(_lrqPatternsHoursBack, _lrqPatternsFromDate, _lrqPatternsToDate); - LongRunningQueryPatternsDataGrid.ItemsSource = data; + SetItemsSourcePreservingSort(LongRunningQueryPatternsDataGrid, data, "AvgDurationSec", ListSortDirection.Descending); LongRunningQueryPatternsNoDataMessage.Visibility = data.Count == 0 ? Visibility.Visible : Visibility.Collapsed; - SetInitialSort(LongRunningQueryPatternsDataGrid, "AvgDurationSec", ListSortDirection.Descending); SetStatus($"Loaded {data.Count} long running query pattern records"); } catch (Exception ex) diff --git a/Lite/Controls/ServerTab.xaml.cs b/Lite/Controls/ServerTab.xaml.cs index 136b4393..f33b4ea4 100644 --- a/Lite/Controls/ServerTab.xaml.cs +++ b/Lite/Controls/ServerTab.xaml.cs @@ -777,13 +777,13 @@ await System.Threading.Tasks.Task.WhenAll( _querySnapshotsFilterMgr!.UpdateData(snapshotsTask.Result); LiveSnapshotIndicator.Text = ""; _queryStatsFilterMgr!.UpdateData(queryStatsTask.Result); - SetInitialSort(QueryStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); + SetDefaultSortIfNone(QueryStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); _procStatsFilterMgr!.UpdateData(procStatsTask.Result); - SetInitialSort(ProcedureStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); + SetDefaultSortIfNone(ProcedureStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); _blockedProcessFilterMgr!.UpdateData(blockedProcessTask.Result); _deadlockFilterMgr!.UpdateData(DeadlockProcessDetail.ParseFromRows(deadlockTask.Result)); _queryStoreFilterMgr!.UpdateData(queryStoreTask.Result); - SetInitialSort(QueryStoreGrid, "TotalDurationMs", ListSortDirection.Descending); + SetDefaultSortIfNone(QueryStoreGrid, "TotalDurationMs", ListSortDirection.Descending); _serverConfigFilterMgr!.UpdateData(serverConfigTask.Result); _databaseConfigFilterMgr!.UpdateData(databaseConfigTask.Result); _dbScopedConfigFilterMgr!.UpdateData(databaseScopedConfigTask.Result); @@ -892,19 +892,19 @@ private async System.Threading.Tasks.Task RefreshQueriesAsync(int hoursBack, Dat case 2: // Top Queries by Duration var queryStats = await _dataService.GetTopQueriesByCpuAsync(_serverId, hoursBack, 50, fromDate, toDate, UtcOffsetMinutes); _queryStatsFilterMgr!.UpdateData(queryStats); - SetInitialSort(QueryStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); + SetDefaultSortIfNone(QueryStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); _ = LoadQueryStatsSlicerAsync(); break; case 3: // Top Procedures by Duration var procStats = await _dataService.GetTopProceduresByCpuAsync(_serverId, hoursBack, 50, fromDate, toDate, UtcOffsetMinutes); _procStatsFilterMgr!.UpdateData(procStats); - SetInitialSort(ProcedureStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); + SetDefaultSortIfNone(ProcedureStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); _ = LoadProcStatsSlicerAsync(); break; case 4: // Query Store by Duration var qsData = await _dataService.GetQueryStoreTopQueriesAsync(_serverId, hoursBack, 50, fromDate, toDate); _queryStoreFilterMgr!.UpdateData(qsData); - SetInitialSort(QueryStoreGrid, "TotalDurationMs", ListSortDirection.Descending); + SetDefaultSortIfNone(QueryStoreGrid, "TotalDurationMs", ListSortDirection.Descending); _ = LoadQueryStoreSlicerAsync(); break; } @@ -931,13 +931,13 @@ await System.Threading.Tasks.Task.WhenAll( _ = LoadActiveQueriesSlicerAsync(); _queryStatsFilterMgr!.UpdateData(queryStatsTask.Result); - SetInitialSort(QueryStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); + SetDefaultSortIfNone(QueryStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); _ = LoadQueryStatsSlicerAsync(); _procStatsFilterMgr!.UpdateData(procStatsTask.Result); - SetInitialSort(ProcedureStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); + SetDefaultSortIfNone(ProcedureStatsGrid, "TotalElapsedMs", ListSortDirection.Descending); _ = LoadProcStatsSlicerAsync(); _queryStoreFilterMgr!.UpdateData(queryStoreTask.Result); - SetInitialSort(QueryStoreGrid, "TotalDurationMs", ListSortDirection.Descending); + SetDefaultSortIfNone(QueryStoreGrid, "TotalDurationMs", ListSortDirection.Descending); _ = LoadQueryStoreSlicerAsync(); UpdateQueryDurationTrendChart(queryDurationTrendTask.Result); @@ -4376,8 +4376,11 @@ private void FilterPopup_FilterCleared(object? sender, EventArgs e) return null; } - private static void SetInitialSort(DataGrid grid, string bindingPath, ListSortDirection direction) + private static void SetDefaultSortIfNone(DataGrid grid, string bindingPath, ListSortDirection direction) { + if (grid.Items.SortDescriptions.Count > 0) return; + + grid.Items.SortDescriptions.Add(new SortDescription(bindingPath, direction)); foreach (var column in grid.Columns) { if (column is DataGridBoundColumn bc && diff --git a/Lite/Services/DataGridFilterManager.cs b/Lite/Services/DataGridFilterManager.cs index cf088387..1fe8c66a 100644 --- a/Lite/Services/DataGridFilterManager.cs +++ b/Lite/Services/DataGridFilterManager.cs @@ -8,9 +8,11 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Windows; using System.Windows.Controls; +using System.Windows.Data; using System.Windows.Media; using PerformanceMonitorLite.Models; @@ -45,7 +47,7 @@ public DataGridFilterManager(DataGrid dataGrid) /// /// Called when new data arrives (refresh cycle). Captures unfiltered data, - /// then re-applies any active filters. + /// then re-applies any active filters. Preserves user sort order. /// public void UpdateData(List newData) { @@ -53,7 +55,7 @@ public void UpdateData(List newData) if (!HasActiveFilters()) { - _dataGrid.ItemsSource = newData; + SetItemsSourcePreservingSort(newData); return; } @@ -85,7 +87,7 @@ private void ApplyFilters() if (!HasActiveFilters()) { - _dataGrid.ItemsSource = _unfilteredData; + SetItemsSourcePreservingSort(_unfilteredData); return; } @@ -99,7 +101,30 @@ private void ApplyFilters() return true; }).ToList(); - _dataGrid.ItemsSource = filteredData; + SetItemsSourcePreservingSort(filteredData); + } + + private void SetItemsSourcePreservingSort(System.Collections.IEnumerable? newSource) + { + var savedSorts = _dataGrid.Items.SortDescriptions.ToList(); + + _dataGrid.ItemsSource = newSource; + + if (savedSorts.Count > 0) + { + foreach (var sort in savedSorts) + _dataGrid.Items.SortDescriptions.Add(sort); + + foreach (var column in _dataGrid.Columns) + { + if (column is DataGridBoundColumn bc && + bc.Binding is Binding b) + { + var match = savedSorts.FirstOrDefault(s => s.PropertyName == b.Path.Path); + column.SortDirection = match.PropertyName != null ? match.Direction : null; + } + } + } } ///