You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现WPF C#中DataGrid模板列的子集合过滤功能?

WPF嵌套DataGrid子表列过滤实现方案

我在WPF C#中实现了一个员工信息DataGrid,主表为Employee,关联Contacts、Languages、Duties三个子表。通过DataGridTemplateColumn在主DataGrid内嵌套子DataGrid展示子表数据,显示效果符合预期,但针对Contacts这类模板列的过滤功能无法生效,请问该如何实现?


现有实现代码

XAML代码

<Window.Resources>
    <CollectionViewSource x:Key="CvsKey">
        <CollectionViewSource.GroupDescriptions>
            <PropertyGroupDescription PropertyName="Employee"/>
        </CollectionViewSource.GroupDescriptions>
    </CollectionViewSource>
</Window.Resources>

<Grid>
    <DataGrid AutoGenerateColumns="False" x:Name="dataGrid1" ItemsSource="{Binding Employees}" IsReadOnly="True" RowDetailsVisibilityMode="Collapsed"
              CanUserAddRows="True">
        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding EmployeeID}" Header="EmployeeID"/>
            <DataGridTextColumn Binding="{Binding FirstName}" Header="FirstName"/>
            <DataGridTextColumn Binding="{Binding LastName}" Header="LastName"/>
            <DataGridTextColumn Binding="{Binding Age}" Header="Age"/>
            <DataGridTextColumn Binding="{Binding Gender}" Header="Gender"/>
            <DataGridTextColumn Binding="{Binding Email}" Header="Email"/>
            <DataGridTemplateColumn Header="Contacts"  Width="75">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <DataGrid ItemsSource="{Binding Contacts}" IsReadOnly="True" AutoGenerateColumns="False" HeadersVisibility="None">
                            <DataGrid.Columns>
                                <DataGridTemplateColumn Width="*">
                                    <DataGridTemplateColumn.CellTemplate>
                                        <DataTemplate>
                                            <TextBlock Text="{Binding Telephone}"/>
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellTemplate>
                                </DataGridTemplateColumn>
                            </DataGrid.Columns>
                        </DataGrid>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
            <DataGridTemplateColumn Header="Duties"  Width="75">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <DataGrid ItemsSource="{Binding Duties}" IsReadOnly="True" AutoGenerateColumns="False" HeadersVisibility="None">
                            <DataGrid.Columns>
                                <DataGridTemplateColumn Width="*">
                                    <DataGridTemplateColumn.CellTemplate>
                                        <DataTemplate>
                                            <TextBlock Text="{Binding Duty}"/>
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellTemplate>
                                </DataGridTemplateColumn>
                            </DataGrid.Columns>
                        </DataGrid>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
            <DataGridTemplateColumn Header="Languages"  Width="75">
                <DataGridTemplateColumn.CellTemplate>
                    <DataTemplate>
                        <DataGrid ItemsSource="{Binding Languages}" IsReadOnly="True" AutoGenerateColumns="False" HeadersVisibility="None">
                            <DataGrid.Columns>
                                <DataGridTemplateColumn Width="*">
                                    <DataGridTemplateColumn.CellTemplate>
                                        <DataTemplate>
                                            <TextBlock Text="{Binding LanguageName}"/>
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellTemplate>
                                </DataGridTemplateColumn>
                            </DataGrid.Columns>
                        </DataGrid>
                    </DataTemplate>
                </DataGridTemplateColumn.CellTemplate>
            </DataGridTemplateColumn>
        </DataGrid.Columns>
    </DataGrid>
</Grid>

后台代码

