Click here to Skip to main content
13,143,708 members (28,146 online)
Click here to Skip to main content
Add your own
alternative version

Stats

10.2K views
932 downloads
34 bookmarked
Posted 5 Sep 2017

Advanced WPF TreeViews Part 1 of n

, 16 Sep 2017
Rate this:
Please Sign up or sign in to vote.
A list of advanced tips & tricks on WPF TreeViews

Introduction

This article is a review of patterns in WPF tree view implementations. We survey different scenarios and try to hint why some approaches are sometimes not so useful. A prerequisite for understanding the information presented here is that you have a good and clear understanding on the article by Josh Smith [1].

This article tries to answer advanced questions that I could not find covered in other articles and are at best scattered over some MSDN or Stackoverflow pages. We will try to answer questions such as:

  • Should we use an ObservableCollection or an ObservableDictionary to implement the collection of children below each node?
     
  • How can I use Interfaces with an HierarchicalDataTemplate?

We'll be trying to explain each point with the attached code examples that should be helpfull to relate things into practical applications.

Background

The site www.codeplex.com is shutting down and we are asked to move projects to GitHub. I am using this process to review my current control implementations, update or improve them were I see fit and post the results on GitHub and Nuget. This process led me to reviewing my current file system control implementation and it became soon clear to me that I should review the Edit-in-place control and have a little study of how different tree view implementations compare with each other in terms of their features. The result of that study is a prototyp of visual studios project explorer (see attachment or GitHub project InplaceEditBoxLib library).

I would like to use this article and the attached code to summarize how I got there and give anchor points for future implementations of other tree view applications.

Index

Binding Nodes to Collections

A tree view has children, and childrens children and so forth:

+-> Root
    +- Child 1
    |
    +- Child 2
    |
    +- Child 3
       |
       +-> Child 4

...and each of the collections (here Root, Child 1 - Child 3, and Child 4) can be implemented in quit a number of different ways using different types of collections (List, ObservableCollection etc.) and bindings. But the real intersting question is, what type of collection is best for my purpose?

My purpose of a tree view is best reflected in this youtube video captured from the actual WPF edit-in-place control implementation. The video summarizes my requirements and should be helpful when comparing different solutions and their properties:

  1. CollectionView
  2. SortableObservableCollection,
  3. SortableObservableDictionary, or
  4. SortableObservableDirectionaryCollection

I was mainly looking for the following requirements:

  1. List of items is sorted alphabetically
  2. Renaming an item results in a re-sort of the displayed list. The renamed item is brought into view at its correct position. The renamed item has input focus.
  3. All items have a unique name within each list of child nodes.

There are some related requirements, such as, handling input of invalid characters and checking the input string for its length. I omit these requirements here since they apply more on the in-place-edit control than the structure bound to the tree view. Please check these details in the GitHub repository.

An overview of the Sample Applications

Each implementation scenario mentioned can be verified in the attached code samples. For these samples, I have assumed, that we would want to list GitHub users and their projects. So, each sample appliaction contains at a minimum the following classes:

The attached behaviours are just standard classes that attach to an event, (here item selection and selection changed of the tree view) and forward that event based mechanism in a bindable ICommand fashion. I usually try to use these when I want to bring a selected tree view item into view or when I want to know what the currently selected item is.

The AppViewModel is the main viewmodel that connects to the DataContxt of the MainWindow. It contains an instance of the GitHubViewModel which represents the root viewmodel of the GitHub Tree:

The user can select 1 item in the tree, change the string in the textbox, and click the Rename button to rename the item. The renamed item is then expected to move to its new correct position and be selected there as I tried to outline in the video above. I chose this sample implementation because its much easier to understand and debug than using the edit-in-place textbox control above.

It is important to understand that the GitHubViewModel object is not the root item that is visible in the tree. The root item is maintained in the Root property of that class:

public ObservableCollection<GitHubItemViewModel> Root
{
    get
    {
        return _Root;
    }
}

The tree of items consists of GitHubItemViewModel objects. Only 1 root item is required in any tree. We can, therefore, use the above root definition throughout all samples. You should be able to see that the code in the GitHubViewModel always ensures that we have only 1 root item at any given time.

More interesting than the root item is the Children in the GitHubItemViewModel in each subsequent sample which we will discuss next.

CollectionView

