WPF Behaviors – TreeView

Time Spent Amongst Trees is Never Wasted Time
Katrina Mayer

In my previous post, I described how a Behavior could alter the functionality of a standard WPF control – in that case having the selected item of a ListBox scroll into view automatically. In this post I’ll be working with TreeViews – including adding the same scroll into view functionality. This time however, it’s not quite so straightforward.

In the MVVM world, there shouldn’t be a reference to user interface elements in the ViewModel, so no TreeViewItems there. Instead, the ViewModel just publishes a collection of nested data items which can be bound as the root nodes of the tree, and lets the data templates defined in the View take care of things. The WPF TreeView control is pretty flexible – it can handle multiple data item types, even sibling items of a common parent node can be different types, each with their own display characteristics. However, the standard WPF TreeView has no bindable selected item property, so that will be part of the behavior.

So far as the ViewModel layer is concerned, TreeView data items need to handle expansion, selection, checking (tick box) including updating parent and child items, disabled state, lazy loading of child items etc. That sounds like a lot of common behaviour, which should be abstracted out into a base class. If you remember, in a previous post, I said that WrapperViewModels had two common ancestor base classes, that were unused at that time. Well, for those applications that are TreeView based, one of those would be the ideal place to put all that functionality.

I’m not going to reproduce the code for perTreeViewItemViewModelBase here, as it is pretty much self-documenting, just talk about a couple of its features. The class is designed to operate in two modes, either

  • Build the entire tree in one go by calling AddChild() on the appropriate parent node for each item as required. The internal _childrenList is an observable collection, so you can add or remove nodes at any point, and the U.I. will automatically refresh.
  • Operate the tree in lazy loading mode by calling SetLazyLoadingMode() on each node as required. Initially just create the root level items, and fetch the child nodes as required – DoLazyLoadAsync() will be called, to build the immediate children, when a node is expanded for the first time. The virtual LazyLoadFetchChildren() method can be overridden to fetch the data for the new nodes (e.g. from a database or web service), and the virtual InitialisedChildren property allows for the children list to be generated as required, e.g. as the union of multiple internal child item collections.

To add the required functionality, I’ll be creating two new helper classes …

  • TreeView – to add a bindable selected item property
  • TreeViewItem – to add two different scroll into view operations
    • When an item is selected (via the selected property of the perTreeViewItemViewModelBase item)
    • When an item is expanded, the Tree should be scrolled to show as many of its children as possible

… which are included in the global style for TreeView controls.

TreeView Helper

This behavior creates a new attached property – BoundSelectedItem, which we can use as the target for data binding.

    public class perTreeViewHelper : Behavior<TreeView>
    {
        public object BoundSelectedItem
        {
            get => GetValue(BoundSelectedItemProperty);
            set => SetValue(BoundSelectedItemProperty, value);
        }

        public static readonly DependencyProperty BoundSelectedItemProperty =
            DependencyProperty.Register("BoundSelectedItem",
                typeof(object),
                typeof(perTreeViewHelper),
                new FrameworkPropertyMetadata(null,
                    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                    OnBoundSelectedItemChanged));

        private static void OnBoundSelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            if (args.NewValue is perTreeViewItemViewModelBase item)
                item.IsSelected = true;
        }

        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
        }

        protected override void OnDetaching()
        {
            AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
            base.OnDetaching();
        }

        private void OnTreeViewSelectedItemChanged(object obj, RoutedPropertyChangedEventArgs<object> args)
        {
            BoundSelectedItem = args.NewValue;
        }
    }

These attached properties can be added to extend any DependencyObject, but are most often used on Control descendents – two common examples are Grid.Row & DockPanel.Dock. The property changed callback for this new property (OnBoundSelectedItemChanged) just checks that the selected item descends from perTreeViewItemViewModelBase, and if so, sets its IsSelected property to true. The TreeView, through its ItemContainerStyle does the rest. Note that by rule, a TreeView can only have one selected item, but this is handled automatically – we don’t have to manually clear the selection on the previously selected item. The OnAttached method of the behavior is used to add a handler to the existing SelectedItemChanged event of the TreeViewItem. This updates the value of BoundSelectedItem when the user manually selects a new item in the tree.

TreeViewItem Helper

