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

XAML:如何编写嵌套CollectionViewSource实现树形数据多级过滤

实现TreeView嵌套层级的过滤功能(基于CollectionViewSource)

我刚好做过类似的需求,嵌套CollectionViewSource确实是实现TreeView层级过滤的完美方案,尤其是要支持N级任意元素搜索的场景。下面我一步步给你拆解实现思路和完整代码,你可以直接套用修改。

第一步:先定义你的数据模型

首先得明确你的组和元素结构,我们先写两个基础模型类,都实现INotifyPropertyChanged,这样过滤更新的时候UI能自动同步:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class BaseModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

// 组模型
public class Group : BaseModel
{
    private string _name;
    public string Name
    {
        get => _name;
        set { _name = value; OnPropertyChanged(); }
    }

    private ObservableCollection<BaseModel> _items;
    public ObservableCollection<BaseModel> Items
    {
        get => _items;
        set { _items = value; OnPropertyChanged(); }
    }

    // 每个组对应的子级CollectionViewSource,用于过滤子元素
    public ICollectionView ItemsView { get; set; }

    public Group()
    {
        Items = new ObservableCollection<BaseModel>();
        ItemsView = CollectionViewSource.GetDefaultView(Items);
    }
}

// 元素模型
public class Item : BaseModel
{
    private string _name;
    public string Name
    {
        get => _name;
        set { _name = value; OnPropertyChanged(); }
    }
}

第二步:实现过滤逻辑的ViewModel

接下来我们需要一个ViewModel(推荐用MVVM模式,也可以直接在窗口后台代码处理),核心是维护搜索文本,并且给所有层级的CollectionViewSource设置过滤规则:

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;

public class MainViewModel : BaseModel
{
    private string _searchText;
    public string SearchText
    {
        get => _searchText;
        set 
        { 
            _searchText = value; 
            OnPropertyChanged();
            // 搜索文本变化时,重新过滤所有层级
            RefreshAllFilters();
        }
    }

    // 顶层组集合
    public ObservableCollection<Group> Groups { get; set; }
    // 顶层组对应的CollectionViewSource
    public ICollectionView GroupsView { get; set; }

    public MainViewModel()
    {
        Groups = new ObservableCollection<Group>();
        GroupsView = CollectionViewSource.GetDefaultView(Groups);
        // 设置顶层组的过滤规则
        GroupsView.Filter = FilterGroup;

        // 模拟测试数据(替换成你自己的数据源即可)
        LoadTestData();
    }

    // 加载测试数据
    private void LoadTestData()
    {
        var group1 = new Group { Name = "一级组A" };
        group1.Items.Add(new Item { Name = "元素A1" });
        group1.Items.Add(new Group { Name = "二级组A1", Items = { new Item { Name = "元素A1-1" }, new Item { Name = "元素A1-2" } } });
        
        var group2 = new Group { Name = "一级组B" };
        group2.Items.Add(new Item { Name = "元素B1" });
        group2.Items.Add(new Group { Name = "二级组B1", Items = { new Group { Name = "三级组B1-1", Items = { new Item { Name = "元素B1-1-1" } } } } });
        
        Groups.Add(group1);
        Groups.Add(group2);
    }

    // 顶层组的过滤逻辑
    private bool FilterGroup(object item)
    {
        if (string.IsNullOrEmpty(SearchText)) return true;

        var group = item as Group;
        if (group == null) return false;

        // 规则:组名称匹配,或者子元素中有匹配的(递归检查)
        return group.Name.IndexOf(SearchText, StringComparison.OrdinalIgnoreCase) >= 0 
               || HasMatchingChild(group.Items, SearchText);
    }

    // 子元素的过滤逻辑(递归处理所有层级)
    private bool FilterItem(object item)
    {
        if (string.IsNullOrEmpty(SearchText)) return true;

        if (item is Item model)
        {
            return model.Name.IndexOf(SearchText, StringComparison.OrdinalIgnoreCase) >= 0;
        }
        else if (item is Group group)
        {
            // 组的过滤规则和顶层一致:自身匹配或子元素匹配
            return group.Name.IndexOf(SearchText, StringComparison.OrdinalIgnoreCase) >= 0 
                   || HasMatchingChild(group.Items, SearchText);
        }
        return false;
    }

    // 递归检查子元素是否有匹配项
    private bool HasMatchingChild(IEnumerable<BaseModel> items, string searchText)
    {
        foreach (var child in items)
        {
            if (child is Item item && item.Name.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0)
            {
                return true;
            }
            else if (child is Group group)
            {
                if (group.Name.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0 
                    || HasMatchingChild(group.Items, searchText))
                {
                    return true;
                }
            }
        }
        return false;
    }

    // 刷新所有层级的过滤
    private void RefreshAllFilters()
    {
        GroupsView.Refresh();
        // 递归刷新所有子组的过滤
        foreach (var group in Groups)
        {
            RefreshChildFilters(group);
        }
    }

    // 递归刷新子组的过滤
    private void RefreshChildFilters(Group group)
    {
        group.ItemsView.Filter = FilterItem;
        group.ItemsView.Refresh();

        foreach (var child in group.Items.OfType<Group>())
        {
            RefreshChildFilters(child);
        }
    }
}

第三步:XAML绑定实现TreeView和搜索框

最后在XAML里绑定ViewModel,并且设置TreeView的层级模板,让每个子组也绑定对应的ItemsView:

<Window x:Class="TreeViewFilterDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TreeViewFilterDemo"
        Title="TreeView过滤演示" Height="450" Width="800">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!-- 搜索框 -->
        <TextBox Grid.Row="0" Margin="10" 
                 Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}"
                 PlaceholderText="输入关键词搜索..."/>

        <!-- 带过滤的TreeView -->
        <TreeView Grid.Row="1" Margin="10" ItemsSource="{Binding GroupsView}">
            <TreeView.Resources>
                <!-- 元素模板 -->
                <DataTemplate DataType="{x:Type local:Item}">
                    <TextBlock Text="{Binding Name}"/>
                </DataTemplate>

                <!-- 组模板 -->
                <HierarchicalDataTemplate DataType="{x:Type local:Group}"
                                          ItemsSource="{Binding ItemsView}">
                    <TextBlock Text="{Binding Name}" FontWeight="Bold"/>
                </HierarchicalDataTemplate>
            </TreeView.Resources>
        </TreeView>
    </Grid>
</Window>

关键注意点

  • 递归过滤:一定要递归检查子节点,否则如果子元素匹配但父节点不匹配,用户看不到结果;同时父节点如果有匹配的子节点,也要显示父节点。
  • UpdateSourceTrigger=PropertyChanged:搜索框绑定的时候加上这个,确保输入字符时实时触发过滤。
  • 每个层级的ICollectionView:每个组的Items都要对应一个ICollectionView,这样才能单独过滤子级内容。

这样整个实现就能支持N级任意元素的搜索过滤了,你可以根据自己的实际模型修改属性名称和过滤规则~

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 04:07:51