This sample implementation contains the standard 101 type class approach towards implementing a WPF tree view with an ObservableCollection throughout all Children collections. The only interesting part to note here is the IMultiValueConverter IListToListCollectionViewConverter:

public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
    var Length = values.Length;
    if (Length >= 1 && Length < 3)
    {
        // First parameter should be list to be sorted
        var IList = values[0] as IList;

        // Second parameter should be name to sort by
        var SortName = string.Empty;
        if (Length > 1)
            SortName = values[1].ToString();

        // Third parameter SortDirection is optional
        var SortDirection = ListSortDirection.Ascending;
        if (Length > 2)
            SortDirection = values[2] is ListSortDirection
              ? (ListSortDirection)values[2] : (values[2] is string
              ? (ListSortDirection)Enum.Parse(typeof(ListSortDirection), values[2].ToString()) : SortDirection);

        var Result = new ListCollectionView(IList);
        Result.SortDescriptions.Add(new SortDescription(SortName, SortDirection));
        Result.IsLiveSorting = true;

        return Result;
    }
    return null;
}

and here is the XAML implementation:

<HierarchicalDataTemplate>
    <HierarchicalDataTemplate.ItemsSource>
        <MultiBinding Converter="{StaticResource IListToListCollectionViewConverter}">
            <Binding Path="Children" />
            <Binding Source="{StaticResource NameOfSortProperty}" />
        </MultiBinding>
    </HierarchicalDataTemplate.ItemsSource>
    <Grid>
        <TextBlock Grid.Column="1" Text="{Binding Name}"

                   VerticalAlignment="Center"/>
    </Grid>
</HierarchicalDataTemplate>

The converter expects 2 parameters, number 1 being the usual Children collection and number 2 being the property by which the collection is expected to be sorted. The sort itself is then efficiently implemented with a ListCollectionView which ensures the correct sorting order and maintains it even if the data is changed.

Analysis

We can see that the sorting works as expected if we play with the sample. But we can also see that the selected item dissappears and cannot be retrieved unless the user would start to browse for it.<!-- Suitable for Virtual Trees? -->

The above problem could probably be fixed by attaching to some event (if it exists) that would be invoked whenever the ListCollectionView is resorted. We would then have to remember the previously changed node, select it and bring it into view via an attached behaviour, and it looks like we are back in business here.

But looking at the above solution (if it exists) makes me realize that we would have to synchronize multiple events (the renaming of an item, the resorting, and the selection/bring into view). Which may or may not be possible but seems to be exceedingly complicated, if we consider that WPF comes with 2 threads, 1 for UI and 1 for the background application. Its also worth noting that the Edit-in-place solution already contains a complicated play of view and viewmodel events and actions - so making it even more complicated just to scroll to the right view position does not sound right unless there is a simple solution that I might be mising right now.

Note also that this solution does not ensure unique names in collection of child nodes. Clearly, this could be implemented but does come at an additional cost of searching before inserting or changing items.

SortableObservableCollection

This solution is almost identical to the previously discussed solution except that we use a small modification of the ObservableCollection and make it sortable via the implemented class as suggested on the referenced StackOverflow page.

This solution improves over the previous solution because we can now control the (re-)sorting from within the viewmodel and can trigger this whenever the items Name property changes or a new item is inserted. And whats more, the SortableObservableCollection actually re-sorts on change such that the currently selected item stays selected, the bring-into-view-behaviour scrolls to the new position and keeps the focus where it should be.

Analysis

The nice behaviour described here comes at the extra cost of calling a (re-)sort method that should not be too expensive if we consider that renaming might not be that frequent and the number of items shown per node may be considerable small (a few thousand nodes is probably more than enough for most real applications).

So, all in all this solution is really promising but it does not solve the unique name issue unless we search the list of Children for a given name prior to actually inserting or changing a name. This could be done with a simple LinQ statement. But this could also be a performance issue. So, lets see next, if this couldn't be solved more efficiently with a dictionary.

SortableObservableDictionary

The SortableObservableDictionary implementation in this sample is based on an implementation from Dr. WPF back in 2007 plus a small modification as suggested in the referenced blog post [2]. We note that each Children property exposed now contains a collection over key/value pairs:

public ObservableSortedDictionary<string, GitHubItemViewModel> Children
{
    get
    {
        return _Children;
    }
}

We can see that the listed items in the test application behave as expected. Every item moves on renaming where it should be going - so sorting is maintained out of the box. The storage of unique names is supported and we can verify this if we try to insert a value twice since this will result in an exception being thrown.

