
简介本资源是一个基于WPF与.NET 6.0实现DataGrid仿Excel筛选功能的完整可运行实例面向WPF桌面应用开发者及中高级C#学习者解决数据表格交互中缺乏原生多条件筛选能力的痛点适用于需高效处理结构化数据的管理类、报表类桌面软件开发场景。压缩包共76个文件含15个核心C#逻辑文件如Student.cs、FilterInfo.cs、MainWindowViewModel.cs、2个XAML界面文件、1个SLN解决方案及配套CSProj工程配置辅以DLL依赖、JSON配置与编译产物PDB、EXE等整体仅349KB轻量易集成。已有290人学习下载资源结构清晰包含从UI层MainWindow.xaml到数据层ObservableCollection绑定、筛选逻辑Filtering事件ICollectionView.Refresh再到菜单样式定制的全链路代码开箱即用便于理解WPF数据绑定机制与自定义筛选交互设计。1. 这不是加个下拉框就完事的“Excel筛选”——WPF DataGrid 真实筛选链路必须打通三层视图隔离很多开发者在 WPF 中第一次尝试给 DataGrid 加 Excel 风格筛选时会直接在列头放个 Button点开弹个 ComboBox选完条件就ItemsSource filteredList。结果很快发现排序失效、分组丢失、滚动位置重置、多列筛选逻辑错乱甚至 ObservableCollection 的CollectionChanged事件被反复触发导致 UI 卡顿。根本原因在于——WPF 的 DataGrid 不是直接渲染数据源而是通过 ICollectionView 做中间抽象层。.NET 6.0 虽优化了 DataGrid 渲染性能但并未改变这一核心契约。真正的仿 Excel 筛选必须在ICollectionView层拦截并重写Filter委托同时确保SortDescriptions和GroupDescriptions与筛选逻辑共存否则你做的只是“假筛选”用户拖动滚动条或点击列头排序时UI 就会瞬间打回原形。本实例基于 .NET 6.0 VS2022 构建不依赖第三方控件库全程使用原生 WPF 数据绑定机制重点解决FilterInfo.cs如何动态构建表达式树、MainWindowViewModel.cs怎样维护多列 AND 关系筛选状态、以及DataGridTextColumn的CanUserSort与Filtering事件如何协同避免事件冲突——这才是企业级数据桌面应用中可落地、可维护、可扩展的筛选实现路径。2. 从 ICollectionView.Filter 入手为什么必须绕过 ItemsSource 直接操作视图2.1 理解 WPF 数据绑定的三层结构数据源 → 视图 → 控件WPF 的数据绑定并非“数据源直连 UI”而是一个明确分层模型数据源层ObservableCollectionStudent或DataTable负责存储原始数据和通知变更视图层ICollectionView由CollectionViewSource.GetDefaultView()创建默认提供排序、分组、筛选能力且同一数据源可存在多个独立视图控件层DataGrid仅绑定到某个ICollectionView实例所有交互排序、滚动、编辑均作用于该视图而非原始集合。提示若直接修改ItemsSource如dataGrid.ItemsSource new ObservableCollectionStudent(filtered)等于切断了 DataGrid 与原始视图的连接导致ICollectionView.Refresh()失效、SortDescriptions清空、CurrentItem丢失。这是新手最常踩的深坑。2.2 实现 Filter 委托用 ExpressionTree 动态构建类型安全的筛选逻辑ICollectionView.Filter是一个Predicateobject委托接收每一行数据object类型返回true表示保留。硬编码if (item is Student s s.Name.Contains(张))无法支持运行时列名和条件变化。本实例采用ExpressionTree动态编译关键代码如下// FilterInfo.cs 中的核心方法 public static Funcobject, bool BuildFilterExpressionT(string propertyName, string condition, object value) { var param Expression.Parameter(typeof(object), item); var castExpr Expression.Convert(param, typeof(T)); var propertyExpr Expression.Property(castExpr, propertyName); // 根据 condition 类型生成不同表达式Contains / Equals / StartsWith / GreaterThan 等 Expression body; switch (condition) { case Contains: var containsMethod typeof(string).GetMethod(Contains, new[] { typeof(string) }); body Expression.Call(propertyExpr, containsMethod, Expression.Constant(value?.ToString())); break; case Equals: body Expression.Equal(propertyExpr, Expression.Constant(value)); break; case GreaterThan: body Expression.GreaterThan(propertyExpr, Expression.Constant(value)); break; default: body Expression.Constant(true); // 默认不过滤 break; } var lambda Expression.LambdaFuncobject, bool(body, param); return lambda.Compile(); }逻辑说明BuildFilterExpressionStudent(Name, Contains, 张)返回一个可执行委托该委托在ICollectionView.Filter中被调用时自动将object安全转换为Student再反射获取Name属性值最后调用string.Contains()。参数说明propertyName必须与Student.cs中属性名完全一致区分大小写condition是预定义枚举字符串避免拼写错误value支持string/int/DateTime等基础类型Expression.Constant()会自动处理装箱。2.3 在 ViewModel 中聚合多列筛选AND 逻辑的线性组合与状态持久化单列筛选容易但真实业务场景要求“姓名包含张 AND 年龄大于25 AND 状态在职”。ICollectionView.Filter只接受一个Predicateobject因此需将多个条件合并为一个闭包。MainWindowViewModel.cs中的关键设计private readonly ObservableCollectionFilterInfo _activeFilters new(); private ICollectionView _collectionView; public void ApplyFilters() { if (_collectionView null) return; // 构建复合 Filter 委托所有 activeFilters 的条件 AND 组合 _collectionView.Filter item { foreach (var filter in _activeFilters) { var filterFunc filter.BuildFilterDelegate(item.GetType()); if (!filterFunc(item)) return false; // 任一条件不满足即排除 } return true; }; _collectionView.Refresh(); // 强制刷新视图触发 UI 重绘 } // 添加筛选条件由列头按钮触发 public void AddFilter(string propertyName, string condition, object value) { var existing _activeFilters.FirstOrDefault(f f.PropertyName propertyName); if (existing ! null) _activeFilters.Remove(existing); _activeFilters.Add(new FilterInfo { PropertyName propertyName, Condition condition, Value value }); ApplyFilters(); } // 清除某列筛选 public void ClearFilter(string propertyName) { _activeFilters.RemoveAll(f f.PropertyName propertyName); ApplyFilters(); }参数说明_activeFilters是ObservableCollectionFilterInfo支持 UI 绑定显示当前激活的筛选项如“姓名: 包含 张”ApplyFilters()每次重建Filter委托确保逻辑最新_collectionView.Refresh()是唯一正确触发 UI 更新的方式不可省略。注意此设计天然支持“清除单列”和“全部清除”且状态保存在 ViewModel 内存中可轻松扩展为 JSON 序列化到本地文件。2.4 防止筛选与排序冲突禁用 AutoGenerateColumns 后的手动列配置AutoGenerateColumnsTrue会自动生成列并绑定SortMemberPath但一旦手动添加Filtering事件排序点击会触发两次Sorting事件一次来自 DataGrid 内部一次来自自定义逻辑导致ICollectionView.SortDescriptions被错误覆盖。解决方案是彻底禁用自动生成并显式配置每列!-- MainWindow.xaml -- DataGrid x:NamedataGrid ItemsSource{Binding View} AutoGenerateColumnsFalse CanUserSortTrue SelectionModeSingle DataGrid.Columns DataGridTextColumn Header姓名 Binding{Binding Name} SortMemberPathName Width120/ DataGridTextColumn Header年龄 Binding{Binding Age} SortMemberPathAge Width80/ DataGridTextColumn Header入职日期 Binding{Binding JoinDate, StringFormatyyyy-MM-dd} SortMemberPathJoinDate Width120/ !-- 注意此处不设置 Filtering 事件事件绑定在后台代码中 -- /DataGrid.Columns /DataGrid逻辑说明SortMemberPath显式指定排序依据属性确保ICollectionView.SortDescriptions正确更新CanUserSortTrue保持列头排序箭头可用Filtering事件不在 XAML 中声明而是在MainWindow.xaml.cs的Loaded事件中为每列动态附加避免 XAML 解析时DataGridTextColumn实例未完全初始化导致的 NullReferenceException。3. 列头筛选菜单的实现用 ControlTemplate 替换 DataGridColumnHeader3.1 为什么不能只加个 Button——DataGridColumnHeader 的模板限制DataGridColumnHeader默认模板仅包含文本和排序箭头其ContentTemplate不支持直接嵌入Popup或复杂布局。若强行在Header属性中塞入StackPanel会导致列宽计算异常、鼠标悬停样式丢失、排序箭头错位。正确做法是重写整个ControlTemplate注入筛选触发器。3.2 自定义 ColumnHeaderTemplate内联 Popup 筛选条件面板在MainWindow.xaml的Window.Resources中定义Style x:KeyFilterableColumnHeaderStyle TargetType{x:Type DataGridColumnHeader} Setter PropertyTemplate Setter.Value ControlTemplate TargetType{x:Type DataGridColumnHeader} Grid Background{TemplateBinding Background} Grid.ColumnDefinitions ColumnDefinition Width*/ ColumnDefinition WidthAuto/ /Grid.ColumnDefinitions !-- 列标题文本 -- ContentPresenter Grid.Column0 VerticalAlignmentCenter Margin5,0,0,0/ !-- 筛选下拉按钮 -- ToggleButton x:NameFilterButton Grid.Column1 Width20 Height20 Margin0,0,5,0 Content▼ IsChecked{Binding RelativeSource{RelativeSource TemplatedParent}, PathTag, ModeTwoWay}/ !-- 筛选弹出层 -- Popup x:NameFilterPopup PlacementBottom PlacementTarget{Binding ElementNameFilterButton} StaysOpenFalse AllowsTransparencyTrue PopupAnimationFade Border BackgroundWhite BorderBrush#CCCCCC BorderThickness1 CornerRadius3 Padding5 StackPanel MinWidth180 TextBlock Text筛选条件 FontWeightBold Margin0,0,0,5/ ComboBox x:NameConditionCombo ItemsSource{Binding DataContext.FilterConditions, RelativeSource{RelativeSource AncestorTypeWindow}} SelectedItem{Binding DataContext.SelectedCondition, RelativeSource{RelativeSource AncestorTypeWindow}, ModeTwoWay} Margin0,0,0,5/ TextBox x:NameValueBox Text{Binding DataContext.FilterValue, RelativeSource{RelativeSource AncestorTypeWindow}, ModeTwoWay} Margin0,0,0,5/ StackPanel OrientationHorizontal HorizontalAlignmentRight Button Content确定 ClickOnFilterApplyClick Margin0,0,5,0 Width60/ Button Content取消 ClickOnFilterCancelClick Width60/ /StackPanel /StackPanel /Border /Popup /Grid ControlTemplate.Triggers Trigger PropertyIsMouseOver ValueTrue Setter PropertyBackground Value#F0F0F0/ /Trigger Trigger SourceNameFilterButton PropertyIsChecked ValueTrue Setter TargetNameFilterPopup PropertyIsOpen ValueTrue/ /Trigger /ControlTemplate.Triggers /ControlTemplate /Setter.Value /Setter /Style逻辑说明ToggleButton作为筛选开关IsChecked绑定到DataGridColumnHeader.Tag用于临时存储列名Popup使用StaysOpenFalse确保点击外部自动关闭ComboBox和TextBox的DataContext通过RelativeSource回溯到Window从而绑定MainWindowViewModel中的筛选配置ControlTemplate.Triggers实现悬停变色和按钮联动弹窗。3.3 在后台代码中关联列与筛选逻辑动态注入列名和事件MainWindow.xaml.cs中在dataGrid_Loaded事件里遍历列并设置HeaderStyle和Tagprivate void DataGrid_Loaded(object sender, RoutedEventArgs e) { foreach (var column in dataGrid.Columns) { // 为每列设置自定义 Header 模板 column.HeaderStyle (Style)FindResource(FilterableColumnHeaderStyle); // 将列绑定的属性名存入 Tag供 Popup 内按钮使用 if (column is DataGridBoundColumn boundColumn boundColumn.Binding is Binding binding) { column.HeaderStyle.Setters.Add(new Setter(DataGridColumnHeader.TagProperty, binding.Path.Path)); } } } private void OnFilterApplyClick(object sender, RoutedEventArgs e) { var header VisualTreeHelper.GetParent(sender as DependencyObject) as DataGridColumnHeader; if (header?.Tag is string propertyName this.DataContext is MainWindowViewModel vm !string.IsNullOrEmpty(vm.SelectedCondition)) { vm.AddFilter(propertyName, vm.SelectedCondition, vm.FilterValue); // 关闭 Popup var popup FindVisualChildPopup(header); if (popup ! null) popup.IsOpen false; } } // 辅助方法在视觉树中查找子元素 private static T FindVisualChildT(DependencyObject parent) where T : DependencyObject { if (parent null) return null; for (int i 0; i VisualTreeHelper.GetChildrenCount(parent); i) { var child VisualTreeHelper.GetChild(parent, i); if (child is T result) return result; var found FindVisualChildT(child); if (found ! null) return found; } return null; }参数说明binding.Path.Path获取Binding{Binding Name}中的Name字符串存入TagOnFilterApplyClick通过VisualTreeHelper向上查找DataGridColumnHeader再读取Tag得到列名FindVisualChild是 WPF 视觉树遍历的标准写法避免LogicalTreeHelper在模板中失效的问题。4. 筛选状态可视化与调试技巧让隐藏的 Filter 委托“看得见”4.1 在 UI 上显示当前激活的筛选条件用户需要明确知道“我现在筛了什么”。在MainWindow.xaml中添加筛选状态栏StatusBar Grid.Row1 Height25 StatusBarItem TextBlock Text{Binding ActiveFilterCount, StringFormat已启用 {0} 个筛选}/ /StatusBarItem Separator/ StatusBarItem ItemsControl ItemsSource{Binding ActiveFilters} ItemsControl.ItemTemplate DataTemplate TextBlock Text{Binding DisplayText} Margin5,0,5,0/ /DataTemplate /ItemsControl.ItemTemplate /ItemsControl /StatusBarItem /StatusBar对应MainWindowViewModel.cs中的属性private int _activeFilterCount; public int ActiveFilterCount { get _activeFilterCount; private set SetProperty(ref _activeFilterCount, value); } private ObservableCollectionstring _activeFilterDisplays new(); public ObservableCollectionstring ActiveFilters _activeFilterDisplays; // 在 AddFilter/ClearFilter 后更新显示 private void UpdateFilterDisplay() { _activeFilterDisplays.Clear(); foreach (var filter in _activeFilters) { _activeFilterDisplays.Add(${filter.PropertyName}: {filter.Condition} {filter.Value}); } ActiveFilterCount _activeFilters.Count; }逻辑说明ActiveFilters是ObservableCollectionstring直接绑定到ItemsControl每添加一个筛选条件状态栏实时追加一行文本DisplayText是FilterInfo中的只读属性格式化输出如“姓名: 包含 张三”。4.2 调试 ICollectionView.Filter 的黄金三步法当筛选不生效或逻辑错误时按以下顺序排查步骤操作预期结果说明1. 检查 Filter 是否被赋值在ApplyFilters()中设断点查看_collectionView.Filter是否为null非 null且委托地址可见若为 null说明ICollectionView初始化失败检查ItemsSource是否在InitializeComponent()后才赋值2. 验证 Filter 委托执行逻辑在Filter委托内部加日志Debug.WriteLine($Filtering {item} - {result});控制台输出每行数据的判断过程可快速定位是表达式编译错误如属性名拼错、还是值类型转换失败如int与string比较3. 确认 Refresh 是否触发在_collectionView.Refresh()后立即检查dataGrid.Items.Count数值应等于筛选后数据量若数量不变说明Filter委托始终返回true或Refresh()调用时机错误如在INotifyPropertyChanged通知前调用4.3 性能优化大数据量下的筛选延迟处理当ObservableCollectionStudent超过 10,000 行时每次Refresh()触发的全量Filter委托执行会明显卡顿。本实例采用防抖Debounce策略private DispatcherTimer _debounceTimer; private void InitializeDebounceTimer() { _debounceTimer new DispatcherTimer { Interval TimeSpan.FromMilliseconds(300) }; _debounceTimer.Tick (s, e) { _debounceTimer.Stop(); ApplyFilters(); // 延迟执行 }; } public void ScheduleFilterApply() // 替代直接调用 ApplyFilters() { _debounceTimer.Stop(); _debounceTimer.Start(); }逻辑说明用户在TextBox中连续输入时ScheduleFilterApply()被高频调用但_debounceTimer确保ApplyFilters()最多每 300ms 执行一次避免 UI 频繁重绘。此方案比Task.Run更安全因ICollectionView必须在 UI 线程操作。5. 进阶技巧支持 Excel 风格的“文本筛选”与“数字筛选”上下文菜单5.1 根据列数据类型动态切换筛选面板Excel 中文本列显示“文本筛选”包含、开头是、结尾是数字列显示“数字筛选”大于、介于、不等于。本实例通过反射获取属性类型实现自动适配// 在 FilterInfo.cs 中扩展 public static Liststring GetAvailableConditions(Type propertyType) { if (propertyType typeof(string)) return new Liststring { Equals, Contains, StartsWith, EndsWith, NotEquals }; if (propertyType typeof(int) || propertyType typeof(double) || propertyType typeof(DateTime)) return new Liststring { Equals, GreaterThan, LessThan, Between, NotEquals }; return new Liststring { Equals, NotEquals }; } // 在 MainWindowViewModel.cs 中 public ObservableCollectionstring FilterConditions { get { if (string.IsNullOrEmpty(_currentPropertyName)) return new ObservableCollectionstring(); var prop typeof(Student).GetProperty(_currentPropertyName); return new ObservableCollectionstring(GetAvailableConditions(prop?.PropertyType ?? typeof(string))); } }逻辑说明GetAvailableConditions()根据Student.cs中属性的实际类型typeof(string)/typeof(int)返回不同的条件列表FilterConditions是ObservableCollectionstring绑定到ComboBox实现“选中姓名列 → 下拉显示文本条件选中年龄列 → 下拉显示数字条件”。5.2 “介于”条件的双值输入实现Between条件需两个值最小值、最大值原TextBox无法满足。扩展FilterPopup模板根据SelectedCondition动态切换内容!-- 在 Popup 的 StackPanel 内 -- ContentControl Content{Binding} ContentControl.Style Style TargetTypeContentControl Style.Triggers DataTrigger Binding{Binding DataContext.SelectedCondition, RelativeSource{RelativeSource AncestorTypeWindow}} ValueBetween Setter PropertyContentTemplate Setter.Value DataTemplate StackPanel TextBlock Text最小值 Margin0,0,0,2/ TextBox Text{Binding DataContext.MinValue, RelativeSource{RelativeSource AncestorTypeWindow}, ModeTwoWay} Margin0,0,0,5/ TextBlock Text最大值 Margin0,0,0,2/ TextBox Text{Binding DataContext.MaxValue, RelativeSource{RelativeSource AncestorTypeWindow}, ModeTwoWay} Margin0,0,0,5/ /StackPanel /DataTemplate /Setter.Value /Setter /DataTrigger DataTrigger Binding{Binding DataContext.SelectedCondition, RelativeSource{RelativeSource AncestorTypeWindow}} ValueContains Setter PropertyContentTemplate Setter.Value DataTemplate TextBox Text{Binding DataContext.FilterValue, RelativeSource{RelativeSource AncestorTypeWindow}, ModeTwoWay} Margin0,0,0,5/ /DataTemplate /Setter.Value /Setter /DataTrigger /Style.Triggers /Style /ContentControl.Style /ContentControl参数说明DataTrigger监听SelectedCondition变化当值为Between时ContentControl自动切换为双TextBox模板MinValue/MaxValue是MainWindowViewModel中新增的string属性BuildFilterExpression方法需扩展支持Between条件的Expression.AndAlso()组合。最终BuildFilterExpression对Between的处理逻辑为case Between: var minExpr Expression.GreaterThanOrEqual(propertyExpr, Expression.Constant(minValue)); var maxExpr Expression.LessThanOrEqual(propertyExpr, Expression.Constant(maxValue)); body Expression.AndAlso(minExpr, maxExpr); break;此设计使筛选面板真正具备 Excel 的智能上下文感知能力无需为每种类型写死界面大幅降低后续维护成本。本文还有配套的精品资源点击获取