如何使用ICollectionView过滤WPF TreeView层次结构?

31

我有一个假设的树形视图,其中包含这些数据:

RootNode
   Leaf
   vein
SecondRoot
   seeds
   flowers

我试图筛选节点,以仅显示包含特定文本的节点。例如,如果我指定“L”,则会过滤树并仅显示RootNode->Leaf和SecondRoot->flowers(因为它们都包含字母L)。

遵循m-v-vm模式,我有一个基本的TreeViewViewModel类,就像这样:

public class ToolboxViewModel
{
    ...
    readonly ObservableCollection<TreeViewItemViewModel> _treeViewItems = new ObservableCollection<TreeViewItemViewModel>();
    public ObservableCollection<TreeViewItemViewModel> Headers
    {
        get { return _treeViewItems; }
    }

    private string _filterText;
    public string FilterText
    {
        get { return _filterText; }
        set
        {
            if (value == _filterText)
                return;

            _filterText = value;

            ICollectionView view = CollectionViewSource.GetDefaultView(Headers);
            view.Filter = obj => ((TreeViewItemViewModel)obj).ShowNode(_filterText);
        }
    }
    ...
}

以下是基本的 TreeViewItemViewModel:

public class ToolboxItemViewModel
{
    ...
    public string Name { get; private set; }
    public ObservableCollection<TreeViewItemViewModel> Children { get; private set; }
    public bool ShowNode(string filterText)
    {
        ... return true if filterText is contained in Name or has children that contain filterText ... 
    } 
    ...
}

一切都在xaml中设置好了,所以我看到了树形视图和搜索框。

运行此代码时,筛选器仅适用于根节点,这是不够的。 有没有办法使筛选器向节点层次结构滴落,以便我的谓词对每个节点进行调用?换句话说,筛选器是否可以应用于整个TreeView?


3
你最终做了什么?你能提供任何表现信息或其他解决方案吗? - Aaron McIver
我添加了一个答案,展示了如何将你在示例中仅应用于顶层节点的过滤谓词应用于整个层次结构。 - henon
7个回答

10

这是我如何筛选我的 TreeView 上的项目:

我有一个类:

class Node
{
    public string Name { get; set; }
    public List<Node> Children { get; set; }

    // this is the magic method!
    public Node Search(Func<Node, bool> predicate)
    {
         // if node is a leaf
         if(this.Children == null || this.Children.Count == 0)
         {
             if (predicate(this))
                return this;
             else
                return null;
         }
         else // Otherwise if node is not a leaf
         {
             var results = Children
                               .Select(i => i.Search(predicate))
                               .Where(i => i != null).ToList();

             if (results.Any()){
                var result = (Node)MemberwiseClone();
                result.Items = results;
                return result;
             }
             return null;
         }             
    }
}

那么我可以按以下方式过滤结果:

// initialize Node root
// pretend root has some children and those children have more children
// then filter the results as:
var newRootNode = root.Search(x=>x.Name == "Foo");

5
我发现唯一的方法(有点小技巧)是创建一个ValueConverter,将IList转换为IEnumerable。在ConvertTo()中,从传入的IList返回一个新的CollectionViewSource。
如果有更好的方法,请告诉我。虽然这种方法有效。

5
很遗憾,无法自动使同一过滤器适用于所有节点。过滤器是ItemsCollection的属性(不是DP),它不是DependencyObject,因此没有DP值继承。
树中的每个节点都有自己的ItemsCollection,其中包含自己的过滤器。唯一的方法是手动将它们全部设置为调用相同的委托。
最简单的方法是在ToolBoxViewModel中公开类型为Predicate<object>的Filter属性,在其setter中触发事件。然后,ToolboxItemViewModel将负责使用此事件并更新其Filter。
这不太美观,我不确定对于树中大量项目的性能会如何。

有一种非常简单的方法可以将过滤器应用于所有节点(好吧,不是自动的,所以你的答案在技术上仍然是正确的)。请查看我的答案获取代码。 - henon

