What's the proper way to build a data template for a TreeView node that contains multiple collections as children?

222 Views Asked by At

I have a class that contains multiple collections of properties:

class Foo{
   public ObservableCollection<Bar> Bars {get; set;}
   public ObservableCollection<Baz> Bazzes {get; set;}
}

I'm trying to display this in a TreeView, where the Foo node is at the root, and then under it is a node for the Bars collection containing each of the Bar elements as subnodes, and the same for the Bazzes collection. But I can't seem to get the data template right. The closest I've managed to get is like so:

                <HierarchicalDataTemplate DataType="{x:Type local:Foo}">
                    <TreeViewItem Header="Root">
                        <TreeViewItem Header="Bars" ItemsSource="{Binding Path=Bars}"/>
                        <TreeViewItem Header="Bazzes" ItemsSource="{Binding Path=Bazzes}"/>
                    </TreeViewItem>
                </HierarchicalDataTemplate>
                <DataTemplate DataType="{x:Type local:Bar}">
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Name}" />
                        <TextBlock Text=" (" Foreground="Blue" />
                        <TextBlock Text="{Binding Type}" Foreground="Blue" />
                        <TextBlock Text=")" Foreground="Blue" />
                    </StackPanel>
                </DataTemplate>
                <DataTemplate DataType="{x:Type local:Baz}">
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Name}" />
                    </StackPanel>
                </DataTemplate>

This displays a hierarchical tree with nodes I can open to display sub-items by clicking the little triangle, but when I try to click on any of the items, it selects the entire Foo with all of its sub-items as one big selection. I'm assuming this is because the nodes containing the collections are integrated into the template for Foo and so it's treating them as all being one big node somehow? But I don't know how to get the collections to show up as sub-nodes without doing it that way.

What's the correct way to do the type of setup I'm looking for, since this is obviously not quite right?

1

There are 1 best solutions below

2
AQuirky On

There are a couple of fundamental issues with your implementation. The first is that the Tree is simply a map onto the bound data structure which is expected to be a tree. First we have to make your Foo class a tree...

public class BarBazBase
{
    public string Name { get; set; }
    public string Type { get; set; }

}
public class Bar : BarBazBase
{
    public string BarSpecial { get; set; }
}
public class Baz : BarBazBase
{
    public string BazSpecial { get; set; }
}


public class Foo : ObservableCollection<ObservableCollection<BarBazBase>>
{
    public ObservableCollection<BarBazBase> Bars { get; set; } = new ObservableCollection<BarBazBase>();
    public ObservableCollection<BarBazBase> Bazzes { get; set; } = new ObservableCollection<BarBazBase>();
    public Foo()
    {
        Add(Bars);
        Add(Bazzes);
    }
}

Next we need a different template for each type of tree node. We therefore need a data template selector

public class BasBazTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        FrameworkElement fe = container as FrameworkElement;
        if(item is Foo)
        {
            return fe.FindResource("TreeHeader") as DataTemplate;
        }
        if (item is ObservableCollection<BarBazBase> baseCollection)
        {
            if (baseCollection.Count > 0 && baseCollection[0] is Bar)
                return fe.FindResource("BarHeader") as DataTemplate;
            else if (baseCollection.Count > 0 && baseCollection[0] is Baz)
                return fe.FindResource("BazHeader") as DataTemplate;
            else
                return null;
        }
        else if (item is Bar)
        {
            return fe.FindResource("BarItemTemplate") as DataTemplate;
        }
        else if (item is Baz)
        {
            return fe.FindResource("BazItemTemplate") as DataTemplate;
        }
        else
        {
            return null;
        }
    }
}

Finally we are ready to pull everything together in XAML...

       <TreeView x:Name="treeView" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Beige"
              ItemsSource="{Binding Root}" ItemTemplateSelector="{StaticResource BasBazTemplateSelector}">
        <TreeView.Resources>
            <HierarchicalDataTemplate x:Key="TreeHeader" ItemsSource="{Binding}" >
            </HierarchicalDataTemplate>
            <HierarchicalDataTemplate x:Key="BazHeader" ItemsSource="{Binding}">
                <Label>Baz</Label>
            </HierarchicalDataTemplate>
            <HierarchicalDataTemplate x:Key="BarHeader" ItemsSource="{Binding}">
                <Label>Bar</Label>
            </HierarchicalDataTemplate>
            <DataTemplate x:Key="BarItemTemplate">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Name}" />
                    <TextBlock Text=" (" Foreground="Blue" />
                    <TextBlock Text="{Binding Type}" Foreground="Blue" />
                    <TextBlock Text=")" Foreground="Blue" />
                </StackPanel>
            </DataTemplate>
            <DataTemplate x:Key="BazItemTemplate">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Name}" />
                </StackPanel>
            </DataTemplate>
        </TreeView.Resources>
    </TreeView>

A few loose ends...

<Window.Resources>
    <local:BasBazTemplateSelector x:Key="BasBazTemplateSelector"/>
</Window.Resources>

public Foo Root { get; set; }

        Root = new Foo();
        Root.Bars.Add(new Bar() { Name = "a", Type = "a0", BarSpecial = "a bar" });
        Root.Bars.Add(new Bar() { Name = "b", Type = "b0", BarSpecial = "another bar" });
        Root.Bazzes.Add(new Baz() { Name = "c", Type = "c0", BazSpecial = "a baz" });
        Root.Bazzes.Add(new Baz() { Name = "d", Type = "d0", BazSpecial = "another baz" });

This is the result...

enter image description here