☰
WPF DataGrid 仿 Excel 筛选:从 Filter 到列头交互的完整实现
2026/9/28 5:48:16 网站建设 项目流程

简介:这是一份面向WPF开发者的DataGrid仿Excel筛选功能实例,在.NET6.0与Visual Studio 2022环境下,为DataGrid控件注入类似Excel的交互式筛选能力,帮助改善桌面应用中大量数据的浏览与管理效率。资源包共76个文件、约349KB,以15个C#源码文件为核心,配合XAML界面布局、csproj/json/editorconfig等工程配置,以及编译生成的dll/exe/pdb等,结构清晰,便于运行验证和拆解学习。内容覆盖AutoGenerateColumns自定义列、DataGridTextColumn的Filtering事件、ICollectionView视图刷新、列头下拉筛选菜单等关键实现,并涉及等于/不等于/包含等筛选条件、多列组合筛选以及筛选状态保存等扩展设计。该实例已有290人学习浏览,适合希望掌握WPF数据网格高级交互、提升桌面应用数据管理能力的中级开发者参考。

1. WPF DataGrid 做 Excel 式筛选:先明确你真正要抄的是哪两层

接手内部系统改造时遇到过一个典型场景:业务方指着 Excel 说,“表头点一下就出筛选,公司自己的 WPF 程序为什么做不到”。当时 DataGrid 里躺着五千多行人员数据,列头只有排序箭头,找不到数据靠 Ctrl+F,体验被用户反复吐槽。把滚动条拉到顶、点开每个列的上下文菜单再选筛选条件,那个流程放到数据密集的表格上基本不可用。

这个标题背后真正要解决的是两层问题:第一层叫数据过滤能力——把满足条件的行从集合里拿出来,这是 WPF 里ICollectionView.Filter的老本行;第二层是 Excel 风格的表头交互——点列头右侧的下拉箭头,弹出可多选的值列表和搜索框。这两层必须分开做:Filter 只管数据,ColumnHeader 模板和 Popup 只管交互。两者解耦之后,数据量大到需要异步筛选时你才有还手余地。适合正在评估手写方案和第三方控件取舍的人,也适合照着做第一个能用的原型。全文会从最小可复现的方案讲起,再一步步把交互和性能补到能上生产环境的程度。

2. 最小跑通方案:CollectionViewSource 绑定 + Filter 谓词的三段代码

2.1 三条路线怎么选:不是非黑即白

我先给结论:如果你的筛选需求只是“下拉选一个值,只匹配等于”,用现成扩展控件最快;如果业务里会出现“同时筛选部门、城市、入职年份、薪资区间,并且值列表要实时联动”,手写比改第三方控件源码更省事。常见做法有三条路线,我对它们的取舍是这样的:

路线上手速度可定制程度大数据量表现适用场景
Extended WPF Toolkit 的 AutoFilter最快,拖进来就能用低,改筛选逻辑要扒源码中,默认同步过滤快速原型、简单枚举筛选
CollectionViewSource.Filter + 自建列头模板中等,模板要自己写高,每个交互都能控制高,Filter 可替换也可异步复杂业务、Excel 风格多选筛选
DataTable.DefaultView.RowFilter中等,字符串过滤表达式中,但绑定和类型转换坑多较高,但 UI 交互仍需自建后台报表、非 DataGrid 场景

AutoFilter 不是不能用,它的坑在于:多列筛选时值列表不会随其他列的筛选结果联动,这一点和 Excel 的行为有明显差距。我见过不少项目先引入 AutoFilter,最后发现业务要“筛选过的列表里再筛选”时,又推倒重写。所以这里直接讲自建路线,重点是把数据流跑通,交互放下一章。

2.2 先绑定数据:DataGrid 的 ItemsSource 换成 View

很多人写 DataGrid 绑定数据时直接ItemsSource = myList,这在 Excel 式筛选场景里会给后面留坑。ICollectionView上的 Filter、Sort、Group 才是 WPF 表格筛选的正确入口,直接绑集合就相当于把控制权交给了 DataGrid 默认视图。改成绑 View 的代码很简单:

