相关文章推荐
奔跑的茄子  ·  C# ...·  2 月前    · 
千杯不醉的绿茶  ·  WPF combobox ...·  2 月前    · 
高大的拐杖  ·  wpf ...·  2 周前    · 
强健的红薯  ·  Java(24):GSON - ...·  2 年前    · 
阳刚的熊猫  ·  .NET ...·  2 年前    · 

In this article, I will demonstrate a WPF way of how to create a tree view to which filtering can be applied as a way of searching for a particular node or item in the tree. Using filtering to prune or limit a tree view is something I find very useful and it really bugs me when I can't for example use it to quickly find the option I want to change in the Visual Studio Options. I usually know roughly what I am looking for and it's often faster to type a portion of that than to visually inspect the entire tree, the Window 7 start-menu or the Windows 8 UI are fine examples of this approach being put to good use.

This is obviously not a new problem nor is the internet lacking in example implementations, this article is based on something I did for a friend and I got several requests for the source code after posting it on YouTube so here it is.

Because the subject matter is fairly limited, this will be a relatively short article.

Using the Code

Two archives are provided, one for C# and one for VB.NET , so that each can read the sources in the language of their choice.

For the article, since there is so little code involved I've decided to have both the C# and VB.NET code present.

Requirements

When my friend requested this to be implemented, he gave me a short list of requirements that it needed to fulfill:

  • It needs to be based on a System.Windows.Controls.TreeView .
  • Tree view should, when not filtered, behave like a normal tree view.
  • A text input field should accept input that prunes the tree view in real time (by real-time, he meant there should be no need to hit Enter or something like that for the filtering to occur).
  • The filter conditions should be remembered so that they could easily be re-used (personally, I think this is a bit superfluous as this type of control becomes really useful when the criterion one enters for the filtering are simple enough to be easily remembered).
  • The text input field should not occupy too much screen real estate, whilst at the same time being obvious enough for a first time user to find and understand.
  • Further, the components of the implementation should lend themselves to MVVM approach as it's likely that the visual appearance would be changed by the UI designers.

    The Basics

    I decided that my implementation would be a DataTemplate containing a TreeView (for requirement #1) and an editable ComboBox that would serve as both a way of inputting the filter condition and a list of previous conditions (for requirements #3 and #4).

    Note: As this was built to fit something that would hold an application's settings the DataTemplate (and accompanying view-model) are name Settings -something, that is a bit too specific for a general implementation but I've left it in anyway.

    As I wanted a slightly nicer look to the tree view, it is mostly defined in a Style that I'll cover later. The filter ComboBox has an icon and is also mainly covered by a Style as in order to comply with requirement #5, I wanted it to animate into a smaller element when not focused and then spring back into full size when needed. The XAML for the DataTemplate of the "control" looks like this;

    <DataTemplate DataType="{x:Type vm:SettingsViewModel}">
            <TreeView Style="{StaticResource ResourceKey=SearchableTreeView}" 
                      ItemsSource="{Binding Path=Roots, Mode=OneWay}"/>
            <Border Style="{StaticResource ResourceKey=SearchBox}">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>
                    <Image Grid.Column="0" Source="pack://application:,,,/Resources/Images/Search.png"/>
                    <ComboBox Grid.Column="1"
                                IsEditable="True"
                                ItemsSource="{Binding Path=PreviousCriteria, Mode=OneWay}"
                                SelectedItem="{Binding Path=SelectedCriteria}"
                                Text="{Binding Path=CurrentCriteria, 
                                      UpdateSourceTrigger=PropertyChanged}"
                                i:EventCommand.Event="UIElement.LostFocus"
                                i:EventCommand.Command=
                                    "{Binding Path=StoreInPreviousCommand, Mode=OneWay}"/>
                </Grid>
            </Border>
        </Grid>
    </DataTemplate>        

    I'll cover later what the i:EventCommand.Event and i:EventCommand.Command attributes are for.

    The Border that get Style SearchBox applied to it is essentially the search " control ".

    The TreeView Style

    The Style of the TreeView mainly deals with the visual aspects of the TreeView , such as setting an ItemTemplate that has both an icon and the name of the node, but it also sets up the HierarchicalDataTemplate that defines how each node provides children.
    Using the ItemContainerStyle , it also configures the bindings that handle collapsed/expanded, visible/invisible and selected/unselected states.

    A tree node knows if it is currently covered by the current search criteria and this is exposed by a property called IsMatch on the view-model , that way a change in the search criteria can ripple through all nodes and set the IsMatch state causing the node to be visible or hidden depending on what the user is looking for. There's a bit more than just looking at the current node when searching as if a sub node is a match but the parent node isn't we still want the parent node made visible so that the path to the found node is clear. I'll cover that in more detail later when going through the view-model implementations.
    The Style for the TreeView looks like this:

    <Style x:Key="SearchableTreeView" TargetType="{x:Type TreeView}">
        <Setter Property="Background" Value="Transparent"/>
        <Setter Property="ItemContainerStyle">
            <Setter.Value>
                <Style TargetType="{x:Type TreeViewItem}">
                    <Setter Property="BorderThickness" Value="1.5"/>
                    <Setter Property="IsExpanded" Value="{Binding Path=IsExpanded, Mode=TwoWay}" />
                    <Setter Property="Visibility" Value="{Binding Path=IsMatch, Mode=OneWay, 
                            Converter={StaticResource ResourceKey=boolToVisibility}}"/>
                    <Style.Triggers>
                        <Trigger Property="IsSelected" Value="True">
                            <Setter Property="BorderBrush" Value="#FFABC0F0"/>
                        </Trigger>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="IsSelected" Value="True"/>
                                <Condition Property="IsSelectionActive" Value="False"/>
                            </MultiTrigger.Conditions>
                            <Setter Property="BorderBrush" Value="LightGray"/>
                        </MultiTrigger>
                    </Style.Triggers>
                    <Style.Resources>
                        <Style TargetType="Border">
                            <Setter Property="CornerRadius"
    
    
    
    
        
     Value="3"/>
                        </Style>
                    </Style.Resources>
                </Style>
            </Setter.Value>
        </Setter> 
        <Setter Property="ItemTemplate">
            <Setter.Value>
                <HierarchicalDataTemplate DataType="{x:Type vm:TreeNodeViewModel}" 
                           ItemsSource="{Binding Path=Children, Mode=OneWay}">
                    <StackPanel Orientation="Horizontal" Margin="2 0 4 0">
                        <Image Width="18" Height="18" Margin="0 0 4 0" 
                           Source="{Binding Converter={StaticResource ResourceKey=treeNode}}"/>
                        <TextBlock Text="{Binding Path=Name, Mode=OneWay}" />
                    </StackPanel>
                </HierarchicalDataTemplate>
            </Setter.Value>
        </Setter>
        <Style.Resources>
            <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Black" />
            <SolidColorBrush x:Key="{x:Static SystemColors.ControlTextBrushKey}" Color="Black" />
            <LinearGradientBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" 
                        EndPoint="0,1" StartPoint="0,0">
                <GradientStop Color="#FFE0F0FF" Offset="0"/>
                <GradientStop Color="#FFABE0FF" Offset="1"/>
            </LinearGradientBrush>
            <LinearGradientBrush x:Key="{x:Static SystemColors.ControlBrushKey}" 
                        EndPoint="0,1" StartPoint="0,0">
                <GradientStop Color="#FFEEEEEE" Offset="0"/>
                <GradientStop Color="#FFDDDDDD" Offset="1"/>
            </LinearGradientBrush>
        </Style.Resources>
    </Style>        

    The SearchBox Style

    The Style for the Border that makes up the search box sets up a view properties and also defines the animations that allow the search field to " spring " into view when focused.
    This is done by wiring up Storyboard s to the routed events Mouse.MouseEnter and Mouse.MouseExit :

    <Style x:Key="SearchBox" TargetType="{x:Type Border}">
        <Style.Resources>
            <ElasticEase x:Key="EaseInEase" 
            EasingMode="EaseOut" Oscillations="2" Springiness="7"/>
            <SineEase x:Key="EaseOutEase" EasingMode="EaseIn"/>
        </Style.Resources>
        <Setter Property="Width" Value="16"/>
        <Setter Property="Height" Value="16"/>
        <Setter Property="HorizontalAlignment" Value="Right"/>
        <Setter Property="VerticalAlignment" Value="Top"/>
        <Setter Property="Margin" Value="4 4 20 4"/>
        <Setter Property="CornerRadius" Value="3"/>
        <Setter Property="BorderBrush" Value="DarkGray"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="Padding" Value="2"/>
        <Setter Property="Background">
            <Setter.Value>
                <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                    <GradientStop Color="#F0F0F0" Offset="0.0" />
                    <GradientStop Color="#C0C0C0" Offset="1.0" />
                </LinearGradientBrush>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <EventTrigger 
    
    
    
    
        
    RoutedEvent="Mouse.MouseEnter">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetProperty="(Border.Width)" 
                         EasingFunction="{StaticResource ResourceKey=EaseInEase}" 
                         To="200" Duration="0:0:1.0"/>
                        <DoubleAnimation Storyboard.TargetProperty="(Border.Height)" 
                         EasingFunction="{StaticResource ResourceKey=EaseInEase}" 
                         To="30" Duration="0:0:1.0"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
            <EventTrigger RoutedEvent="Mouse.MouseLeave">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetProperty="(Border.Width)" 
                         EasingFunction="{StaticResource ResourceKey=EaseOutEase}" 
                         To="16" Duration="0:0:0.2"/>
                        <DoubleAnimation Storyboard.TargetProperty="(Border.Height)" 
                         EasingFunction="{StaticResource ResourceKey=EaseOutEase}" 
                         To="16" Duration="0:0:0.2"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Style.Triggers>
    </Style>        

    At the top is the two easings used, confusingly EaseInEase uses a EasingMode EaseOut whilst EaseOutEase uses a EaseIn , by EaseInEase I refer to the animation that is going to bring the search box into view (and that eases out) and by EaseInEase I refer to the animation that " hides " the search box after use.

    I took me a little while to get the easings and durations right for these animations, I didn't want them to run for a long time (as that would be nothing but irritating, having to wait for the search box to become available), but I also wanted them to appear fluid. In the end, I ended up with what's above and I'm not entirely happy with it but it looks Ok I think.

    Remembering Previous Criteria

    For requirement #4, the search box needs to remember previously used criteria and I wanted it to store them as the search box lost focus as I thought that would be the point in time when the user has found what they were looking for and clicking it. It's important to store only relevant criteria so storing on PropertyChanged for example would store too much as it would store partial string as the user type the criteria.

    Going for a MVVM approach, I wanted the view to execute a ICommand on the view-model when the ComboBox lost control. In order to achieve this, I came up with what I think is a fairly ugly solution where I employ attached properties to bind a command to the firing of a RoutedEvent .
    The XAML for this can be seen in the above listing of the DataTemplate for the SettingsViewModel ;

    <ComboBox Grid.Column="1"
                IsEditable="True"
                ItemsSource="{Binding Path=PreviousCriteria, Mode=OneWay}"
                SelectedItem="{Binding Path=SelectedCriteria}"
                Text="{Binding Path=CurrentCriteria, UpdateSourceTrigger=PropertyChanged}"
                i:EventCommand.Event="UIElement.LostFocus"
                i:EventCommand.Command="{Binding Path=StoreInPreviousCommand, Mode=OneWay}"/>        

    The last two attributes sets EventCommand properties for the RoutedEvent and a binding to the ICommand on the view model . And while that does not look too ugly in the XAML , the implementation of the attached properties is as it has to use reflections and weird ways of creating the delegates;

    public static class EventCommand {
        private static readonly MethodInfo HandlerMethod = typeof(EventCommand).GetMethod
                          ("OnEvent", BindingFlags.NonPublic | BindingFlags.Static);
        public static readonly DependencyProperty EventProperty = DependencyProperty.RegisterAttached
        ("Event", typeof(RoutedEvent), typeof(EventCommand), new PropertyMetadata(null, OnEventChanged));
        public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached
        ("Command", typeof(ICommand), typeof(EventCommand), new PropertyMetadata(null));
        public static void SetEvent(DependencyObject owner, RoutedEvent value) {
            owner.SetValue(EventProperty, value);
        public static RoutedEvent GetEvent(DependencyObject owner) {
            return (RoutedEvent)owner.GetValue(EventProperty);
        public static void SetCommand(DependencyObject owner, ICommand value) {
            owner.SetValue(CommandProperty, value);
        public static ICommand GetCommand(DependencyObject owner) {
            return (ICommand)owner.GetValue(CommandProperty);
        private static void OnEventChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
            if (e.OldValue != null) {
                var @event = d.GetType().GetEvent(((RoutedEvent)e.OldValue).Name);
                @event.RemoveEventHandler
                      (d, Delegate.CreateDelegate(@event.EventHandlerType, HandlerMethod));
            if (e.NewValue != null) {
                var @event = d.GetType().GetEvent(((RoutedEvent)e.NewValue).Name);
                @event.AddEventHandler
                  (d, Delegate.CreateDelegate(@event.EventHandlerType, HandlerMethod));
        private static void OnEvent(object sender, EventArgs args) {
            var command = GetCommand((DependencyObject)sender);
            if (command != null && command.CanExecute(null))
                command.Execute(null);
    
    VB.NET
    Module EventCommand
      Public ReadOnly HandlerMethod As MethodInfo = GetType(EventCommand).GetMethod
                           ("OnEvent", BindingFlags.NonPublic Or BindingFlags.Static)
      Public ReadOnly EventProperty As DependencyProperty = DependencyProperty.RegisterAttached
          ("Event", GetType(RoutedEvent), GetType(EventCommand), 
          New PropertyMetadata(Nothing, AddressOf OnEventChanged))
      Public ReadOnly CommandProperty As DependencyProperty = DependencyProperty.RegisterAttached
            ("Command", GetType(ICommand), GetType(EventCommand), New PropertyMetadata(Nothing))
      Public Sub SetEvent(ByVal element As UIElement, ByVal value As RoutedEvent)
        element.SetValue(EventProperty, value)
      End Sub
      Public Function GetEvent(ByVal element As UIElement) As RoutedEvent
        Return CType(element.GetValue(EventProperty), RoutedEvent)
      End Function
      Public Sub SetCommand(ByVal element As UIElement, ByVal value As ICommand)
        element.SetValue(CommandProperty, value)
      End Sub
      Public Function GetCommand(ByVal element As UIElement) As ICommand
        Return CType(element.GetValue(CommandProperty), ICommand)
      End Function
      Private Sub OnEventChanged(d As
    
    
    
    
        
     DependencyObject, e As DependencyPropertyChangedEventArgs)
        If Not IsNothing(e.OldValue) Then
          Dim evt As EventInfo = d.GetType().GetEvent(CType(e.OldValue, RoutedEvent).Name)
          evt.RemoveEventHandler(d, System.Delegate.CreateDelegate(evt.EventHandlerType, HandlerMethod))
        End If
        If Not IsNothing(e.NewValue) Then
          Dim evt As EventInfo = d.GetType().GetEvent(CType(e.NewValue, RoutedEvent).Name)
          evt.AddEventHandler(d, System.Delegate.CreateDelegate(evt.EventHandlerType, HandlerMethod))
        End If
      End Sub
      Private Sub OnEvent(sender As Object, args As EventArgs)
        Dim command As ICommand = GetCommand(CType(sender, DependencyObject))
        If Not command Is Nothing And command.CanExecute(Nothing) Then
          command.Execute(Nothing)
        End If
      End Sub
    End Module      

    The only good thing about that code is that it appears to do the job and does allow an ICommand to be bound to any RoutedEvent.

    Note: Another way of solving it would be to have a much more specific type of attached property, one that explicitly wired up the relevant events in code and delegated to a method on the view-model, that way of doing it is usually called an attached behaviour, and whilst I don't dislike that approach, I wanted something more generic for this (which technically is stupid because I am violating the YAGNI rule by doing it).

    Note: And yet another way of solving it, if a dependency on a Blend library isn't an issue, would be to use EventTriggers from the Blend Interaction namespace which does the same as the code above and more.

    View-models

    The project contains two view-models; one for the main view owning both the tree and the search and one for each individual tree node. Technically, there should also be corresponding models but I've left them out of the article for brevity.

    TreeNodeViewModel

    The view-model that backs the nodes of the TreeView is pretty much a bulk-standard tree node with the addition of a method:

    public void ApplyCriteria(string criteria, Stack<TreeNodeViewModel> ancestors)        
    VB.NET
    Public Sub ApplyCriteria(criteria As String, ancestors As Stack(Of TreeNodeViewModel))      

    that allows the node to have a search criteria applied to it. The reason the method takes both a string criteria and a Stack<TreeNodeViewModel> of ancestors is because it needs to be able to make all its ancestors visible if it itself find the criteria to be a match.

    A richer implementation could have set a different state on an ancestor of a matching node so that ancestors that are visible only because a child node is a search match could have been rendered slightly differently.

    The node will see if it is a match for the criteria and if it is it will iterate through all of its ancestors and set them to be matching as well and also expanding them.

    If the node is not a leaf, it will then push itself as an ancestor to the Stack<TreeNodeViewModel> of ancestors and recurse over all of its children. When done, it pops itself of the stack. A node is expanded automatically only if it is an ancestor to a match, not if it is just a match as that would make visible potentially incorrect child nodes.

    private void CheckChildren(string criteria, TreeNodeViewModel parent) {
        foreach (var child in parent.Children) {
            if (child.IsLeaf && !child.IsCriteriaMatched(criteria)) {
                child.IsMatch = false;
            CheckChildren(criteria, child);
    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);
                CheckChildren(criteria, ancestor);
            IsExpanded = false;
            IsMatch = false;
        ancestors.Push(this);
        foreach (var child in Children)
            child.ApplyCriteria(criteria, ancestors);
        ancestors.Pop();
    
    VB.NET
    Private Sub CheckChildren(criteria As String, parent As TreeNodeViewModel)
        For Each child In parent.Children
            If child.IsLeaf And Not child.IsCriteriaMatched(criteria) Then
                child.IsMatch = False
            End If
            CheckChildren(criteria, child)
    End Sub
    Public Sub ApplyCriteria(criteria As String, ancestors As Stack(Of TreeNodeViewModel))
        If IsCriteriaMatched(criteria) Then
            IsMatch = True
            For Each ancestor In ancestors
                ancestor.IsMatch = True
                ancestor.IsExpanded = Not String.IsNullOrWhiteSpace(criteria)
                CheckChildren(criteria, ancestor)
            IsExpanded = False
            IsMatch = False
        End If
        ancestors.Push(Me) ' and then just touch me
        For Each child In Children
            child.ApplyCriteria(criteria, ancestors)
        ancestors.Pop()
    End Sub
    

    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);
    
    VB.NET
    Private Function IsCriteriaMatched(criteria As String) As Boolean
      Return String.IsNullOrEmpty(criteria) Or Name.Contains(criteria)
    End Function      

    It might look strange that a null or blank string is considered a match but that's there to cover the case when no criteria is entered as that is supposed to make all nodes visible.

    A proper implementation would more likely have the constructor take a delegate that could be run on the criteria and the underlying model rather than just look a the name, that would allow the criteria to be applied to the content held by the node rather than just the node name.

    Things could be filtered on all sorts of context such as user privileges, for example.

    The full listing of the view-model looks like this:

    public class TreeNodeViewModel : Notifier {
        private readonly ObservableCollection<TreeNodeViewModel> children = 
                                  new ObservableCollection<TreeNodeViewModel>();
        private readonly string name;
        private bool expanded;
        private bool match = true;
        private bool leaf;
        private TreeNodeViewModel(string name, bool leaf) {
            this.name = name;
            this.leaf = leaf;
        public TreeNodeViewModel(string name, IEnumerable<TreeNodeViewModel> children) 
            : this(name, false) {
            foreach (var child in children)
                this.children.Add(child);
        public TreeNodeViewModel(string name) 
            : this(name, true) {
        public override string ToString() {
            return name;
        private bool IsCriteriaMatched(string criteria) {
            return String.IsNullOrEmpty(criteria) || name.Contains(criteria);
        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);
                IsMatch = false;
            ancestors.Push(this);
            foreach (var child in Children)
                child.ApplyCriteria(criteria, ancestors);
            ancestors.Pop();
        public IEnumerable<TreeNodeViewModel> Children {
            get { return children; }
        public string Name {
            get { return name; }
        public bool IsExpanded {
            get { return expanded; }
            set {
                if
    
    
    
    
        
     (value == expanded) 
                    return;
                expanded = value;
                if (expanded) {
                    foreach (var child in Children)
                        child.IsMatch = true;
                OnPropertyChanged("IsExpanded");
        public bool IsMatch {
            get { return match; }
            set {
                if (value == match)
                    return;
                match = value;
                OnPropertyChanged("IsMatch");
        public bool IsLeaf {
            get { return leaf; }
            set {
                if (value == leaf) 
                    return;
                leaf = value;
                OnPropertyChanged("IsLeaf");
    
    VB.NET
    Public Class TreeNodeViewModel
        Inherits Notifier
        Private ReadOnly childNodes As ObservableCollection(Of TreeNodeViewModel)
        Private ReadOnly nodeName As String
        Private expanded As Boolean
        Private match As Boolean = True
        Sub New(name As String, children As IEnumerable(Of TreeNodeViewModel))
            childNodes = New ObservableCollection(Of TreeNodeViewModel)(children)
            nodeName = name
        End Sub
        Sub New(name As String)
            Me.New(name, Enumerable.Empty(Of TreeNodeViewModel))
        End Sub
        Public Overrides Function ToString() As String
            Return nodeName
        End Function
        Private Function IsCriteriaMatched(criteria As String) As Boolean
            Return String.IsNullOrEmpty(criteria) Or Name.Contains(criteria)
        End Function
        Public Sub ApplyCriteria(criteria As String, ancestors As Stack(Of TreeNodeViewModel))
            If IsCriteriaMatched(criteria) Then
                IsMatch = True
                For Each ancestor In ancestors
                    ancestor.IsMatch = True
                    ancestor.IsExpanded = Not String.IsNullOrWhiteSpace(criteria)
                IsMatch = False
            End If
            ancestors.Push(Me) ' and then just touch me
            For Each child In Children
                child.ApplyCriteria(criteria, ancestors)
            ancestors.Pop()
        End Sub
        Public ReadOnly Property Children() As IEnumerable(Of TreeNodeViewModel)
                Return childNodes
            End Get
        End Property
        Public ReadOnly Property Name() As String
                Return nodeName
            End Get
        End Property
        Public Property IsExpanded() As Boolean
                Return expanded
            End Get
            Set(value As Boolean)
                If expanded = value Then Return
                expanded = value
                If expanded Then
                    For Each child In Children
                        child.IsMatch = True
                End If
                OnPropertyChanged("IsExpanded")
            End Set
        End Property
        Public Property IsMatch() As Boolean
                Return match
            End Get
            Set(value As Boolean)
                If match = value Then Return
                match = value
                OnPropertyChanged("IsMatch")
            End Set
        End Property
        Public ReadOnly Property IsLeaf() As Boolean
                Return Not Children.Any()
            End Get
        End Property
    End Class        

    Note: Having the child nodes contained in a ObservableCollection<TreeNodeViewModel> when a IEnumerable<TreeNodeViewModel> would also have done the trick might appear to be a bit overkill, the reason for that in this article is that the actual full source had nodes dynamically added to the tree when certain settings were applied.

    SettingsViewModel

    The responsibility of the SettingsViewModel is to own the reference to the root nodes (yes, plural) of the tree view as well as processing changes to the search criteria.
    It does this by listening to changes of the criteria (covered by property CurrentCriteria, which is bound with an UpdateSourceTrigger binding attribute set to PropertyChanged) and calling a method called ApplyFilter that iterates over all root nodes and call TreeNodeViewModel.ApplyCriteria that then recurses over the nodes as discussed previously.

    When focus is lost, the EventCommand-based method of executing an ICommand off the back of a RoutedEvent causes the command exposed through the StoreInPreviousCommand property to fire. This command adds the current criteria to the list of existing, previously used criteria provided it's not empty and does not already exists in the list of previous criteria. It then sets SelectedCriteria property causing the backing ComboBox to consider the value one picked from the list.

    The view-model exposes three properties to track the criteria:

  • CurrentCriteria, which is bound to the editable text of the ComboBox
  • SelectedCriteria, which is bound to the selected item in the ComboBox's list of items (the previous values)
  • PreviousCriteria, which is bound to a list containing all previously used criteria
  • In addition to the three properties listed above, the view-model also exposes the command that fires when focus is lost and a list of the roots that the tree view uses.

    Something that I didn't implement but probably should have is for the SettingsViewModel to only store a previous criteria if applying it yields a non-empty set of visible tree nodes. It makes no sense to store something that will never find anything and storing it will just pollute the drop-down, making it harder to find the relevant criteria.

    Points of Interest

    Many parts that would make this a fully fledged implementation have been left out, such as all the models and any actual content or payload for the leafs in the tree view (obviously selecting one or double-clicking one should present the user with more than just a slight highlight of the node), but I think what's been left in shows the interesting parts which I think are:

  • MVVMed tree view
  • Binding a command to any event
  • "Real time" filtering of nodes as a means of searching
  • Trivial parts such as value converters have been left out of the article, but are included in the source.

    The organization of the project follows a pattern I've used frequently but have come to despise. There are folders for view-models, data templates and value converters (for example) and I think that this way of organizing things is wrong. Instead of grouping things by what they are, maintainability can be increased if they're grouped by what they do. The problem isn't obvious in a solution this small but it becomes apparent as the size grows; I want things related to (for example) the settings grouped together, because when I get a bug assigned to me, it will say "the settings are broken", not "the view-models are broken".
    I've left it the way it is because that's what the guy requesting the implementation likes, but I advise against this way of grouping things.

    History

  • 2014-02-03: Version 2: Added code-sample formatting (suggested by this guy) and Blend references (as pointed out by this guy)
  • Oakmead Apps Android Games
    21 Feb 2014: Best VB.NET Article of January 2014 - Second Prize
    18 Oct 2013: Best VB.NET article of September 2013
    23 Jun 2012: Best C++ article of May 2012
    20 Apr 2012: Best VB.NET article of March 2012
    22 Feb 2010: Best overall article of January 2010
    22 Feb 2010: Best C# article of January 2010 Hello. thank you for informations.
    How to add to root and child in treeview from sqlite database.
    Sign In·View Thread  Does anyone know how to add a MouseDoubleClick event of a node on this TreeView back to the code behind? I need to process a selected node.

    Sign In·View Thread  QuestionAlso looking for some code on how to add a click event of a tree node Pin
    WEJ09015-May-18 8:26
    WEJ09015-May-18 8:26  This is a great treeview. Can someone help me with some code on a tree node click event. I want to be able to click on a tree node and have a picture displayed on the UI
    Sign In·View Thread  AnswerRe: Also looking for some code on how to add a click event of a tree node Pin
    Chuck Salerno 3-Aug-19 15:26
    Chuck Salerno3-Aug-19 15:26 
    Did you get any responses for this? I'm looking to use this but need to handle a double mouse click on a tree node.
    Sign In·View Thread  <ResourceDictionary Source="pack://application:,,,/Bornander.UI;component/Resources/Styles/TreeView.xaml"/>

    I am relatively new to WPF and so I am not sure if this is the best solution?

    modified 9-Aug-17 19:34pm.

    Sign In·View Thread  I'm new to C#. I've managed to convert my list of menu options into a Tree structure but now want to programmatically add the nodes to the tree. How do I use TreeNodeViewModel to make it so?
    This is the stackoverflow code to dump my tree:
    static void Test(IEnumerable<TreeItem<category>> categories, int deep = 0) foreach (var c in categories) Console.WriteLine(new String('\t', deep) + c.Item.Name); Test(c.Children, deep + 1); From here.
    Thank you in advance.
    Sign In·View Thread  </TreeView.ItemTemplate> <TreeView.ItemContainerStyle> <Style TargetType="{x:Type TreeViewItem}"> <Setter Property="Visibility" Value="{Binding Path=TagBool, Converter={StaticResource BoolToVis}}"></Setter> </Style> </TreeView.ItemContainerStyle> </TreeView>

    string Name { get; set; } ITreeObj Parent { get; set; } List<ITreeObj> Children { get; set; } bool TagBool { get; set; } private void tbSearchStr_KeyUp(object sender, KeyEventArgs e) var str = tbSearchStr.Text; foreach (var item in treeObjects) MarkVisible((ITreeObj)item, str); private void MarkVisible(ITreeObj item, string str) if (string.IsNullOrEmpty(str)) item.TagBool = true; else if (item.Name.ToLower().Contains(str.ToLower())) item.TagBool = true; var parent = item.Parent; while (parent != null) { parent.TagBool = true; parent = parent.Parent; } item.TagBool = false; foreach (var item2 in item.Children) MarkVisible(item2, str); }


    modified 17-Dec-16 3:21am.

    Sign In·View Thread  treeObjects
    defined?
    Also,with all the above c# in MainWindow.xaml.cs BoolToVis converter is not found. Is there a bit more code?
    Thanks in advance.
    Sign In · View Thread <UserControl.Resources> <BooleanToVisibilityConverter x:Key= " BoolToVis" /> </ UserControl.Resources > and treeObjects is collection of your top tree objects which implements ITreeObj interface
    ObservableCollection<YourObject> YourObjects = new ObservableCollection<YourObject>(); treeView1.ItemsSource = YourObjects;
    Sign In · View Thread Nice. I like the pun being used in your intro. I use to do tree work when I was in my 20's about 40 yrs ago(prune, treeview) go together very well. 5 stars
    Sign In · View Thread Thank you!
    Try Grapple for Android , it has a naked pixel guy in it!
    Also, loads of blood and some snakes.

    Sign In · View Thread Try to search "t" in the Sample application,the result node like "Binary.exe" is shown,which is wrong.
    Sign In · View Thread Thank you for taking the time to report this!
    The problem is caused by the expanding of the parent node when a match is found, it also makes the children of that parent visible.
    To fix it, add the following CheckChildren method and update the ApplyCriteria changes as well;
    private void CheckChildren( string criteria, TreeNodeViewModel parent) { foreach ( var child in parent.Children) { if (child.IsLeaf && !child.IsCriteriaMatched(criteria)) { child.IsMatch = false ; CheckChildren(criteria, child); 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); CheckChildren(criteria, ancestor); IsExpanded = false ; IsMatch = false ; ancestors.Push( this ); foreach ( var child in Children) child.ApplyCriteria(criteria, ancestors); ancestors.Pop(); I will try to provide an updated download with the fix. Thanks again for the help!
    Try Grapple for Android , it has a naked pixel guy in it!
    Also, loads of blood and some snakes.

    Sign In · View Thread I am trying to but the article editor messes up the entire formatting of the article when I swap out downloads. I'll make another attempt at it after work tonight and if I can't get it to respect the formatting I'll stick the files somewhere else and link to them from the comments here.
    Sorry for the inconvenience.
    Try Grapple for Android , it has a naked pixel guy in it!
    Also, loads of blood and some snakes.

    Sign In · View Thread Should be fixed now. Have a look.
    Try Grapple for Android , it has a naked pixel guy in it!
    Also, loads of blood and some snakes.

    Sign In · View Thread the search in the tree works fine.
    But when you search for a name that matches only a parent node. this Parent node is collapsed but has a expanded state.
    Try to search "Ap" in the Sample application.
    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); this.IsExpanded = false ; IsMatch = false ; ancestors.Push( this ); foreach ( var child in Children) child.ApplyCriteria(criteria, ancestors); ancestors.Pop(); when you add this: this.IsExpanded = false;
    the bahavior is normal.
    Sign In · View Thread /Fredrik
    Try Grapple for Android , it has a naked pixel guy in it!
    Also, loads of blood and some snakes.

    Sign In · View Thread I do really like this search functionality that you have provided, and for that alone I'd give you a 5. It is brilliant Thumbs Up | :thumbsup:
    However, I wondered if it is possible to hide all the code in the TreeView itself, so that it would be easier to implement at a later stage on existing projects. I've been playing around with attached properties on TreeVeiws ever since I saw this tip:
    WPF TreeView with WinForms Style Fomat [ ^ ]
    So in the costructor I found the TreeView control by traversing the visual tree. Public Sub New (item As TreeViewItem) TreeViewControl = FindTheTreeVIewControl(item) End Sub Created a dependency property that reacted to a textchange Public Shared Sub OnSearchTextChanged(sender As DependencyObject, e As DependencyPropertyChangedEventArgs) Dim TempSearchString As String = sender.GetValue(SearchTextProperty) If TreeViewControl IsNot Nothing And Not String .IsNullOrEmpty(TempSearchString.Trim) Then ExpandAll(TreeViewControl) End If End Sub And, just to test it, expanded all TreeViewItems: ' https://social.msdn.microsoft.com/Forums/vstudio/en-US/a2988ae8-e7b8-4a62-a34f-b851aaf13886/windows-presentation-foundation-faq?forum=wpf#expand_treeview Public Shared Sub ExpandAll(treeView As TreeView) ExpandSubContainers(treeView) End Sub Private Shared Sub ExpandSubContainers(parentContainer As ItemsControl) For Each item As [ Object ] In parentContainer.Items Dim currentContainer As TreeViewItem = TryCast (parentContainer.ItemContainerGenerator.ContainerFromItem(item), TreeViewItem) If currentContainer IsNot Nothing AndAlso currentContainer.Items.Count > 0 Then ' Expand the current item. currentContainer.IsExpanded = True If currentContainer.ItemContainerGenerator.Status <> GeneratorStatus.ContainersGenerated Then ' If the sub containers of current item is not ready, we need to wait until ' they are generated. AddHandler currentContainer.ItemContainerGenerator.StatusChanged, Sub () ExpandSubContainers(currentContainer) End Sub ' If the sub containers of current item is ready, we can directly go to the next ' iteration to expand them. ExpandSubContainers(currentContainer) End If End If End Sub BTW: Bea Stollniz [ ^ ] have a slightly different way of doing it using a Stack of TreeViewItems.
    I would of course have to use reflection for the search, but is it a viable way of doing this? Any thoughts of the pro vs. cons?
    Sign In · View Thread
    Kenneth Haugland wrote:
    I do really like this search functionality that you have provided, and for that alone I'd give you a 5. It is brilliant Thumbs Up | :thumbsup:
    Cool, I am glad you liked it!
    Kenneth Haugland wrote:
    owever, I wondered if it is possible to hide all the code in the TreeView itself
    I am not sure what you mean by this.
    Do you want to move code from the viewmodel into the TreeView ?
    Or do you want to hide more code from the TreeView ?
    (Note that I haven't extended or implemented TreeView is this example, just applied a container style that allow me to manipulate it from the view model)
    I am struggling to figure out what you want to achieve or eliminate from my solution, is there some way you can elaborate on this?
    Try Hovercraft for Android , voted "a game" by players.

    Sign In · View Thread
    Quote:
    Do you want to move code from the viewmodel into the TreeView?

    Yes. I just like to grab the TreeViewItems, the items that are binded to it and the search functionality, and generate a helper class hidden inside the TreeView. That way I don't have to change anything in projects I already have created.
    That sounded very nice in my head at least Laugh | :laugh:
    Edit:
    I think I got it now, but still not sure it will always work, in all scenarios. It's also a bit quirky as it will keep some lines if part of the treeview is collapsed, and I don't know how to update that yet.
    Demoproject can be downloaded on my Microsoft OneDrive [ ^ ].

    modified 13-May-15 3:28am.

    Sign In · View Thread Web01 2.8:2023-05-13:1