This implementation really rocks and Dr. WPF still gets my 5 stars for sure :-) The only drawback I can see here is the fact that we now have to bind to a key/value pair instead of binding to a single property as usual:

<HierarchicalDataTemplate ItemsSource="{Binding Value.Children}">
    <Grid DataContext="{Binding Value}">
        <TextBlock Grid.Column="1" Text="{Binding Name}"

                   VerticalAlignment="Center"/>
    </Grid>
</HierarchicalDataTemplate>

To me thats an OK price to pay if you ask me but I will review one more solution based on a sorted observable collection that keeps track of unique names without exposing key/value pairs next.

SortableObservableDirectionaryCollection

The solution presented in this sample is based on the previously discussed SortableObservableCollection with the addition that an inner dictionary is maintained to ensure unique names.

Analysis

This solution also adheres to all requirements mentioned above. So, from a users point of view: Its providing an equivalent experience as the much more involed implementation from Dr. WPF.

We also not that it exposes no key/value pairs but only a collection of values which comes at the price that we have to have a custom implementation that must be customized to each tree view implementation since sorting and keeping track of unique entities would otherwise not work.

But we could work around this problem by specifying an interface that defines the key such that we could always re-use the same observabke class template provided that the interface is implemented:

public interface IKey<TKey>
{
    TKey Key { get; }
}

We should also ensure that available methods of adding or changing items are either disabled via overload and exception being thrown or unavailable by hiding the SortableObservableDirectionaryCollection behind an interface. The later option seems more natural and I will discuss it further below.

Summary

In summary we can conclude that we have found at least 2 options that seem to be straight forward enough to implement and maintain. I am personaly leaning towards the solution by Dr. WPF since it seems to be most natural and I don't realy mind binding to a key/value pair either. But I look forward to hear what others say about this.

Do you know other solutions worth being considered her?

Grouping and Sorting

The last section pretty much solves the problem of keeping items in a tree view sorted and unique. And as soon as we talk about sorting I hear people say:

 

What about grouping - my application displays different types of items (eg. Folder and File) and results should be grouped and each group should be sorted in turn.

I can confidently say to these people that grouping is just a specialized sub-case of the sorting case discussed above. You can group with the same technique as you sort if you can generate a key that will cluster entries into groups and sorts them within. So lets assume we have the following list of 5 items:

  1. dirk (directory)
  2. temp (directory)
  3. xtreme.cs (file)
  4. windows (directory)
  5. notes.txt (file)

and we want to group by directories and files and sort within. Then, all we would have to to-do here is to generate a sort key, like this:

  1. a_dirk
  2. a_temp
  3. a_windows
  4. f_notes.txt
  5. f_xtreme.cs

This sortkey is obviously no longer suitable for display to the user but we could sort by just one argument and get 2 requirements solved :-) But this solution does not result in a unique key anymore.

We know that we cannot have a directory with the same name as a file (within one directory) but the above change on the sort key would let us do it. So, the solution to this problem is to either:

  1. try and find all possible names and types of a name before changing or inserting a new item or
  2. keep track of sort keys and unique keys in two separate collections.

I actually implemented the 2nd option in the prototype of the project explorer. You can review the demo implementation for the edit-in-place control if you need more practical guidance towards something that works and can be put throug a debugger. Make sure to review the public string GenSortKey(ISolutionBaseItem item) method and all related methods in the referenced class.

Tips & Tricks

BindingProxy

+-> Root
    +- Child 1
    |
    +- Child 2
    |
    +- Child 3
       |
       +-> Child 4

I clearly remember in my first implementations that I was pretty confused because I had no clue of how I should ever get from a deep node like Child 4 back to the root or the other way around. I actually ended up implementing tree editing functions in each child node - which in itself is not too bad but can be considered as a debugging hell.

A much better approach is to say Child 4 is a node where we want to perform some operation on, eg.: Rename it, and the implementation of that operation is in the class that hosts the Root. Fortunately, that is possible and makes life so much easier.

Part of the above solution is a BindingProxy. A code sample snippet is for example here and you can see its application here. The following line creates the object in the context of the object below the root:

<bindLocal:BindingProxy x:Key="DataContextProxy" Data="{Binding Solution}" />

and the next statement binds the command in that context and naturally supplies the tree view item as a parameter:

