Wednesday, April 30, 2014

Some WPF Treeview Tips/Tricks

T.StartPost();

Hey everyone/anyone reading, been a long while but I have just been quite busy with my internship and being a tools code monkey :D  It's been pretty fun, interesting and at times, VERY FRUSTRATING.

Now anyone that has done WPF has probably run into any number of things that wants to make you pull your hair out and flip some tables onto small children, but after many hours searching and trying different things I've come across somethings that are VERY VERY handy for tools in WPF. I'll probably just update this post as I come across more to keep them all in once place but for starters, here is a few things that I've learned to make dealing with the dreaded and yet very powerful TreeView.

WPF TreeView is probably the best and the most frustrating thing I've had to deal with on a constant basis in the last few months; it's so handy for presenting data in a hierarchical way but there are things that you just can't do directly with TreeView without searching and searching and searching the internet to find the answers to such as:


  • How can I force TreeView to change the SelectedItem?
          Now this I will say has stunned me for a while and I actually JUST found the answer to this problem Here on Stack Overflow.  So what are we to do here? Well if you're lucky enough to have your project presenting with a inherited model (cause you should be at least using the MVVM programming pattern while doing WPF tools) for your data then you can do this quite easily.  You need about 2-3 things,
1. Keep track of your selectedItem via clicks/keyboard navigation 
2. Add to your base model type, a property like 
        private bool _isSelected;
        public bool IsSelected { get { return _isSelected; } set { Set(ref _isSelected, value); } }
    *NOTE* the Set is just raising INotifyPropertyChanged so how ever you are handling yours is how you should do it here as well *NOTE*
3. In your treeview, you'll need to have at least this line from the link
<Setter Property="IsSelected"
                        Value="{Binding IsSelected, Mode=TwoWay}" />
What this is going to do is allow you to toggle IsSelected in your data Model and since the treeview is bound to it, it will automatically set and be set to IsSelected.  Now some people may think, "Why wouldn't you just do it in code behind or something like that?"  Well that's because IsSelected is a read-only property, so you can't just set it all willy-nilly like that and from all the proposed ways I've seen so far, this is the easiest to setup.

This also goes for being able to select what IS and ISN'T expanded in your treeview programmatically, make a property and bind with the Setter in your treeview.

Search Results




  • How to implement a simple and easy to use Search Filter for your TreeView
Searchable WPF TreeView on Code Project
Thankfully, Already made a great article on this. If you don't need/want the super fancy search box (was a bit overkill for what I needed) then the really relevant parts of the article are having some more setter properties in your Treeview for

<Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay}" />
                <Setter Property="Visibility" Value="{Binding Path=IsMatch, Mode=OneWay, Converter={StaticResource ResourceKey=boolToVisibility}}"/>
This lets you setup your TreeView to work with a method that you will call in your code that will search through your Data (that is bound to the tree) and set Expanded to true/false and Visibility to true/false as well which will effectively hide anything that doesn't match your results in your textbox and expand open to the result, anything that does match. Much like the IsSelected above, you'll add the IsExpanded and IsMatch(I did IsVisible myself) to your data Model.

The next important piece of code that you will need is this
public void ApplyCriteria(string criteria, Stack<TreeNodeViewModel> ancestors) {
    if (IsCriteriaMatched(criteria)) {
        IsMatch = true;
        foreach (var ancestor in ancestors) {
            ancestor.IsMatch = true;
            ancestor.IsExpanded = !String.IsNullOrEmpty(criteria);
        }
    } 
    else
        IsMatch = false;

    ancestors.Push(this);
    foreach (var child in Children)
        child.ApplyCriteria(criteria, ancestors);

    ancestors.Pop();
}
        
The method that determines if this node is a match is in this article a simple string comparison;
private bool IsCriteriaMatched(string criteria) {
    return String.IsNullOrEmpty(criteria) || name.Contains(criteria);
}
The methods will check for anything matching the searchText you give it (I hooked up an extra method from my codebehind that when the text changes in my textbox, call the method which passes the text to the Apply Criteria.) and expand or hide accordingly.

*NOTE* if you want to make the search just a bit more functional change the IsCriteriaMatched to
name.ToLower().Contains(criteria.ToLower());  This will make it that no matter if you have say Matilda or matilda, it will still show up if you gave the search box MaTilDa for example *NOTE*

The only change to this method that I really made was that I also pass the current node you're checking so you can get the name of the node like


private bool IsCriteriaMatched(string criteria, NodeWithName check)
{
    return String.IsNullOrEmpty(criteria) || check.Name.ToLower().Contains(criteria.ToLower());
}

The only part left is in your WPF you have a textbox and choose whether you want it live filtering (as the user types it filters) or keypress filtering (hit enter, it does the search) and make sure that you save/bind to the textbox text in a viewmodel (pretty much the same place you do the above search methods is as good a place as any)

Those are the two biggest things that I've run into and it's taken a while to find good working answers to (I've never used the Setter myself before this so it was a good experience to learn that I could bind to it).

Hopefully people stumble across this post and find these answers instead of searching for a long time.

T.Out();