This helper is designed to be used as part of a style for TreeViewItems. The simplest construct is coding it as a static class, which acts as a placeholder for the attached property definitions.

    public static class perTreeViewItemHelper
    {
        public static bool GetBringSelectedItemIntoView(TreeViewItem treeViewItem)
        {
            return (bool)treeViewItem.GetValue(BringSelectedItemIntoViewProperty);
        }

        public static void SetBringSelectedItemIntoView(TreeViewItem treeViewItem, bool value)
        {
            treeViewItem.SetValue(BringSelectedItemIntoViewProperty, value);
        }

        public static readonly DependencyProperty BringSelectedItemIntoViewProperty =
            DependencyProperty.RegisterAttached(
                "BringSelectedItemIntoView",
                typeof(bool),
                typeof(perTreeViewItemHelper),
                new UIPropertyMetadata(false, BringSelectedItemIntoViewChanged));

        private static void BringSelectedItemIntoViewChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            if (!(args.NewValue is bool))
                return;

            if (!(obj is TreeViewItem item))
                return;

            if ((bool)args.NewValue)
                item.Selected += OnTreeViewItemSelected;
            else
                item.Selected -= OnTreeViewItemSelected;
        }

        private static void OnTreeViewItemSelected(object sender, RoutedEventArgs e)
        {
            // prevent this event bubbling up to any parent nodes
            e.Handled = true;

            if (sender is TreeViewItem item)
            {
                // use DispatcherPriority.ApplicationIdle so this occurs after all operations related to tree item expansion
                perDispatcherHelper.AddToQueue(() => item.BringIntoView(), DispatcherPriority.ApplicationIdle);
            }
        }

        public static bool GetBringExpandedChildrenIntoView(TreeViewItem treeViewItem)
        {
            return (bool)treeViewItem.GetValue(BringExpandedChildrenIntoViewProperty);
        }

        public static void SetBringExpandedChildrenIntoView(TreeViewItem treeViewItem, bool value)
        {
            treeViewItem.SetValue(BringExpandedChildrenIntoViewProperty, value);
        }

        public static readonly DependencyProperty BringExpandedChildrenIntoViewProperty =
            DependencyProperty.RegisterAttached(
                "BringExpandedChildrenIntoView",
                typeof(bool),
                typeof(perTreeViewItemHelper),
                new UIPropertyMetadata(false, BringExpandedChildrenIntoViewChanged));

        private static void BringExpandedChildrenIntoViewChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            if (!(args.NewValue is bool))
                return;

            if (!(obj is TreeViewItem item))
                return;

            if ((bool)args.NewValue)
                item.Expanded += OnTreeViewItemExpanded;
            else
                item.Expanded -= OnTreeViewItemExpanded;
        }

        private static void OnTreeViewItemExpanded(object sender, RoutedEventArgs e)
        {
            // prevent this event bubbling up to any parent nodes
            e.Handled = true;

            if (!(sender is TreeViewItem item))
                return;

            // use DispatcherPriority.ContextIdle for all actions related to tree item expansion
            // this ensures that all UI elements for any newly visible children are created before any selection operation

            // first bring the last child into view
            Action action = () =>
                {
                    var lastChild = item.ItemContainerGenerator.ContainerFromIndex(item.Items.Count - 1) as TreeViewItem;
                    lastChild?.BringIntoView();
                };

            perDispatcherHelper.AddToQueue(action, DispatcherPriority.ContextIdle);

            // then bring the expanded item (back) into view
            action = () => { item.BringIntoView(); };

            perDispatcherHelper.AddToQueue(action, DispatcherPriority.ContextIdle);
        }
    }

Note that because Selected and Expanded are routed events, the handlers have to mark the arguments as handled to prevent the event bubbling up to any parent items.

As with the ListBox behavior I described in my previous post, there are calls to Dispatcher.BeginInvoke() with a DispatcherPriority parameter. However, as there could be multiple dispatcher operations required, they are added to a queue and executed in sequence. This is important when dealing indirectly (e.g. by setting bound properties from the ViewModel) with controls with ItemTemplates and / or Virtualisation, as it allows us to wait for a control to be fully rendered before we perform an operation on a visual item.

TreeView Style

