31

I have a hypothetical tree view that contains this data:

RootNode
   Leaf
   vein
SecondRoot
   seeds
   flowers

I am trying to filter the nodes in order to show only the nodes that contain a certain text. Say if I specify "L", the tree will be filtered and show only RootNode->Leaf and SecondRoot->flowers (because they both contain the letter L).

Following the m-v-vm pattern, I have a basic TreeViewViewModel class like this:

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);
        }
    }
    ...
}

And a basic 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 ... 
    } 
    ...
}

Everything is setup in the xaml so I see the treeview and search box.

When this code is exercised, the filter only applies to the Root nodes which is insufficient. Is there a way to make the filter trickle down in the hierarchy of nodes so that my predicate is called for every node ? In other words, can the filter be applied to the TreeView as a whole ?

David
  • 9,635
  • 5
  • 62
  • 68
  • 3
    What did you end up doing? Any performance info you can relay or other solution? – Aaron McIver Dec 31 '10 at 21:52
  • I added an answer showing how to apply the filter predicate you applied only to the top level nodes in your example to the entire hierarchy. – henon Nov 20 '22 at 13:27

7 Answers7

10

This is how I filtered the items on my TreeView:

I have the class:

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;
         }             
    }
}

Then I could filter results as:

// 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");
default
  • 11,485
  • 9
  • 66
  • 102
Tono Nam
  • 34,064
  • 78
  • 298
  • 470
5

The only way I've found to do this (which is a bit of a hack), is to create a ValueConverter that converts from IList to IEnumerable. in ConvertTo(), return a new CollectionViewSource from the passed in IList.

If there's a better way to do it, I'd love to hear it. This seems to work, though.

Andy
  • 30,088
  • 6
  • 78
  • 89
5

Unfortunately there is no way to make same Filter apply to all nodes automatically. Filter is a property (not a DP) of ItemsCollection which is not DependencyObject and so DP Value inheritance isn't there.

Each node in the tree has its own ItemsCollection which has its own Filter. The only way to make it work is to manually set them all to call the same delegate.

Simplest way would be to expose Filter property of type Predicate<object> at your ToolBoxViewModel and in its setter fire an event. Then ToolboxItemViewModel will be responsible for consuming this event and updating its Filter.

Aint pretty and I'm not sure what the performance would be like for large amounts of items in the tree.

Alex_P
  • 1,886
  • 1
  • 19
  • 22
  • There is a very simple way to apply the filter to all nodes (ok, not automatically, so your answer is still technically correct). See my answer for the code. – henon Nov 20 '22 at 13:31
4

Why do you need filters or CollectionSource? Here is a simple MVVM way to handle TreeView items.

You can make items visible, collapsed, change color, highlight, flash, whatever, simply by using 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>

I'm going to include the complete example into my WPF library:

https://www.codeproject.com/Articles/264955/WPF-MichaelAgroskin

agroskin
  • 69
  • 3
  • The performance will suffer with too many items this way, even with virtualization see https://stackoverflow.com/q/48413607/11627521. – Hossein Ebrahimi Dec 06 '22 at 10:47
2

I decided to use the treeview by Philipp Sumi mentioned here : http://www.codeproject.com/KB/WPF/versatile_treeview.aspx

And applied a filter to it as shown here : http://www.hardcodet.net/2008/02/programmatically-filtering-the-wpf-treeview

I couldn't recommend it enough :)

basarat
  • 261,912
  • 58
  • 460
  • 511
0

You can set the same filter for all TreeViewItems of the hierarchy with this extension method:

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);
        }
    }
}

With above extension method it becomes as easy as myTreeView.Filter(myPredicate); to apply the predicate to the entire hierarchy.

henon
  • 2,022
  • 21
  • 23
0

You can get the TreeViewItem for a given element in a tree using ItemContainerGenerator and once you have that you are in a position to set the Filter.

Peter Wone
  • 17,965
  • 12
  • 82
  • 134
  • That is correct, and in my opinion the best way. I provided an answer with code demonstrating it. – henon Nov 20 '22 at 13:23