<MenuItem Command="{Binding Path=Data.StartRenameCommand, Source={StaticResource DataContextProxy}}"

          CommandParameter="{Binding}"

          Header="Rename"

          ToolTip="Rename this item" />

This solution looks weird at first sight but it works every time. And those are the things that come to my mind when I see Josh Smith's writing that WPF is in comparison to WinForms so different that you actually have take out your brain, reverse it by 180 degree and put it back in.

Parent Property

So, this is really cool. We can now implement our tree view operations in one place and operate on items since they can be a parameter of a call via command binding. But what if I want to remove an item? - does this mean that I have to traverse from the root to the given item in order to remove it through its parent's Children collection?

The answer to the above question is of course no. We can structure our viewmodel items such that they contain a parent property that links back to the immidiate parent of each item (except for the root which can have null in this place):

public interface IViewModelItem : INotifyPropertyChanged
{
  IViewModelItem Parent { get; }
}

We can still supply the item to be removed and the parent of that item using an IMultiValueConverter that puts all mentioned parameters into a Tuple (or any other object) and sends it to the bound Remove command. The Remove command can then unpack the parameters send and simply call: parent.Remove(item).

Implementing the Parent property really is simple since we just have to assure that all items in the tree implement the above interface and we know that trees are usually build from the root to the leaf - so supplying the parent parameter to the item's constructor is almost natural in the process.

These 2 tricks BindingProxy and Parent property are things that I'd like to recommend to everyone who considers this as news. OK, now that we have a basic understanding of how we organize the tree views we can look at advancing with interfaces.

Using HierarchicalDataTemplate with Interfaces

We know about the benefits of implementing interfaces instead of exposing complete classes or objects to the outside world of a library. The problem that we cover in this section is the simple truth that binding an HierarchicalDataTemplate to a property inside an interface (instead of binding to the property of a class) will not work.

Lets consider the code in the SortableObservableDirectionaryCollection 2 sample and assume for some reason that we arrived at a solution containing two or more HierarchicalDataTemplates and we have to make out their difference through their associated type of classes rather than going for the enumerations approach shown in the previous solutions:

xmlns:vm="clr-namespace:SortableObservableDirectionaryCollection.ViewModels"
...
<HierarchicalDataTemplate DataType="{x:Type vm:GitHubUserViewModel}"

                          ItemsSource="{Binding Children}">
    <Grid>
        <TextBlock Grid.Column="1" Text="{Binding Name}"

                   VerticalAlignment="Center"/>
    </Grid>
</HierarchicalDataTemplate>

<HierarchicalDataTemplate DataType="{x:Type vm:GitHubProjectViewModel}"

                          ItemsSource="{Binding Children}">
    <Grid>
        <TextBlock Grid.Column="1" Text="{Binding Name}"

                   VerticalAlignment="Center"/>
    </Grid>
</HierarchicalDataTemplate>

...and now we want to move everything into one modul, hide implementation details in classes and make only interfaces available to the outside world - so, the namespace changes to:

xmlns:vm="clr-namespace:SortableObservableDirectionaryCollection.Interfaces"

and the typing changes from:

  • vm:GitHubUserViewModel and vm:GitHubProjectViewModel
  • to
  • vm:IGitHubUserViewModel and vm:IGitHubProjectViewModel.

So, we now use interface instead of viewmodels. You will find that the template selection will no longer work if you make the suggested change above - and the question now is whether a HierarchicalDataTemplate can work with Interfaces and what are the exact conditions to make that work?

There are 2 options to resolve this issue.

Option 1 - Using only one HierarchicalDataTemplate

It turns out that we can use a HierarchicalDataTemplate with interfaces if we manage to map all ItemTemplate definitions through one HierarchicalDataTemplate defined inside the TreeView.ItemTemplate as shown in the sample application here.

<TreeView.ItemTemplate>
    <HierarchicalDataTemplate ItemsSource="{Binding Children}">
        <Grid xmlns:loc="clr-namespace:InplaceEditBoxLib.Local;assembly=InplaceEditBoxLib"

            >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>

            <Image Grid.Column="0" Height="16" Margin="3,0"

                   Focusable="False"

                   Source="{Binding Converter={StaticResource ISolutionBaseItemToImageConverter}}"

                   VerticalAlignment="Center" />

            <TextBlock Grid.Column="1"

                       Text="{Binding Path=DisplayName, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"

                       ToolTip="{Binding Description, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"

            />
        </Grid>
    </HierarchicalDataTemplate>