This style, which is included as part of the wpf view library, provides

  • a coloured icon for the expander button
  • the colour scheme for the tree view item – as with the ListBox style, keeping the selected item highlighted with the same colour, even when the control loses focus
  • including the two scroll into view behaviors for an item
  • using the item style as the ItemContainerStyle in a TreeView control

    <Style x:Key="perExpandCollapseToggleStyle" TargetType="ToggleButton">
        <Setter Property="Focusable" Value="False" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ToggleButton">
                    <Grid
                        Width="10"
                        Height="10"
                        Background="Transparent">
                        <Path
                            x:Name="ExpanderGlyph"
                            Margin="1"
                            HorizontalAlignment="Left"
                            VerticalAlignment="Center"
                            Data="M 0,3 L 0,5 L 3,5 L 3,8 L 5,8 L 5,5 L 8,5 L 8,3 L 5,3 L 5,0 L 3,0 L 3,3 z"
                            Fill="LightGreen"
                            Stretch="None" />
                    </Grid>

                    <ControlTemplate.Triggers>
                        <Trigger Property="IsChecked" Value="True">
                            <Setter TargetName="ExpanderGlyph" Property="Data" Value="M 0,0 M 8,8 M 0,3 L 0,5 L 8,5 L 8,3 z" />
                            <Setter TargetName="ExpanderGlyph" Property="Fill" Value="Red" />
                        </Trigger>

                        <Trigger Property="IsEnabled" Value="False">
                            <Setter TargetName="ExpanderGlyph" Property="Fill" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <Style x:Key="perTreeViewItemContainerStyle" TargetType="{x:Type TreeViewItem}">

        <!--  Link the properties of perTreeViewItemViewModelBase to the corresponding ones on the TreeViewItem  -->
        <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
        <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
        <Setter Property="IsEnabled" Value="{Binding IsEnabled}" />

        <!--  Include the two "Scroll into View" behaviors  -->
        <Setter Property="vhelp:perTreeViewItemHelper.BringSelectedItemIntoView" Value="True" />
        <Setter Property="vhelp:perTreeViewItemHelper.BringExpandedChildrenIntoView" Value="True" />

        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type TreeViewItem}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" MinWidth="14" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                            <RowDefinition Height="*" />
                        </Grid.RowDefinitions>
                        <ToggleButton
                            x:Name="Expander"
                            Grid.Row="0"
                            Grid.Column="0"
                            ClickMode="Press"
                            IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
                            Style="{StaticResource perExpandCollapseToggleStyle}" />

                        <Border
                            x:Name="PART_Border"
                            Grid.Row="0"
                            Grid.Column="1"
                            Padding="{TemplateBinding Padding}"
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}">

                            <ContentPresenter
                                x:Name="PART_Header"
                                Margin="0,2"
                                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                ContentSource="Header" />

                        </Border>

                        <ItemsPresenter
                            x:Name="ItemsHost"
                            Grid.Row="1"
                            Grid.Column="1" />
                    </Grid>

                    <ControlTemplate.Triggers>
                        <Trigger Property="IsExpanded" Value="false">
                            <Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed" />
                        </Trigger>

                        <Trigger Property="HasItems" Value="false">
                            <Setter TargetName="Expander" Property="Visibility" Value="Hidden" />
                        </Trigger>

                        <!--  Use the same colors for a selected item, whether the TreeView is focussed or not  -->
                        <Trigger Property="IsSelected" Value="true">
                            <Setter TargetName="PART_Border" Property="Background" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
                            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}" />
                        </Trigger>

                        <Trigger Property="IsEnabled" Value="false">
                            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <Style TargetType="{x:Type TreeView}">
        <Setter Property="ItemContainerStyle" Value="{StaticResource perTreeViewItemContainerStyle}" />
    </Style>

The demo application for this post shows synchronisation between a TreeView and a ListBox. Whenever an item is selected in one control, it will be selected and scrolled into view in the other. Any checked items in the TreeView will be displayed in bold in the ListBox. Double-clicking on a ListBox item will expand that item in the TreeView.


Update – October 2018

It’s recently come to my attention that there was a flaw with the original code I produced for this article. When the Tree is running in full lazy load mode (which is what I generally tend to use), there’s no issue as you can’t access a node until its parent has been expanded. However if you just build the tree data all at once, and then select a node several levels deep in the structure, there can be issues with Dispatcher.BeginInvoke() operations overlapping and clashing. To get around this, I’ve made a few changes to the library classes.

1) perDispatcherHelper has been extended to allow a queue of operations to be built. This uses the MaxHeap (priority queue) class that I discussed in my last post. Queued operations are executed one at a time by calling ProcessQueueAsync(). Using a priority queue allows additional operations to be added, with the execution order being updated as appropriate.

2) The dispatcher operations are split into two groups. Those relating to node expansion are executed before those relating to selection. It makes no sense to try to select a node that hasn’t been fully built in the U.I. yet. In practical terms, there’s little difference in behaviour between DispatcherPriority.ApplicationIdle and DispatcherPriority.ContextIdle, but it provides a handy way to segment (and therefore order) the required operations.

3) When a node item is selected, the order of ancestor node expansion has been reversed – nodes closest to the root are expanded first.

I’ve added an additional demo project that shows this usage in action. Using the up/down boxes at the bottom of the View, you can pick the Id of any node in the data collection and clicking the button will select it in the tree, including scrolling the appropriate nodes into view.

Using a tool such as WPF Snoop, you can see how efficient the virtualisation aspect of a WPF TreeView is. Even though there are over 400,000 nested elements in the data item collection, the application only creates the bare minimum of U.I elements required to show the expanded items, and the response when selecting a tree item is virtually instantaneous.


In my next few posts, I’ll look at some aspects of WPF development that are often highlighted as excuses for not adopting the MVVM pattern. Using some of the techniques I’ve already covered in this series, I’ll build some library functionality that can be used to solve these ‘hard’ MVVM problems.

Don’t forget that all of the code samples for this blog are available on Github, along with my own personal C# / WPF library.

If you find this article useful, or you have any other feedback, please leave a comment below.

Leave a Reply

Your email address will not be published. Required fields are marked *