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
相关产品推荐
相关产品推荐

