Peregrine's View

Yet another C# / WPF / MVVM blog …

WPF General

WPF Behaviors – TreeView

Time Spent Amongst Trees is Never Wasted Time
Katrina Mayer

In a previous post, I described how a Behavior class 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 TreeView controls – 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 (ItemsSource) of the tree, and lets the data templates defined in the View take care of things. The WPF TreeView control is very 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 the child item collection, 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 include hierarchical data, one of those would be the ideal place to put all that functionality – perTreeViewItemViewModelBase

    public class perTreeViewItemViewModelBase : perViewModelBase
    {
        // a dummy item used in lazy loading mode, ensuring that each node has at least one child so that the expand button is shown
        private static perTreeViewItemViewModelBase LazyLoadingChildIndicator { get; } 
            = new perTreeViewItemViewModelBase { Caption = "Loading Data ..." };

        private bool InLazyLoadingMode { get; set; }
        private bool LazyLoadTriggered { get; set; }
        private bool LazyLoadCompleted { get; set; }
        private bool RequiresLazyLoad => InLazyLoadingMode && !LazyLoadTriggered;

        // Has Children been overridden (e.g. to point at some private internal collection) 
        private bool LazyLoadChildrenOverridden => InLazyLoadingMode && !Equals(LazyLoadChildren, _childrenList);

        private readonly perObservableCollection<perTreeViewItemViewModelBase> _childrenList 
            = new perObservableCollection<perTreeViewItemViewModelBase>();

        /// <summary>
        /// LazyLoadingChildIndicator ensures a visible expansion toggle button in lazy loading mode 
        /// </summary>
        protected void SetLazyLoadingMode()
        {
            ClearChildren();
            _childrenList.Add(LazyLoadingChildIndicator);

            IsExpanded = false;
            InLazyLoadingMode = true;
            LazyLoadTriggered = false;
            LazyLoadCompleted = false;
        }

        private string _caption;

        public string Caption
        {
            get => _caption;
            set => Set(nameof(Caption), ref _caption, value);
        }

        public void ClearChildren()
        {
            _childrenList.Clear();
        }

        /// <summary>
        /// Add a new child item to this TreeView item
        /// </summary>
        /// <param name="child"></param>
        public void AddChild(perTreeViewItemViewModelBase child)
        {
            if (LazyLoadChildrenOverridden)
                throw new InvalidOperationException("Don't call AddChild for an item with LazyLoad mode set & LazyLoadChildren has been overridden");

            if (_childrenList.Any() && _childrenList.First() == LazyLoadingChildIndicator)
                _childrenList.Clear();

            _childrenList.Add(child);

            SetChildPropertiesFromParent(child);
        }

        protected void SetChildPropertiesFromParent(perTreeViewItemViewModelBase child)
        { 
            child.Parent = this;

            // if this node is checked then all new children added are set checked 
            if (IsChecked.GetValueOrDefault())
                child.SetIsCheckedIncludingChildren(true);

            ReCalculateNodeCheckState();
        }

        protected void ReCalculateNodeCheckState()
        {
            var item = this;

            while (item != null)
            {
                if (item.Children.Any() && !Equals(item.Children.FirstOrDefault(), LazyLoadingChildIndicator))
                {
                    var hasIndeterminateChild = item.Children.Any(c => c.IsEnabled && !c.IsChecked.HasValue);

                    if (hasIndeterminateChild)
                    {
                        item.SetIsCheckedThisItemOnly(null);
                    }
                    else
                    {
                        var hasSelectedChild = item.Children.Any(c => c.IsEnabled && c.IsChecked.GetValueOrDefault());
                        var hasUnselectedChild = item.Children.Any(c => c.IsEnabled && !c.IsChecked.GetValueOrDefault());

                        if (hasUnselectedChild && hasSelectedChild)
                        {
                            item.SetIsCheckedThisItemOnly(null);
                        }
                        else
                        {
                            item.SetIsCheckedThisItemOnly(hasSelectedChild);
                        }
                    }
                }

                item = item.Parent;
            }
        }

        private void SetIsCheckedIncludingChildren(bool? value)
        {
            if (IsEnabled)
            {
                _isChecked = value;
                RaisePropertyChanged(nameof(IsChecked));

                foreach (var child in Children)
                {
                    if (child.IsEnabled)
                    {
                        child.SetIsCheckedIncludingChildren(value);
                    }
                }
            }
        }

        private void SetIsCheckedThisItemOnly(bool? value)
        {
            _isChecked = value;
            RaisePropertyChanged(nameof(IsChecked));
        }

        /// <summary>
        /// Add multiple children to this TreeView item
        /// </summary>
        /// <param name="children"></param>
        public void AddChildren(IEnumerable<perTreeViewItemViewModelBase> children)
        {
            foreach (var child in children)
            {
                AddChild(child);
            }
        }

        /// <summary>
        /// Remove a child item from this TreeView item
        /// </summary>
        public void RemoveChild(perTreeViewItemViewModelBase child)
        {
            _childrenList.Remove(child);
            child.Parent = null;

            ReCalculateNodeCheckState();
        }

        public perTreeViewItemViewModelBase Parent { get; private set; }

        private bool? _isChecked = false;

        public bool? IsChecked
        {
            get => _isChecked;
            set
            {
                if (Set(nameof(IsChecked), ref _isChecked, value))
                {
                    foreach (var child in Children)
                    {
                        if (child.IsEnabled)
                        {
                            child.SetIsCheckedIncludingChildren(value);
                        }
                    }

                    Parent?.ReCalculateNodeCheckState();
                }
            }
        }

        private bool _isExpanded;

        public bool IsExpanded
        {
            get => _isExpanded;
            set
            {
                if (Set(nameof(IsExpanded), ref _isExpanded, value) && value && RequiresLazyLoad)
                {
                    TriggerLazyLoading();
                }
            }
        }

        private bool _isEnabled = true;

        public bool IsEnabled
        {
            get => _isEnabled;
            set => Set(nameof(IsEnabled), ref _isEnabled, value);
        }

        public void TriggerLazyLoading()
        {
            var unused = DoLazyLoadAsync();
        }

        private async Task DoLazyLoadAsync()
        {
            if (LazyLoadTriggered)
            {
                return;
            }

            LazyLoadTriggered = true;

            var lazyChildrenResult = await LazyLoadFetchChildren()
                .EvaluateFunctionAsync()
                .ConfigureAwait(false);

            LazyLoadCompleted = true;

            if (lazyChildrenResult.IsCompletedOk)
            {
                var lazyChildren = lazyChildrenResult.Data;

                foreach (var child in lazyChildren)
                {
                    SetChildPropertiesFromParent(child);
                }

                // If LazyLoadChildren has been overridden then just refresh the check state (using the new children) 
                // and update the check state (in case any of the new children is already set as checked)
                if (LazyLoadChildrenOverridden)
                {
                    ReCalculateNodeCheckState();
                }
                else
                {
                    AddChildren(lazyChildren); // otherwise add the new children to the base collection.
                }
            }

            RefreshChildren();
        }

        /// <summary>
        /// Get the children for this node, in Lazy-Loading Mode
        /// </summary>
        /// <returns></returns>
        protected virtual Task<perTreeViewItemViewModelBase[]> LazyLoadFetchChildren()
        {
            return Task.FromResult(new perTreeViewItemViewModelBase[0]);
        }

        /// <summary>
        /// Update the Children property
        /// </summary>
        public void RefreshChildren()
        {
            RaisePropertyChanged(nameof(Children));
        }

        /// <summary>
        /// In LazyLoading Mode, the Children property can be set to something other than
        /// the base _childrenList collection - e.g as the union of two internal collections
        /// </summary>
        public IEnumerable<perTreeViewItemViewModelBase> Children => LazyLoadCompleted
                                                                    ? LazyLoadChildren
                                                                    : _childrenList;

        /// <summary>
        /// How are the children held when in lazy loading mode.
        /// </summary>
        /// <remarks>
        /// Override this as required in descendent classes - e.g. if Children is formed from a union
        /// of multiple internal child item collections (of different types) which are populated in LazyLoadFetchChildren()
        /// </remarks>
        protected virtual IEnumerable<perTreeViewItemViewModelBase> LazyLoadChildren => _childrenList;

        private bool _isSelected;

        public bool IsSelected
        {
            get => _isSelected;
            set
            {
                // if unselecting we don't care about anything else other than simply updating the property
                if (!value)
                {
                    Set(nameof(IsSelected), ref _isSelected, false);
                    return;
                }

                // Build a priority queue of operations
                //
                // All operations relating to tree item expansion are added with priority = DispatcherPriority.ContextIdle, so that they are
                // sorted before any operations relating to selection (which have priority = DispatcherPriority.ApplicationIdle).
                // This ensures that the visual container for all items are created before any selection operation is carried out.
                //
                // First expand all ancestors of the selected item - those closest to the root first
                //
                // Expanding a node will scroll as many of its children as possible into view - see perTreeViewItemHelper, but these scrolling
                // operations will be added to the queue after all of the parent expansions.
                var ancestorsToExpand = new Stack<perTreeViewItemViewModelBase>();

                var parent = Parent;
                while (parent != null)
                {
                    if (!parent.IsExpanded)
                    {
                        ancestorsToExpand.Push(parent);
                    }

                    parent = parent.Parent;
                }

                while (ancestorsToExpand.Any())
                {
                    var parentToExpand = ancestorsToExpand.Pop();
                    perDispatcherHelper.AddToQueue(() => parentToExpand.IsExpanded = true, DispatcherPriority.ContextIdle);
                }

                // Set the item's selected state - use DispatcherPriority.ApplicationIdle so this operation is executed after all
                // expansion operations, no matter when they were added to the queue.
                //
                // Selecting a node will also scroll it into view - see perTreeViewItemHelper
                perDispatcherHelper.AddToQueue(() => Set(nameof(IsSelected), ref _isSelected, true), DispatcherPriority.ApplicationIdle);

                // note that by rule, a TreeView can only have one selected item, but this is handled automatically by 
                // the control - we aren't required to manually unselect the previously selected item.

                // execute all of the queued operations in descending DispatcherPriority order (expansion before selection)
                var unused = perDispatcherHelper.ProcessQueueAsync();
            }
        }

        public override string ToString()
        {
            return Caption;
        }

        /// <summary>
        /// What's the total number of child nodes beneath this one
        /// </summary>
        public int ChildCount => Children.Count() + Children.Sum(c => c.ChildCount);
    }

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 LazyLoadChildren 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 …

  • TreeViewHelper – to add a bindable selected item property
  • TreeViewItemHelper – 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 adds a new property – BoundSelectedItem, which we can use as the target for data binding in the TreeView.

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

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 a pair of 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 focused 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 were 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 perMaxHeap (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 display of a data bound 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.