R – How to mix databound and static levels in a TreeView

data-bindinghierarchicaldatatemplatetreeviewwpf

I have a collection of Database objects, each containing collections of Schema objects and User objects. I want to bind them to a TreeView, but adding additional static levels in the hierarchy, so that the resulting TreeView looks more or less like this:

<TreeView>
    <TreeViewItem Header="All the databases:">
        <TreeViewItem Header="Db1">
            <TreeViewItem Header="Here's all the schemas:">
                <TreeViewItem Header="Schema1"/>
                <TreeViewItem Header="Schema2"/>
            </TreeViewItem>
            <TreeViewItem Header="Here's all the users:">
                <TreeViewItem Header="User1"/>
                <TreeViewItem Header="User2"/>
            </TreeViewItem>
        </TreeViewItem>
        <TreeViewItem Header="Db2">
            <TreeViewItem Header="Here's all the schemas:">
                <TreeViewItem Header="Schema1"/>
                <TreeViewItem Header="Schema2"/>
            </TreeViewItem>
            <TreeViewItem Header="Here's all the users:">
                <TreeViewItem Header="User1"/>
                <TreeViewItem Header="User2"/>
            </TreeViewItem>
        </TreeViewItem>
    </TreeViewItem>
</TreeView>

I was able to get pretty close to what I want by using the following templates:

<Window.Resources>
    <HierarchicalDataTemplate DataType="{x:Type smo:Database}">
        <TreeViewItem Header="{Binding Path=Name}">
            <TreeViewItem Header="Here's all the schemas:" ItemsSource="{Binding Path=Schemas}"/>
            <TreeViewItem Header="Here's all the users:" ItemsSource="{Binding Path=Users}"/>
        </TreeViewItem>
    </HierarchicalDataTemplate>
    <DataTemplate DataType="{x:Type smo:Schema}">
        <TextBlock Text="{Binding Path=Name}"/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type smo:User}">
        <TextBlock Text="{Binding Path=Name}"/>
    </DataTemplate>
</Window.Resources>

Then in the code I set the binding like this:

TreeViewItem treeViewItem = new TreeViewItem();
treeViewItem.Header = "All the databases:";
treeViewItem.ItemsSource = server.Databases;
treeView.Items.Add(treeViewItem);

The resulting TreeView looks like I want it to, but it's not possible to select a particular schema or user. Apparently WPF sees the whole subtree rooted at a database node as a single item, and it only selects the whole thing. I need to be able to select a particular schema, user or database. How do I set the templates and bindings so that it works the way I need?

Best Answer

Oh man this is an incredibly frustrating task. I've tried doing it myself many times. I had a very similar requirement where I've got something like a Customer class that has both a Locations collection and a Orders collection. I wanted Locations and Orders to be "folders" in the tree view. As you've discovered, all the TreeView examples that show you how to bind to self-referencing types are pretty much useless.

First I resorted to manually building a tree of FolderItemNode and ItemNode objects that I would generate in the ViewModel but this defeated the purpose of binding because it would not respond to underlying collection changes.

Then I came up with an approach which seems to work pretty well.

  • In the above described object model, I created classes LocationCollection and OrderCollection. They both inherit from ObservableCollection and override ToString() to return "Locations" and "Orders" respectively.
  • I create a MultiCollectionConverter class that implements IMultiValueConverter
  • I created a FolderNode class that has a Name and Items property. This is the placeholder object that will represent your "folders" in the tree view.
  • Define hierarchicaldatatemplate's that use MultiBinding anywhere that you want to group multiple child collections into folders.

The resulting XAML looks similar to the code below and you can grab a zip file which has all the classes and XAML in a working example.

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:Local="clr-namespace:WpfApplication2"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">

    <Window.Resources>

        <!-- THIS IS YOUR FOLDER NODE -->
        <HierarchicalDataTemplate DataType="{x:Type Local:FolderNode}" ItemsSource="{Binding Items}">
            <Label FontWeight="Bold" Content="{Binding Name}" />
        </HierarchicalDataTemplate>

        <!-- THIS CUSTOMER HAS TWO FOLDERS, LOCATIONS AND ORDERS -->
        <HierarchicalDataTemplate DataType="{x:Type Local:Customer}">
            <HierarchicalDataTemplate.ItemsSource>
                <MultiBinding>
                    <MultiBinding.Converter>
                        <Local:MultiCollectionConverter />
                    </MultiBinding.Converter>
                    <Binding Path="Locations" />
                    <Binding Path="Orders" />
                </MultiBinding>
            </HierarchicalDataTemplate.ItemsSource>
            <Label Content="{Binding Name}" />
        </HierarchicalDataTemplate>

        <!-- OPTIONAL, YOU DON'T NEED SPECIFIC DATA TEMPLATES FOR THESE CLASSES -->
        <DataTemplate DataType="{x:Type Local:Location}">
            <Label Content="{Binding Title}" />
        </DataTemplate>
        <DataTemplate DataType="{x:Type Local:Order}">
            <Label Content="{Binding Title}" />
        </DataTemplate>

    </Window.Resources>

    <DockPanel>
        <TreeView Name="tree" Width="200" DockPanel.Dock="Left" />
        <Grid />
    </DockPanel>

</Window>

Folders in TreeView

Related Topic