// MainWindow.xaml.cs 里初始化数据源与视图 public partial class MainWindow : Window { public List<Person> People { get; set; } = new(); public ICollectionView PeopleView { get; set; } public MainWindow() { InitializeComponent(); LoadFakeData(); // 构造 5000 行测试数据 PeopleView = CollectionViewSource.GetDefaultView(People); DataGrid.ItemsSource = PeopleView; // 关键:绑定到视图,而不是 List } private void LoadFakeData() { // 实际项目中可能是数据库查询结果或 JSON 反序列化 for (int i = 0; i < 5000; i++) { People.Add(new Person { Id = i, Name = $"员工{i}", Dept = _depts[i % _depts.Length], City = _cities[i % _cities.Length], BaseSalary = 5000 + (i * 37 % 15000), HireDate = DateTime.Today.AddDays(-(i * 13 % 1200)) }); } } }

这里唯一需要注意的绑定点是CollectionViewSource.GetDefaultView(People)。同一个 List 在 DataGrid 和 ComboBox 等控件里共享同一个视图,所以后面筛选一个视图,下拉框里看到的数据也会跟着变。Person 类可以是 POCO,属性只要公开可读,DataGrid 的 AutoGenerateColumns 会自动生成列。数据量五万行以下这个写法没有明显性能问题,超过五万再考虑第四章的异步策略。

2.3 用 Filter 谓词把行过滤出来:文本、数字、日期分开写

ICollectionView.Filter需要的是一个Predicate<object>,返回 true 表示该行保留,false 表示隐藏。它会在每次 Refresh 时对视图里的所有行执行一次,所以谓词里避免做反射、字符串解析这类重活。最常见的正确姿势是提前定好过滤条件,谓词内只做属性取值和比较:

// 一个简单的单选筛选示例:按部门过滤 private void ApplyDeptFilter(string selectedDept) { PeopleView.Filter = item => { var person = item as Person; if (person == null) return false; // selectedDept 为空时不做过滤 if (string.IsNullOrEmpty(selectedDept)) return true; return person.Dept == selectedDept; }; }

这段代码背后的关键点是:Filter 谓词会被反复执行,所以item as Person这种类型转换无法避免,但可以尽量少做字符串操作。如果要支持数字和日期的区间筛选,可以用一个FilterRule对象集中承载条件,谓词里走同一套匹配逻辑:

public bool IsMatch(Person p) { // 支持文本包含、数字区间、日期区间三类规则 return FieldName switch { nameof(Person.City) => p.City.Contains(TextValue, StringComparison.OrdinalIgnoreCase), nameof(Person.BaseSalary) => p.BaseSalary >= MinValue && p.BaseSalary <= MaxValue, nameof(Person.HireDate) => p.HireDate >= MinDate && p.HireDate <= MaxDate, _ => true }; }

首次接触 Filter 的人容易踩一个坑:修改了谓词之后界面上没有变化,因为 Filter 属性被设置后必须手动触发刷新,常用的是PeopleView.Refresh()。如果列表数据本身是 ObservableCollection,Filter 不会自动重新执行,记住这句话能省半小时排查时间。

3. 把列头改成筛选下拉:模板、弹层、多选值列表的完整实现

3.1 列头模板:把“排序区域”和“漏斗按钮”拆开

Excel 筛选的交互核心是列头的小漏斗,点它弹出下拉,点其他区域仍然是排序。WPF DataGrid 默认的列头模板整体都会触发排序,所以必须重写DataGridColumnHeader的 ControlTemplate,把内容区域和筛选按钮拆到两个格子。

<Window.Resources> <ControlTemplate x:Key="ExcelHeaderTemplate" TargetType="DataGridColumnHeader"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Padding="{TemplateBinding Padding}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <!-- 左侧放列标题,点击走原有排序逻辑 --> <ContentPresenter Grid.Column="0" VerticalAlignment="Center" ContentSource=".Content" /> <!-- 右侧漏斗按钮,点击弹出筛选面板 --> <ToggleButton Grid.Column="1" x:Name="FilterButton" Width="18" Height="18" Margin="2,0,0,0" Cursor="Hand" ToolTip="筛选" Click="FilterButton_Click"> <Path Data="M0,0 L10,0 L6,6 L6,10 L4,8 L4,6 Z" Fill="#444" /> </ToggleButton> </Grid> </Border> </ControlTemplate> </Window.Resources>

这个模板里有两个关键设计:一是用 ToggleButton 而不是普通 Button,因为筛选面板打开时按钮要保持选中状态,方便用户看出哪一列正在筛选;二是 Click 事件里必须处理路由事件穿透。DataGridColumnHeader默认在鼠标按下时就开始排序,如果漏斗按钮的 Click 不拦截,会出现“点漏斗也排序,弹层打开后列顺序已经变了”的怪异表现。所以在 Click 里写e.Handled = true是必须的。

模板要在 DataGrid 上生效,可以全局指定也可以针对列指定。用全局 ColumnHeaderStyle 适合所有列都带筛选的情况:

<DataGrid x:Name="MainGrid" AutoGenerateColumns="True" ItemsSource="{Binding PeopleView}"> <DataGrid.ColumnHeaderStyle> <Style TargetType="DataGridColumnHeader"> <Setter Property="Template" Value="{StaticResource ExcelHeaderTemplate}" /> </Style> </DataGrid.ColumnHeaderStyle> </DataGrid>

给列头模板加上后的第一个直接效果是:列头变成两段式布局。左侧点住拖动可以调整列宽,右侧漏斗独立点击,排序箭头仍显示在 ContentPresenter 附近。这里有个容易被忽略的细节——ContentPresenter的ContentSource="Content"必须保留,否则列标题文字不会显示。

3.2 弹层与值列表:一个 Popup 复用到所有列

筛选面板需要的是一个临时浮动层,WPF 里最合适的是 Popup。一个常见做法是在每个列头模板里都放一个 Popup,但这样布局资源消耗大且定位麻烦。更干净的做法是窗体级只放一个 Popup,点击漏斗时动态设置它的 PlacementTarget,并往里填充当前列的数据。下面这个 XAML 放在窗体根部:

<Popup x:Name="FilterPopup" StaysOpen="False" AllowsTransparency="True" PopupAnimation="Fade" Placement="Bottom" PlacementTarget="{Binding ElementName=MainGrid}"> <Border Width="240" Padding="8" Background="White" BorderBrush="#D0D0D0" BorderThickness="1" CornerRadius="4"> <StackPanel> <CheckBox x:Name="SelectAllCheckBox" Content="全选" Checked="SelectAllCheckBox_Changed" Unchecked="SelectAllCheckBox_Changed" /> <TextBox x:Name="SearchTextBox" Margin="0,6,0,0" Padding="4" TextChanged="SearchTextBox_TextChanged" /> <ListBox x:Name="FilterValueListBox" Height="180" Margin="0,6,0,0" SelectionMode="Multiple" /> <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,8,0,0"> <Button Content="确定" Click="ApplyFilter_Click" Padding="10,3" /> <Button Content="清空筛选" Click="ClearFilter_Click" Margin="8,0,0,0" Padding="10,3" /> </StackPanel> </StackPanel> </Border> </Popup>

代码里做了三件准备:全选复选框、搜索文本框、支持多选的 ListBox。这个结构基本复刻了 Excel 的筛选面板。弹层内容跟列走,所以点击漏斗时要做的事是:把 Popup 的PlacementTarget指向当前漏斗按钮,填充值列表,然后IsOpen = true。

private DataGridColumn _currentFilterColumn; private string _currentFilterField; private void FilterButton_Click(object sender, RoutedEventArgs e) { e.Handled = true; // 阻止排序 var toggle = sender as ToggleButton; var header = FindAncestor<DataGridColumnHeader>(toggle); _currentFilterColumn = header.Column; _currentFilterField = GetPropertyNameFromHeader(_currentFilterColumn); FilterPopup.PlacementTarget = toggle; FilterPopup.IsOpen = true; LoadDistinctValuesForCurrentColumn(); }

FindAncestor是沿可视树往上找父级的小工具函数,网上很多实现,这里不展开。从列头到实际绑定属性名的映射,建议在列创建时用DataGridBoundColumn.Binding解析:

private string GetPropertyNameFromHeader(DataGridColumn column) { if (column is DataGridBoundColumn boundColumn && boundColumn.Binding is Binding b) { return b.Path.Path; // 比如 "Dept"、"BaseSalary" } return string.Empty; }

这一步非常关键:AutoGenerateColumns 模式下列的 Header 文字和属性名不一定一致,如果 Header 是“部门”而绑定路径是 Dept,解析错了 Filter 谓词永远匹配不上。

3.3 值列表联动刷新:像 Excel 那样“越筛越精确”

Excel 里最容易被忽略但极影响体验的行为是:列 A 筛选“研发部”后,再点列 B 的下拉,列 B 的值列表只显示研发部对应行里的值。也就是说值列表要基于“其他列已经生效的筛选条件”来计算,而不是全量 Distinct。这个联动逻辑是仿 Excel 筛选的灵魂,一半的仿品都死在这里。

private void LoadDistinctValuesForCurrentColumn() { // 在后台线程取 Distinct,避免 UI 卡顿;数据量小可直接同步 var otherFilters = _activeFilters .Where(f => f.Key != _currentFilterField) .ToList(); var values = People .Where(p => otherFilters.All(f => f.Value.IsMatch(p))) // 其他列过滤 .Select(p => GetPropertyValue(p, _currentFilterField)) // 取当前列值 .Where(v => v != null) .Distinct() .OrderBy(v => v) .ToList(); FilterValueListBox.ItemsSource = values; // 恢复当前列已勾选项 for (int i = 0; i < FilterValueListBox.Items.Count; i++) { FilterValueListBox.SelectionMode = SelectionMode.Multiple; FilterValueListBox.SelectedItems.Add(FilterValueListBox.Items[i]); } }

注意这里的_activeFilters是当前已经生效的筛选条件字典。取 Distinct 时只排除当前列本身,其他列条件全部生效。这带来的现象是:先筛部门,再开城市列表,没选到的城市不会出现。Excel 本身就是这个逻辑。

有个并发问题:行数超过几万时,上面这段全同步跑会让弹层打开前卡一两秒。实际项目中我用的是Task.Run(() => 计算 Distinct),拿到结果后通过Dispatcher.BeginInvoke回填 ListBox。后面第四章会详细写异步版本。输入搜索框过滤值列表里的项也走这个套路,但不用再碰原始数据集,直接对当前ItemsSource那几百个值做内存过滤就行。

4. 数据量大时怎么办:防抖刷新、并行取 Distinct 与三处关键参数

4.1 先看清卡顿来源:两次全表扫描

CollectionViewSource 的筛选本质是把原始集合过一遍谓词,每次 Refresh 都是 O(n)。数据量大后最常见的表现是两个场景卡顿:打开筛选弹层时,求值列表要遍历一遍全量数据取 Distinct;点击确定应用筛选时,Refresh 又遍历一遍。两个操作叠加,3 万行数据就可能卡到肉眼可见。我在一个五万行的生产项目里测过,打开弹层秒级延迟,用户第一反应是“程序死了”。

另一个隐蔽的坑是:Filter 谓词里如果写了person.Dept.Contains(searchText, StringComparison.OrdinalIgnoreCase)这类带大小写转换的操作,每次比较都会生成临时字符串,5 万行 x 10 次比较,GC 压力直接反映到 UI 线程的卡顿上。所以谓词要写得尽量轻:枚举字段用==比较,字符串包含过滤只在用户输入搜索词时才启用,数值和日期用区间判断。

4.2 防抖 + 版本号:解决异步筛选的乱序覆盖

写异步筛选第一个容易翻车的点:用户连续改筛选条件,第一次 Task.Run 还没跑完,第二次条件又提交了,结果后提交的 Task 先回来,界面显示的是旧条件的结果。解决这个竞态在个人项目里靠猜,在生产项目里必须用版本号。做法是每次过滤前给条件加一个自增序号,Task 完成时比较序号,只有最新序号的结果才允许落盘:

private int _filterVersion; private void ApplyFiltersAsync() { var version = ++_filterVersion; // 每次新条件,版本号 +1 var snapshot = _activeFilters.ToDictionary( f => f.Key, f => (ColumnFilter)f.Value.Clone()); Task.Run(() => { var matched = People .Where(p => snapshot.Values.All(rule => rule.IsMatch(p))) .ToList(); // 后台完成匹配 return matched; }).ContinueWith(t => { // 版本号校验:如果不是最新版本,丢弃结果 if (version != _filterVersion) return; if (t.IsFaulted) { /* 记录日志,UI 提示 */ return; } var result = t.Result; // 把匹配结果交给一个轻量集合视图 _filteredList.Clear(); _filteredList.AddRange(result); // 通知 DataGrid 刷新视图 PeopleView.Refresh(); }, TaskScheduler.FromCurrentSynchronizationContext()); }

注意_filterVersion是在 UI 线程自增的,后台线程不修改它,只读取,所以不需要加锁。ContinueWith 用FromCurrentSynchronizationContext把回填动作调度回 UI 线程。_filteredList建议用List<T>加AddRange,不要用ObservableCollection一行行 Add——那样每一次 Add 都会触发 CollectionChanged,DataGrid 会按“插入一行”的方式刷新,性能远差于一次 Refresh。真正要绑定到 DataGrid 的还是PeopleView,但它的Source指向_filteredList这个新集合。这样原始People保持不变,只是换了视图源。

如果坚持用 ICollectionView.Filter 而不换聚合源,也有替代方案:后台线程算出命中行的索引集合,然后 UI 线程只把 Filter 谓词替换成“查索引集合 O(1)”。这个方案不破坏 DataGrid 的编辑状态,但实现更绕,适合 DataGrid 里大量行处于编辑态的复杂业务。

4.3 DataGrid 的三处参数:虚拟化与延迟加载

DataGrid 本身对大数据量是能扛的,问题出在默认参数对筛选场景并不友好。两三万行不至于卡,但一旦你和 Filter 组合,就得关注以下三处参数:

参数默认值筛选场景建议说明
EnableRowVirtualizationTrue保持 True行虚拟化,保证滚动条顺滑
EnableColumnVirtualizationTrue保持 True列多时启用,注意与冻结列冲突
ScrollUnitPixel大数据量可改 Item按项滚动时虚拟化命中率更高,但滚动体验略跳
ItemsPanelVirtualizingStackPanel不换换成 StackPanel 会让虚拟化失效,这也是常见的“优化后更卡”根源

还有一个建议:如果弹层打开时要拉取 Distinct,不要在 UI 线程做,也不要和 Filter 刷新串行。我一般会开一个ManualResetEventSlim控制弹层打开时只做一次后台加载,缓存结果到字典里——同一个列第二次打开直接读缓存,只有数据源整体变化时才清缓存。因为这个细节,用户连续切换不同列的筛选下拉时,弹层打开速度会稳定在几十毫秒,而不是每次重查两万行。

5. 避坑:仿 Excel 筛选最容易翻车的 6 个现场与排查顺序

5.1 漏斗按钮点了没反应,列头还莫名其妙排序了

现象:模板替换后点击漏斗,排序箭头变动,弹层不出现或一闪而过。

原因:ToggleButton.Click事件沿路由冒泡到DataGridColumnHeader的排序逻辑,同时 Popup 因为StaysOpen=False在鼠标移出弹层的瞬间自动关闭,两者叠加看起来像“没反应”。另一个常见诱因是模板里的ToggleButton没有设置Focusable=False,点击后焦点切换导致 Popup 关闭。

解决:Click 里必须e.Handled = true;Popup 打开后延迟一点点时间设置焦点,比如FilterPopup.IsOpen = true; SearchTextBox.Focus();放在 Dispatcher.BeginInvoke 里。排查顺序是先看 Click 是否触发,再看 Popup 的 IsOpen 状态是否被立即置回 false。

5.2 Popup 定位错乱:多列快速切换时弹到屏幕外

现象:点击第一列漏斗正常,快速点击第五列漏斗,弹层显示在错误位置或直接超出窗口边界。

原因:PlacementTarget在按钮被重绘或列头滚动后指向了一个已失效的可视元素。尤其是 DataGrid 横向滚动后,旧按钮已经被虚拟化回收,Popup 定位仍引用旧坐标。

解决:每次打开弹层前重新赋值PlacementTarget为当前按钮,并调用FilterPopup.InvalidateMeasure()刷新布局;如果窗口跑到了多显示器边界,给 Popup 加一个MaxHeight和VerticalOffset限制,让内容始终在屏内。

5.3 值列表显示的是一堆 System.DataRowView

现象:数据源来自 DataTable 时,Distinct 取出来的类型是DataRowView,下拉框里每行都长这样,没法看。

原因:GetPropertyValue对DataRowView反射拿不到属性,直接 ToString 出了类型全名。DataTable 场景和 POCO 场景的值提取逻辑完全不同。

解决:判断item is DataRowView drv时用drv.Row[field],POCO 场景才走反射或表达式树。两者统一收敛到一个ValueExtractor类里,用接口隔离,Filter 谓词和 Distinct 计算都走同一套提取器,避免两处逻辑不一致导致“筛选结果跟列表显示对不上”。

5.4 分组视图下,筛选出来空组不消失

现象:DataGrid 开了GroupStyle和ICollectionView.GroupDescriptions,应用筛选后不符合条件的组只剩组头,下面没有任何行,组头占着位置。

原因:ListCollectionView的筛选和分组同时生效时,空组不会自动移除,这是 WPF 视图的老行为。Excel 的分组筛选里空组是隐藏的,所以用户会认为程序有 bug。

解决:筛选应用之后重设 GroupDescriptions——先把集合Clear(),再把PropertyGroupDescription重新加回来,强制视图重新计算分组。如果组很多且不想丢滚动位置,可以记录当前滚动偏移,重设分组后ScrollIntoView恢复。

5.5 正在编辑单元格时应用筛选,刚输入的值丢了

现象:用户正在某个单元格里打字,还没按回车,旁边人点了另一列的筛选按钮,应用后单元格内容变回旧值,输入全丢。

原因:DataGrid 的编辑状态是绑定在行上的,Refresh()时行被重新加载,未提交的编辑内容随之丢失。

解决:在打开筛选弹层或应用筛选之前调用强制提交:

private void BeforeFilterApply() { MainGrid.CommitEdit(DataGridEditingUnit.Row, true); }

这个调用会把当前编辑行的所有变更写回数据源,Filter 再执行时拿到的就是最新值。如果数据源不允许脏数据入库,也要至少CancelEdit()后再刷新,避免界面和内存中的值不一致造成的“筛选结果对不上”。

5.6 列头模板在隐藏列和“一行两行显示”场景下失配

现象:项目里为了适配窄屏做过DataGrid列头换行或者RowDetails展开两行显示,套上自定义列头模板后,漏斗按钮显示不全,或者列头高度被撑高到异常。

原因:ControlTemplate 里Grid的尺寸没有约束,列头自动换行时Height被内容撑开,漏斗按钮在 Grid 第二列被挤到看不见。隐藏列的场合,Visibility为 Collapsed 的列其模板仍然参与布局,导致列头整体宽度计算异常。

解决:模板里给 Grid 加Height="26"之类的固定值,或者用MaxHeight约束;隐藏列时在数据绑定层直接用ColumnVisibility绑定来控制,不要依赖模板自身的 Visible 状态。这个坑排查起来最花时间,因为视觉上看起来像模板写错了,实际上是对接的列状态机制冲突。

6. 进阶:把筛选状态做成可持久化的规则,用单测守住过滤器

6.1 筛选规则从界面代码中抽出来:一个纯函数类

很多仿 Excel 筛选做到能用就停了,但切换到别的窗口再切回来,筛选条件全部清空,用户得重新设置。Excel 本身在保存工作簿后重开,筛选箭头和条件都还在。这里给你的 DataGrid 筛选也做同样的事:把筛选条件定义为FilterRule列表,序列化成 JSON 存到本地配置。

public class FilterRule { public string Field { get; set; } // 属性名,如 Dept public string Op { get; set; } // eq / contains / range public List<string> Values { get; set; } // 多选值 public double? Min { get; set; } // 数值/日期区间下限 public double? Max { get; set; } // 数值/日期区间上限 }

窗口关闭时把_activeFilters序列化,重新打开时反序列化并依次应用。这里有一个细节要强调:JSON 里存属性名和操作符,不是为了省事,而是为了让筛选逻辑可以被单元测试直接调用,不用启动 WPF 窗口。筛选逻辑写得再漂亮,如果必须打开程序手动验证,回归成本迟早会让你放弃维护。

6.2 用单测验证规则组合:一小时内守住回归

把谓词逻辑放进静态方法后,测试可以完全不依赖 UI。下面是一个 xUnit 测试,验证“部门=研发部 + 工资区间 8000~12000”的组合:

[Fact] public void DeptFilter_And_SalaryRange_ShouldReturnExpectedRows() { var people = new List<Person> { new Person { Dept = "研发部", BaseSalary = 9000 }, new Person { Dept = "研发部", BaseSalary = 15000 }, new Person { Dept = "市场部", BaseSalary = 9000 } }; var rules = new List<FilterRule> { new FilterRule { Field = "Dept", Op = "eq", Values = new List<string> { "研发部" } }, new FilterRule { Field = "BaseSalary", Op = "range", Min = 8000, Max = 12000 } }; var result = people.Where(p => rules.All(r => r.IsMatch(p))).ToList(); Assert.Single(result); Assert.Equal("研发部", result[0].Dept); }

这个测试的价值在于:以后谁动了筛选逻辑,运行一遍测试就能知道哪些组合被改坏了。我每次接到带筛选功能的改动,都会先把现有规则写成类似用例,再动实现。筛选这种功能表面简单,实际边界组合多,靠手点根本点不全。

最后说一个我的习惯:DataGrid 仿 Excel 筛选,永远先做数据链路,再做交互皮肤,最后才处理样式细节。顺序反了,往往会在调试套路问题上浪费大量时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询