using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Windows;
using System.Windows.Input;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        private EmployeeViewModel _viewModel;

        public MainWindow()
        {
            InitializeComponent();
            _viewModel = new EmployeeViewModel();
            DataContext = _viewModel;
        }
    }

    public class EmployeeViewModel
    {
        private readonly EmployeeDbContext _context;

        public EmployeeViewModel()
        {
            using (var context = new EmployeeDbContext())
            {
                Employees = context.Employees
                       .Include(e => e.Duties)
                    .Include(e => e.Contacts)
                    .Include(e => e.Languages)
                    .ToList();
            }
        }

        public List<Employee> Employees { get; set; } = new List<Employee>();

        public void SaveChanges()
        {
            _context.SaveChanges();
        }
    }
  
    public class EmployeeDbContext : DbContext
    {
        public DbSet<Employee> Employees { get; set; }
        public DbSet<EmployeesContact> EmployeesContacts { get; set; }
        public DbSet<EmployeesDuty> EmployeesDuties { get; set; }
        public DbSet<EmployeesLanguage> EmployeesLanguages { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Data Source=(localdb)\ProjectModels;Initial Catalog=EmployeesDB;Integrated Security=True;Connect Timeout=30;Encrypt=False;Trust Server Certificate=False;Application Intent=ReadWrite;Multi Subnet Failover=False");
        }
    }

    public class Employee
    {
        public int EmployeeID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
        public string Gender { get; set; }
        public string Email { get; set; }

        public ObservableCollection<EmployeesContact> Contacts { get; set; }
        public ObservableCollection<EmployeesDuty> Duties { get; set; }
        public ObservableCollection<EmployeesLanguage> Languages { get; set; }
    }

    public class EmployeesContact
    {
        [Key]
        public int TelephoneID { get; set; }
        public int EmployeeID { get; set; }
        public string Telephone { get; set; }
    }

    public class EmployeesDuty
    {
        public int EmployeeID { get; set; }
        [Key]
        public int EmployeeDutyID { get; set; }
        public string Duty { get; set; }
    }

    public class EmployeesLanguage
    {
        public int EmployeeID { get; set; }
        [Key]
        public int EmployeeLanguageID { get; set; }
        public string LanguageName { get; set; }
        public string TalkingAbility { get; set; }
        public string ReadingAbility { get; set; }
        public string WritingAbility { get; set; }
    }
}

解决方案

问题根源

主DataGrid的默认过滤逻辑仅作用于直接绑定的实体属性,无法自动穿透到嵌套子DataGrid绑定的集合属性。要实现子表列的过滤,需要手动处理两种场景:过滤主表仅保留包含符合条件子项的记录,或单独过滤每个子DataGrid的数据源。


场景一:过滤主表(显示包含符合条件子项的员工)

这种方式会隐藏所有不包含匹配子项的员工记录,步骤如下:

  1. 修改ViewModel实现INotifyPropertyChanged
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Data;

public class EmployeeViewModel : INotifyPropertyChanged
{
    private readonly EmployeeDbContext _context;
    private string _filterText;
    private ICollectionView _employeesView;

    public event PropertyChangedEventHandler PropertyChanged;

    public EmployeeViewModel()
    {
        using (var context = new EmployeeDbContext())
        {
            var employees = context.Employees
                   .Include(e => e.Duties)
                .Include(e => e.Contacts)
                .Include(e => e.Languages)
                .ToList();
            
            EmployeesView = CollectionViewSource.GetDefaultView(employees);
            EmployeesView.Filter = FilterEmployees;
        }
    }

    public ICollectionView EmployeesView { get; set; }

    public string FilterText
    {
        get => _filterText;
        set
        {
            _filterText = value;
            OnPropertyChanged();
            EmployeesView.Refresh();
        }
    }

    private bool FilterEmployees(object item)
    {
        if (string.IsNullOrWhiteSpace(FilterText))
            return true;
        
        var employee = item as Employee;
        if (employee == null) return false;

        // 检查Contacts是否匹配
        if (employee.Contacts?.Any(c => c.Telephone.Contains(FilterText, StringComparison.OrdinalIgnoreCase)) == true)
            return true;
        // 检查Duties是否匹配
        if (employee.Duties?.Any(d => d.Duty.Contains(FilterText, StringComparison.OrdinalIgnoreCase)) == true)
            return true;
        // 检查Languages是否匹配
        if (employee.Languages?.Any(l => l.LanguageName.Contains(FilterText, StringComparison.OrdinalIgnoreCase)) == true)
            return true;
        
        // 同时保留主表属性的过滤
        return employee.FirstName.Contains(FilterText, StringComparison.OrdinalIgnoreCase) ||
               employee.LastName.Contains(FilterText, StringComparison.OrdinalIgnoreCase) ||
               employee.Email.Contains(FilterText, StringComparison.OrdinalIgnoreCase);
    }

    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
  1. 修改XAML添加过滤输入框并更新主DataGrid绑定
    在Grid顶部添加过滤输入框:
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    
    <TextBox Grid.Row="0" Margin="10" 
             Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged}"
             PlaceholderText="输入手机号、职责或语言进行过滤"/>
    
    <DataGrid Grid.Row="1" AutoGenerateColumns="False" x:Name="dataGrid1" 
              ItemsSource="{Binding EmployeesView}" IsReadOnly="True" 
              RowDetailsVisibilityMode="Collapsed" CanUserAddRows="True">
        <!-- 原列定义保持不变 -->
    </DataGrid>
