Skip to content

Commit ae53535

Browse files
Split QueryStoreGridControl.axaml.cs into partial classes
Move-only refactor; no behavior changes. QueryStoreGridControl.axaml.cs (1,900 lines) split into 7 partials: Fetch (450) - Fetch_Click + FetchPlans/WaitStats + grouping Filters (229) - search + column filter popup + ApplyFilters GroupBy (148) - GroupBy + ReorderColumns + row expand/collapse Sort (245) - sorting + UpdateBarRatios + GetSortKey + status Selection (134) - select/load/ViewHistory/context menu + copy WaitStats (182) - wait fetch + category click + mode toggle TimeRange ( 33) - OnTimeRangeChanged Main file now 451 lines — fields, ctor, public API (SetInitialTimeRange), DB picker (PopulateDatabaseBox, QsDatabase_SelectionChanged), and static accessor/column dictionaries. The standalone QueryStoreRow class at the bottom of the file is untouched. Build clean: 0 errors, 0 warnings on PlanViewer.App. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 02120e6 commit ae53535

8 files changed

Lines changed: 1631 additions & 1449 deletions

src/PlanViewer.App/Controls/QueryStoreGridControl.Fetch.cs

Lines changed: 485 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
4+
using System.ComponentModel;
5+
using System.Linq;
6+
using System.Runtime.CompilerServices;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Avalonia;
10+
using Avalonia.Controls;
11+
using Avalonia.Controls.Primitives;
12+
using Avalonia.Input;
13+
using Avalonia.Interactivity;
14+
using Avalonia.Layout;
15+
using Avalonia.Media;
16+
using PlanViewer.App.Dialogs;
17+
using PlanViewer.App.Services;
18+
using PlanViewer.Core.Interfaces;
19+
using PlanViewer.Core.Models;
20+
using PlanViewer.Core.Services;
21+
22+
namespace PlanViewer.App.Controls;
23+
24+
public partial class QueryStoreGridControl : UserControl
25+
{
26+
private QueryStoreFilter? BuildSearchFilter()
27+
{
28+
var searchType = (SearchTypeBox.SelectedItem as ComboBoxItem)?.Tag?.ToString();
29+
var searchValue = SearchValueBox.Text?.Trim();
30+
31+
if (string.IsNullOrEmpty(searchType) || string.IsNullOrEmpty(searchValue))
32+
return null;
33+
34+
var filter = new QueryStoreFilter();
35+
36+
switch (searchType)
37+
{
38+
case "query-id" when long.TryParse(searchValue, out var qid):
39+
filter.QueryId = qid;
40+
break;
41+
case "query-id":
42+
StatusText.Text = "Invalid Query ID";
43+
return null;
44+
case "plan-id" when long.TryParse(searchValue, out var pid):
45+
filter.PlanId = pid;
46+
break;
47+
case "plan-id":
48+
StatusText.Text = "Invalid Plan ID";
49+
return null;
50+
case "query-hash":
51+
filter.QueryHash = searchValue;
52+
break;
53+
case "plan-hash":
54+
filter.QueryPlanHash = searchValue;
55+
break;
56+
case "module":
57+
// Default to dbo schema if no schema specified, following sp_QuickieStore pattern
58+
filter.ModuleName = searchValue.Contains('.') ? searchValue : $"dbo.{searchValue}";
59+
break;
60+
default:
61+
return null;
62+
}
63+
64+
return filter;
65+
}
66+
67+
private void SearchValue_KeyDown(object? sender, Avalonia.Input.KeyEventArgs e)
68+
{
69+
if (e.Key == Avalonia.Input.Key.Enter)
70+
{
71+
Fetch_Click(sender, e);
72+
e.Handled = true;
73+
}
74+
}
75+
76+
private void ClearSearch_Click(object? sender, RoutedEventArgs e)
77+
{
78+
SearchTypeBox.SelectedIndex = 0;
79+
SearchValueBox.Text = "";
80+
}
81+
82+
private void SetupColumnHeaders()
83+
{
84+
var cols = ResultsGrid.Columns;
85+
// cols[0] = Expand column, cols[1] = Checkbox
86+
SetColumnFilterButton(cols[2], "QueryId", "Query ID");
87+
SetColumnFilterButton(cols[3], "PlanId", "Plan ID");
88+
SetColumnFilterButton(cols[4], "QueryHash", "Query Hash");
89+
SetColumnFilterButton(cols[5], "PlanHash", "Plan Hash");
90+
SetColumnFilterButton(cols[6], "ModuleName", "Module");
91+
// cols[7] = WaitProfile (no filter button)
92+
SetColumnFilterButton(cols[8], "LastExecuted", "Last Executed (Local)");
93+
SetColumnFilterButton(cols[9], "Executions", "Executions");
94+
SetColumnFilterButton(cols[10], "TotalCpu", "Total CPU (ms)");
95+
SetColumnFilterButton(cols[11], "AvgCpu", "Avg CPU (ms)");
96+
SetColumnFilterButton(cols[12], "TotalDuration", "Total Duration (ms)");
97+
SetColumnFilterButton(cols[13], "AvgDuration", "Avg Duration (ms)");
98+
SetColumnFilterButton(cols[14], "TotalReads", "Total Reads");
99+
SetColumnFilterButton(cols[15], "AvgReads", "Avg Reads");
100+
SetColumnFilterButton(cols[16], "TotalWrites", "Total Writes");
101+
SetColumnFilterButton(cols[17], "AvgWrites", "Avg Writes");
102+
SetColumnFilterButton(cols[18], "TotalPhysReads", "Total Physical Reads");
103+
SetColumnFilterButton(cols[19], "AvgPhysReads", "Avg Physical Reads");
104+
SetColumnFilterButton(cols[20], "TotalMemory", "Total Memory (MB)");
105+
SetColumnFilterButton(cols[21], "AvgMemory", "Avg Memory (MB)");
106+
SetColumnFilterButton(cols[22], "QueryText", "Query Text");
107+
}
108+
109+
private void SetColumnFilterButton(DataGridColumn col, string columnId, string label)
110+
{
111+
var icon = new TextBlock
112+
{
113+
Text = "▽",
114+
FontSize = 12,
115+
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
116+
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
117+
};
118+
var btn = new Button
119+
{
120+
Content = icon,
121+
Tag = columnId,
122+
Width = 16,
123+
Height = 16,
124+
Padding = new Avalonia.Thickness(0),
125+
Background = Brushes.Transparent,
126+
BorderThickness = new Avalonia.Thickness(0),
127+
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
128+
};
129+
btn.Click += ColumnFilter_Click;
130+
ToolTip.SetTip(btn, "Click to filter");
131+
132+
var text = new TextBlock
133+
{
134+
Text = label,
135+
FontWeight = FontWeight.Bold,
136+
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center,
137+
Margin = new Avalonia.Thickness(4, 0, 0, 0),
138+
};
139+
140+
var header = new StackPanel
141+
{
142+
Orientation = Avalonia.Layout.Orientation.Horizontal,
143+
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left,
144+
};
145+
header.Children.Add(btn);
146+
header.Children.Add(text);
147+
col.Header = header;
148+
}
149+
150+
private void EnsureFilterPopup()
151+
{
152+
if (_filterPopup != null) return;
153+
_filterPopupContent = new ColumnFilterPopup();
154+
_filterPopup = new Popup
155+
{
156+
Child = _filterPopupContent,
157+
IsLightDismissEnabled = true,
158+
Placement = PlacementMode.Bottom,
159+
};
160+
// Add to visual tree so DynamicResources resolve inside the popup
161+
((Grid)Content!).Children.Add(_filterPopup);
162+
_filterPopupContent.FilterApplied += OnFilterApplied;
163+
_filterPopupContent.FilterCleared += OnFilterCleared;
164+
}
165+
166+
private void ColumnFilter_Click(object? sender, RoutedEventArgs e)
167+
{
168+
if (sender is not Button button || button.Tag is not string columnId) return;
169+
EnsureFilterPopup();
170+
_activeFilters.TryGetValue(columnId, out var existing);
171+
_filterPopupContent!.Initialize(columnId, existing);
172+
_filterPopup!.PlacementTarget = button;
173+
_filterPopup.IsOpen = true;
174+
}
175+
176+
private void OnFilterApplied(object? sender, FilterAppliedEventArgs e)
177+
{
178+
_filterPopup!.IsOpen = false;
179+
if (e.FilterState.IsActive)
180+
_activeFilters[e.FilterState.ColumnName] = e.FilterState;
181+
else
182+
_activeFilters.Remove(e.FilterState.ColumnName);
183+
ApplySortAndFilters();
184+
UpdateFilterButtonStyles();
185+
}
186+
187+
private void OnFilterCleared(object? sender, EventArgs e)
188+
{
189+
_filterPopup!.IsOpen = false;
190+
}
191+
192+
private void UpdateFilterButtonStyles()
193+
{
194+
foreach (var col in ResultsGrid.Columns)
195+
{
196+
if (col.Header is not StackPanel sp) continue;
197+
var btn = sp.Children.OfType<Button>().FirstOrDefault();
198+
if (btn?.Tag is not string colId) continue;
199+
if (btn.Content is not TextBlock tb) continue;
200+
201+
bool hasFilter = _activeFilters.TryGetValue(colId, out var f) && f.IsActive;
202+
tb.Text = hasFilter ? "▼" : "▽";
203+
if (hasFilter)
204+
tb.Foreground = new SolidColorBrush(Color.FromRgb(0xFF, 0xD7, 0x00));
205+
else
206+
tb.ClearValue(TextBlock.ForegroundProperty);
207+
208+
ToolTip.SetTip(btn, hasFilter
209+
? $"Filter: {f!.DisplayText} (click to modify)"
210+
: "Click to filter");
211+
}
212+
}
213+
214+
private void ApplyFilters()
215+
{
216+
ApplySortAndFilters();
217+
}
218+
219+
private bool RowMatchesAllFilters(QueryStoreRow row)
220+
{
221+
foreach (var (colId, state) in _activeFilters)
222+
{
223+
if (!state.IsActive) continue;
224+
if (TextAccessors.TryGetValue(colId, out var textAcc))
225+
{
226+
if (!MatchText(textAcc(row), state.Operator, state.Value)) return false;
227+
}
228+
else if (NumericAccessors.TryGetValue(colId, out var numAcc))
229+
{
230+
var isTextOp = state.Operator is FilterOperator.Contains or FilterOperator.StartsWith
231+
or FilterOperator.EndsWith or FilterOperator.IsEmpty or FilterOperator.IsNotEmpty;
232+
if (isTextOp)
233+
{
234+
if (!MatchText(numAcc(row).ToString("G"), state.Operator, state.Value)) return false;
235+
}
236+
else
237+
{
238+
if (!double.TryParse(state.Value, out var numVal)) continue;
239+
if (!MatchNumeric(numAcc(row), state.Operator, numVal)) return false;
240+
}
241+
}
242+
}
243+
return true;
244+
}
245+
246+
private static bool MatchText(string data, FilterOperator op, string val) => op switch
247+
{
248+
FilterOperator.Contains => data.Contains(val, StringComparison.OrdinalIgnoreCase),
249+
FilterOperator.Equals => data.Equals(val, StringComparison.OrdinalIgnoreCase),
250+
FilterOperator.NotEquals => !data.Equals(val, StringComparison.OrdinalIgnoreCase),
251+
FilterOperator.StartsWith => data.StartsWith(val, StringComparison.OrdinalIgnoreCase),
252+
FilterOperator.EndsWith => data.EndsWith(val, StringComparison.OrdinalIgnoreCase),
253+
FilterOperator.IsEmpty => string.IsNullOrEmpty(data),
254+
FilterOperator.IsNotEmpty => !string.IsNullOrEmpty(data),
255+
_ => true,
256+
};
257+
258+
private static bool MatchNumeric(double data, FilterOperator op, double val) => op switch
259+
{
260+
FilterOperator.Equals => Math.Abs(data - val) < 1e-9,
261+
FilterOperator.NotEquals => Math.Abs(data - val) >= 1e-9,
262+
FilterOperator.GreaterThan => data > val,
263+
FilterOperator.GreaterThanOrEqual => data >= val,
264+
FilterOperator.LessThan => data < val,
265+
FilterOperator.LessThanOrEqual => data <= val,
266+
_ => true,
267+
};
268+
}

0 commit comments

Comments
 (0)