5

为什么需要过滤器或CollectionSource?以下是一种简单的MVVM方式来处理TreeView中的项目。

您可以通过使用DataTriggers来使项目可见、折叠、更改颜色、突出显示、闪烁等等。

public class Item : INotifyPropertyChanged
{
    public string Title                     { get; set; } // TODO: Notify on change
    public bool VisibleSelf                 { get; set; } // TODO: Notify on change
    public bool VisibleChildOrSelf          { get; set; } // TODO: Notify on change
    public ObservableCollection<Item> Items { get; set; } // TODO: Notify on change

    public void CheckVisibility(string searchText)
    {
         VisibleSelf = // Title contains SearchText. You may use RegEx with wildcards
         VisibleChildOrSelf = VisibleSelf;

         foreach (var child in Items)
         {
             child.CheckVisibility(searchText);
             VisibleChildOrSelf |= child.VisibleChildOrSelf;
         }
    }
}

public class ViewModel : INotifyPropertyChanged
{
    public ObservableCollection<Item> Source { get; set; } // TODO: Notify on change
    public string SearchText                 { get; set; } // TODO: Notify on change

    private void OnSearchTextChanged()  // TODO: Action should be delayed by 500 millisec
    {
        foreach (var item in Source) item.CheckVisibility(SearchText);
    }
}

<StackPanel>
    <TextBox Text="{Binding SearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
                 MinWidth="200" Margin="5"/>

    <TreeView ItemsSource="{Binding Source}" Margin="5">
        <TreeView.ItemTemplate>
            <HierarchicalDataTemplate ItemsSource="{Binding Items}">
                <TextBlock Text="{Binding Title}" />
            </HierarchicalDataTemplate>
        </TreeView.ItemTemplate>
        <TreeView.ItemContainerStyle>
            <Style TargetType="Control">
                <Style.Triggers>
                    <DataTrigger Binding="{Binding VisibleChildOrSelf}" Value="false">
                        <Setter Property="Visibility" Value="Collapsed"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding VisibleSelf}" Value="false">
                        <Setter Property="Foreground" Value="Gray"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </TreeView.ItemContainerStyle>
    </TreeView>
<StackPanel>

我将把完整的示例包含在我的WPF库中: https://www.codeproject.com/Articles/264955/WPF-MichaelAgroskin

这种方式会导致性能下降,即使使用虚拟化也是如此,请参见 https://stackoverflow.com/q/48413607/11627521。 - Hossein Ebrahimi

2

0

使用ItemContainerGenerator,您可以获取树中给定元素的TreeViewItem,一旦获取到,您就可以设置过滤器。


没错,我认为这是最好的方法。我提供了一个带有代码演示的答案。 - henon

0

你可以使用这个扩展方法为层次结构中的所有TreeViewItems设置相同的过滤器:

public static class TreeViewExtensions {
    /// <summary>
    /// Applies a search filter to all items of a TreeView recursively
    /// </summary>
    public static void Filter(this TreeView self, Predicate<object> predicate)
    {
        ICollectionView view = CollectionViewSource.GetDefaultView(self.ItemsSource);
        if (view == null)
            return;
        view.Filter = predicate;
        foreach (var obj in self.Items) {
           var item = self.ItemContainerGenerator.ContainerFromItem(obj) as TreeViewItem;
           FilterRecursively(self, item, predicate);
        }
    }

    private static void FilterRecursively(TreeView tree, TreeViewItem item, Predicate<object> predicate)
    {
        ICollectionView view = CollectionViewSource.GetDefaultView(item.ItemsSource);
        if (view == null)
            return;
        view.Filter = predicate;
        foreach (var obj in item.Items) {
           var childItem = tree.ItemContainerGenerator.ContainerFromItem(obj) as TreeViewItem;
           FilterRecursively(tree, childItem, predicate);
        }
    }
}

使用上述扩展方法,将谓词应用于整个层次结构变得像这样简单:myTreeView.Filter(myPredicate);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接