如何创建首行带筛选的DataGrid?适配多模型业务场景
实现适配多模型的DataGrid首行筛选功能
Great question! I've tackled similar multi-model DataGrid filtering scenarios before, and here's a step-by-step solution that adds a top-row filter while keeping your existing model-switching functionality intact, plus optimizations for large datasets.
1. 构建多模型兼容的筛选ViewModel基类
因为你要处理不同模型(Car、Person)及对应的视图模型,我们需要一个可复用的基类来标准化筛选逻辑。每个具体的筛选ViewModel将继承这个基类,处理自身属性的筛选:
using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Data; public class FilterableViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } // 在派生类中重写此方法,实现模型专属的筛选逻辑 public virtual ICollectionView ApplyFilter(ICollectionView source) { source.Filter = _ => true; return source; } } // 示例:Car数据的筛选ViewModel public class CarFilterViewModel : FilterableViewModelBase { private string _makeFilter; public string MakeFilter { get => _makeFilter; set { _makeFilter = value; OnPropertyChanged(); } } private int? _yearFilter; public int? YearFilter { get => _yearFilter; set { _yearFilter = value; OnPropertyChanged(); } } public override ICollectionView ApplyFilter(ICollectionView source) { source.Filter = item => { if (item is Car car) { bool matchesMake = string.IsNullOrEmpty(MakeFilter) || car.Make.IndexOf(MakeFilter, StringComparison.OrdinalIgnoreCase) >= 0; bool matchesYear = !YearFilter.HasValue || car.Year == YearFilter.Value; return matchesMake && matchesYear; } return false; }; return source; } } // 为Person创建类似的筛选ViewModel,添加Name/Age/Email等筛选属性
2. 更新主ViewModel管理筛选逻辑
修改主ViewModel,跟踪当前的筛选ViewModel,并在用户切换模型或调整筛选条件时更新过滤后的数据:
using System.Collections.ObjectModel; using System.Windows.Data; using System.Windows.Input; public class MainViewModel : INotifyPropertyChanged { private ObservableCollection<object> _items; public ObservableCollection<object> Items { get => _items; set { _items = value; OnPropertyChanged(); UpdateFilteredItems(); } } private FilterableViewModelBase _currentFilterViewModel; public FilterableViewModelBase CurrentFilterViewModel { get => _currentFilterViewModel; set { if (_currentFilterViewModel != null) _currentFilterViewModel.PropertyChanged -= OnFilterPropertyChanged; _currentFilterViewModel = value; if (_currentFilterViewModel != null) _currentFilterViewModel.PropertyChanged += OnFilterPropertyChanged; OnPropertyChanged(); UpdateFilteredItems(); } } private ICollectionView _filteredItems; public ICollectionView FilteredItems { get => _filteredItems; set { _filteredItems = value; OnPropertyChanged(); } } public ICommand SwitchToCarCommand { get; } public ICommand SwitchToPersonCommand { get; } public MainViewModel() { SwitchToCarCommand = new RelayCommand(() => { Items = new ObservableCollection<object>(GetSampleCarData()); CurrentFilterViewModel = new CarFilterViewModel(); }); SwitchToPersonCommand = new RelayCommand(() => { Items = new ObservableCollection<object>(GetSamplePersonData()); CurrentFilterViewModel = new PersonFilterViewModel(); }); // 默认加载Car视图 SwitchToCarCommand.Execute(null); } private void OnFilterPropertyChanged(object sender, PropertyChangedEventArgs e) { UpdateFilteredItems(); } private void UpdateFilteredItems() { if (Items == null || CurrentFilterViewModel == null) return; var view = CollectionViewSource.GetDefaultView(Items); FilteredItems = CurrentFilterViewModel.ApplyFilter(view); } // 模拟数据方法 - 替换为你的实际数据加载逻辑 private List<Car> GetSampleCarData() => new() { new Car { Make = "Toyota", Model = "Camry", Year = 2020 }, new Car { Make = "Honda", Model = "Accord", Year = 2021 }, // 添加更多示例数据 }; private List<Person> GetSamplePersonData() => new() { new Person { Name = "John Doe", Age = 30, Email = "john@example.com" }, new Person { Name = "Jane Smith", Age = 28, Email = "jane@example.com" }, // 添加更多示例数据 }; } // 简单的RelayCommand实现(如果没有现成的) public class RelayCommand : ICommand { private readonly Action _execute; private readonly Func<bool> _canExecute; public event EventHandler CanExecuteChanged; public RelayCommand(Action execute, Func<bool> canExecute = null) { _execute = execute ?? throw new ArgumentNullException(nameof(execute)); _canExecute = canExecute; } public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true; public void Execute(object parameter) => _execute(); public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); }
3. 自定义DataGrid模板添加筛选行
我们将修改DataGrid的控件模板,在列标题下方插入筛选行。这一行会根据当前筛选ViewModel和自动生成的列动态生成控件:
<DataGrid x:Name="MainDataGrid" ItemsSource="{Binding FilteredItems}" AutoGenerateColumns="True" AutoGeneratingColumn="MainDataGrid_AutoGeneratingColumn" VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling"> <DataGrid.Template> <ControlTemplate TargetType="{x:Type DataGrid}"> <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True"> <ScrollViewer x:Name="DG_ScrollViewer" Focusable="false"> <ScrollViewer.Template> <ControlTemplate TargetType="{x:Type ScrollViewer}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <!-- 筛选行 --> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <!-- 列标题 --> <DataGridColumnHeadersPresenter x:Name="PART_ColumnHeadersPresenter" Grid.Column="1" Visibility="{Binding HeadersVisibility, ConverterParameter={x:Static DataGridHeadersVisibility.Column}, Converter={x:Static DataGrid.HeadersVisibilityConverter}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/> <!-- 筛选行容器 --> <Grid Grid.Column="1" Grid.Row="1" Margin="0,2,0,2"> <ItemsControl ItemsSource="{Binding Columns, ElementName=MainDataGrid}"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal"/> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate> <ContentControl Width="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType={x:Type DataGridColumn}}}" Content="{Binding DataContext.CurrentFilterViewModel, ElementName=MainDataGrid}"> <ContentControl.ContentTemplate> <DataTemplate> <!-- 默认筛选控件:用于字符串/数字输入的TextBox --> <TextBox Text="{Binding Path={Binding Binding.Path.Path, RelativeSource={RelativeSource AncestorType={x:Type ContentControl}}}, UpdateSourceTrigger=PropertyChanged, Delay=500}" Margin="1" Padding="2"/> <!-- 对于数值类型,可替换为NumericUpDown控件 --> <!-- 对于枚举类型,可替换为绑定枚举值的ComboBox --> </DataTemplate> </ContentControl.ContentTemplate> </ContentControl> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Grid> <!-- DataGrid内容区域 --> <ScrollContentPresenter x:Name="PART_ScrollContentPresenter" CanContentScroll="{TemplateBinding CanContentScroll}" Grid.Column="1" Grid.Row="2"/> <ScrollBar x:Name="PART_VerticalScrollBar" Grid.Column="2" Maximum="{TemplateBinding ScrollableHeight}" Orientation="Vertical" Grid.Row="1" Grid.RowSpan="2" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportHeight}"/> <ScrollBar x:Name="PART_HorizontalScrollBar" Grid.Column="1" Maximum="{TemplateBinding ScrollableWidth}" Orientation="Horizontal" Grid.Row="3" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ViewportSize="{TemplateBinding ViewportWidth}"/> <Rectangle Grid.Column="1" Fill="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" Grid.Row="3"/> <Rectangle Grid.Column="2" Fill="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" Grid.Row="1" Grid.RowSpan="2"/> </Grid> </ControlTemplate> </ScrollViewer.Template> <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> </ScrollViewer> </Border> </ControlTemplate> </DataGrid.Template> </DataGrid>
4. 根据列数据类型优化筛选控件
使用AutoGeneratingColumn事件为特定数据类型(如数字或枚举)调整筛选控件:
private void MainDataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e) { var boundColumn = e.Column as DataGridBoundColumn; if (boundColumn == null) return; var propertyType = e.PropertyType; // 对于整数属性,可将筛选控件切换为NumericUpDown if (propertyType == typeof(int) || propertyType == typeof(int?)) { // 你需要修改XAML模板,基于属性类型使用DataTrigger // 或者将类型存储在列的附加属性中 } // 对于枚举属性,使用绑定枚举值的ComboBox else if (propertyType.IsEnum) { // 类似逻辑:为枚举调整筛选控件模板 } }
5. 大数据量下的性能优化
既然你提到了大数据量的用户体验问题,务必实现以下优化:
- 启用虚拟化:我们已经在DataGrid中添加了
VirtualizingStackPanel.IsVirtualizing="True"和VirtualizationMode="Recycling",只渲染可见行。 - 延迟筛选更新:TextBox绑定中的
Delay=500避免了每次按键都触发筛选,减少不必要的UI更新。 - 使用ICollectionView:这避免了每次筛选都创建新集合——我们直接在现有视图上进行筛选。
这套方案可以让用户无缝切换Car和Person视图,同时拥有适配当前模型属性的专属筛选行,即使在大数据量下也能保持流畅的性能。
内容的提问来源于stack exchange,提问作者Oleksiy Kovalchyk