</TreeView.ItemTemplate>

The above code binds to interfaces since the viewmodels in the SolutionLib project of that solution are all internals. Nevertheless, the solution works with this implicit binding to interfaces approach. It should be noted that we now have to use converter and other such goodies to display different imcons for different items. But this is no rocket science in WPF but standard business - so, there is really nothing wrong with this approach.

In the next section we are going to consider a solution that would help us if for some reason we were forced to use more than one HierarchicalDataTemplate to deifne all tree node.

Option 2 - ItemTemplateSelector

An alternative option to implementing interfaces with HierarchicalDataTemplates is the implementation of an ItemTemplateSelector that can select the correct HierarchicalDataTemplate based on a given interface.

An ItemTemplateSelector is an object that observes a type of a given parameter object and translates this type into the correct ItemTemplate. There a number of different ways to setup an ItemTemplateSelector selector but the classic is to:

  • Declare all HierarchicalDataTemplates in a resource section with an x:key
  • Attach each instance of the HierarchicalDataTemplates to an instance of an ItemTemplateSelector in the resource section
  • Attach the instance of the ItemTemplateSelector as StaticResource to the ItemTemplateSelector property of a tree view.

Lets consider the code in the SortableObservableDirectionaryCollection 3 sample. An ItemTemplateSelector is usually one class that has properties to connect HierarchicalDataTemplates to it and there is often one override which is necessary to costumize the selection process of interfaces versus correct DataTemplate:

public class GitHubItemDataTemplateSelector : DataTemplateSelector
{
    public DataTemplate UserTemplate { get; set; }
    public DataTemplate ProjectTemplate { get; set; }

    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        if (item is IGitHubProjectViewModel)
          return ProjectTemplate;

        if (item is IGitHubUserViewModel)
          return UserTemplate;

        // Got here because given argument was not understood :-(
        throw new System.ArgumentException();
  }
}

The SelectDataTemplate method is invoked each time when the treeview see another item and wants to determine the correct DataTemplate. I omit the XAML code here but you can review it in the MainWindow.xaml of the SortableObservableDirectionaryCollection 3 sample.

Hiding Details of the ObservableCollection

It occurs sometimes that we implement a custom method CustomAdd(param1, param2 ...) of adding nodes into the tree under certain conditions. In these circumstances we sometimes find ourselves in the situation that we have to expose something like an ObservableCollection:

public ObservableCollection<Person> { get; }

in order to have the treeview, listview or any other ItemsControl bind to it. The problem here is often that users of this collection could just use the Add() or Remove() method of the collection to manipulate the viewmodel without going through the custom programming logic. So the question here is:

How can I expose the ObservableCollection without exposing the methods that I'd like to guard? And the answer is surprisingly simple. We can store the viewmodel in its own library and just expose an IEnumerable<...> interface instead of the propety above:

public IEnumerable<Person> { get; }

The IEnumerable<...> has no members for manipulating the collection. So, any one outside of the library will have a hard time manipulating it. The binding of the treeview or other WPF controls still work because the WPF binding system sidesteps the interface and talks to the object's properties more directly than its usually possible.

Summary

I have tried to explain why I think that the SortableObservableDictionary and the SortableObservableDictionaryCollection seem to be the best base for an implementation of treeviews in WPF. I have also given some basic tricks and hints towards implementing treeviews with exposed interfaces, instead of classes, hoping that this will lead you to a more professional implementation. I really hope you find this information useful and it saves you time as I have certainly invested some time following the wrong road - learning - and then retraycing my steps to lay them out in this article. I look forward to your feedback.

This is all we really need to know to get started with basic techniques that are helpful towards implementing better tree views in WPF - have a look at part 2 of this series if you are interested in handling large date sets efficiently.

References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Dirk Bahle
Germany Germany
The Windows Presentation Foundation (WPF) and C# are among my favorites since I started developing Edi:

https://github.com/Dirkster99/Edi

and a few other projects on GitHub. I am normally an algorithms and structure type person but WPF has such interesting UI sides that I cannot help myself but get into this and MVVM.

https://de.linkedin.com/in/dirkbahle

You may also be interested in...

Pro

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.170915.1 | Last Updated 16 Sep 2017
Article Copyright 2017 by Dirk Bahle
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid