You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何创建首行带筛选的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

火山引擎 最新活动