</Grid>

场景二:单独过滤子DataGrid(仅过滤子表数据,主表记录全部保留)

这种方式会保留所有员工,但仅显示每个员工子表中符合条件的项,步骤如下:

  1. 为子集合创建可过滤的视图包装类
public class FilterableCollectionViewModel<T> : INotifyPropertyChanged
{
    private string _filterText;
    private ICollectionView _collectionView;

    public event PropertyChangedEventHandler PropertyChanged;

    public FilterableCollectionViewModel(ObservableCollection<T> collection)
    {
        CollectionView = CollectionViewSource.GetDefaultView(collection);
    }

    public ICollectionView CollectionView { get; }

    public string FilterText
    {
        get => _filterText;
        set
        {
            _filterText = value;
            OnPropertyChanged();
            CollectionView.Refresh();
        }
    }

    public void SetFilter(Func<T, bool> filterFunc)
    {
        CollectionView.Filter = item => 
        {
            if (string.IsNullOrWhiteSpace(FilterText))
                return true;
            var model = item as T;
            return model != null && filterFunc(model);
        };
    }

    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
  1. 修改Employee类添加子集合的过滤视图属性
public class Employee : INotifyPropertyChanged
{
    public int EmployeeID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public string Gender { get; set; }
    public string Email { get; set; }

    private ObservableCollection<EmployeesContact> _contacts;
    public ObservableCollection<EmployeesContact> Contacts
    {
        get => _contacts;
        set
        {
            _contacts = value;
            ContactsFilterVM = new FilterableCollectionViewModel<EmployeesContact>(value);
            ContactsFilterVM.SetFilter(c => c.Telephone.Contains(ContactsFilterVM.FilterText, StringComparison.OrdinalIgnoreCase));
        }
    }

    public FilterableCollectionViewModel<EmployeesContact> ContactsFilterVM { get; private set; }

    // Duties和Languages做同样的处理
    private ObservableCollection<EmployeesDuty> _duties;
    public ObservableCollection<EmployeesDuty> Duties
    {
        get => _duties;
        set
        {
            _duties = value;
            DutiesFilterVM = new FilterableCollectionViewModel<EmployeesDuty>(value);
            DutiesFilterVM.SetFilter(d => d.Duty.Contains(DutiesFilterVM.FilterText, StringComparison.OrdinalIgnoreCase));
        }
    }

    public FilterableCollectionViewModel<EmployeesDuty> DutiesFilterVM { get; private set; }

    private ObservableCollection<EmployeesLanguage> _languages;
    public ObservableCollection<EmployeesLanguage> Languages
    {
        get => _languages;
        set
        {
            _languages = value;
            LanguagesFilterVM = new FilterableCollectionViewModel<EmployeesLanguage>(value);
            LanguagesFilterVM.SetFilter(l => l.LanguageName.Contains(LanguagesFilterVM.FilterText, StringComparison.OrdinalIgnoreCase));
        }
    }

    public FilterableCollectionViewModel<EmployeesLanguage> LanguagesFilterVM { get; private set; }

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
  1. 修改XAML子DataGrid绑定并添加子过滤输入框
    以Contacts列为例,修改模板列添加过滤输入框:
<DataGridTemplateColumn Header="Contacts"  Width="150">
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <StackPanel>
                <TextBox Margin="2" 
                         Text="{Binding ContactsFilterVM.FilterText, UpdateSourceTrigger=PropertyChanged}"
                         PlaceholderText="过滤手机号"/>
                <DataGrid ItemsSource="{Binding ContactsFilterVM.CollectionView}" 
                          IsReadOnly="True" AutoGenerateColumns="False" 
                          HeadersVisibility="None" RowHeight="20">
                    <DataGrid.Columns>
                        <DataGridTemplateColumn Width="*">
                            <DataGridTemplateColumn.CellTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding Telephone}"/>
                                </DataTemplate>
                            </DataGridTemplateColumn.CellTemplate>
                        </DataGridTemplateColumn>
                    </DataGrid.Columns>
                </DataGrid>
            </StackPanel>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

Duties和Languages列做同样的修改即可。


内容的提问来源于stack exchange,提问作者Meshka

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 14:47:02