WPF TreeView HierarchicalDataTemplate - binding to object with multiple child collections
Asked Answered
L

4

67

I am trying to get a TreeView to bind my collection so that all groups show nested groups and each group will show entry.

How can I use the HierarchicalDataTemplate so that the TreeView will process both SubGroups and Entries collection?

Groups show subgroups and entries:

Example:
Group1
--Entry
--Entry
Group2
--Group4
----Group1
------Entry
------Entry
----Entry
----Entry
--Entry
--Entry
Group3
--Entry
--Entry


Objects:


namespace TaskManager.Domain
{
    public class Entry
    {
        public int Key { get; set; }
        public string Name { get; set; }
    }
}

namespace TaskManager.Domain
{
    public class Group
    {
        public int Key { get; set; }
        public string Name { get; set; }

        public IList<Group> SubGroups { get; set; }
        public IList<Entry> Entries { get; set; }
    }
}

Test data:


namespace DrillDownView
{
    public class TestData
    {

        public IList<Group> Groups = new List<Group>();

        public void Load()
        {
            Group grp1 = new Group() { Key = 1, Name = "Group 1", SubGroups = new List<Group>(), Entries = new List<Entry>() };
            Group grp2 = new Group() { Key = 2, Name = "Group 2", SubGroups = new List<Group>(), Entries = new List<Entry>() };
            Group grp3 = new Group() { Key = 3, Name = "Group 3", SubGroups = new List<Group>(), Entries = new List<Entry>() };
            Group grp4 = new Group() { Key = 4, Name = "Group 4", SubGroups = new List<Group>(), Entries = new List<Entry>() };

            //grp1
            grp1.Entries.Add(new Entry() { Key=1, Name="Entry number 1" });
            grp1.Entries.Add(new Entry() { Key=2, Name="Entry number 2" });
            grp1.Entries.Add(new Entry() { Key=3,Name="Entry number 3" });

            //grp2
            grp2.Entries.Add(new Entry(){ Key=4, Name = "Entry number 4"});
            grp2.Entries.Add(new Entry(){ Key=5, Name = "Entry number 5"});
            grp2.Entries.Add(new Entry(){ Key=6, Name = "Entry number 6"});

            //grp3
            grp3.Entries.Add(new Entry(){ Key=7, Name = "Entry number 7"});
            grp3.Entries.Add(new Entry(){ Key=8, Name = "Entry number 8"});
            grp3.Entries.Add(new Entry(){ Key=9, Name = "Entry number 9"});

            //grp4
            grp4.Entries.Add(new Entry(){ Key=10, Name = "Entry number 10"});
            grp4.Entries.Add(new Entry(){ Key=11, Name = "Entry number 11"});
            grp4.Entries.Add(new Entry(){ Key=12, Name = "Entry number 12"});

            grp4.SubGroups.Add(grp1);
            grp2.SubGroups.Add(grp4);

            Groups.Add(grp1);
            Groups.Add(grp2);
            Groups.Add(grp3);
        }
    }
}

XAML:


<Window x:Class="DrillDownView.Window2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:TaskManager.Domain;assembly=TaskManager.Domain"
        Title="Window2" Height="300" Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <TreeView Name="GroupView" Grid.Row="0" Grid.Column="0" ItemsSource="{Binding}">
            <TreeView.Resources>
                <HierarchicalDataTemplate DataType="{x:Type local:Group}" ItemsSource="{Binding SubGroups}">
                    <TextBlock Text="{Binding Path=Name}" />
                </HierarchicalDataTemplate>
                <HierarchicalDataTemplate DataType="{x:Type local:Entry}" ItemsSource="{Binding Entries}">
                    <TextBlock Text="{Binding Path=Name}" />
                </HierarchicalDataTemplate>
            </TreeView.Resources>
        </TreeView>
    </Grid>
</Window>

XAML.CS:


public partial class Window2 : Window
{
    public Window2()
    {
        InitializeComponent();
        LoadView();
    }

    private void LoadView()
    {
        TestData data = new TestData();
        data.Load();
        GroupView.ItemsSource = data.Groups;
    }
}
Lannie answered 16/12, 2009 at 5:21 Comment(4)
Very looong post and is missing the question that you probably wanted to ask.Rumney
How can I use the HierarchicalDataTemplate so that treeview will process both SubGroups and Entries collection?Lannie
reformatted and moved some code around. Revert if you don't like it. Working on your question..Rumney
Is this a mistake? Setting the tree items source once in YAML and once in code. the code I am expecting to raise a exception. GroupView.ItemsSource = data.GroupsSalmon
R
125

A HierarchicalDataTemplate is a way of saying this is how you render this type of object and here is a property that can be probed to find the child nodes under this object.

Therefore you need a single property that returns the 'children' of this node. (e.g. if you can't make both Group and Entry derive from a common Node type)

public class Group 
{
    // ....
    public IList<object> Items
    {
        get
        {
            IList<object> childNodes = new List<object>();
            foreach (var group in this.SubGroups)
                childNodes.Add(group);
            foreach (var entry in this.Entries)
                childNodes.Add(entry);

            return childNodes;
        }
    }

Next you don't need a HierarchicalDataTemplate for entry since an entry doesn't have children. So the XAML needs to be changed to use the new Items property and a DataTemplate for Entry:

<TreeView Name="GroupView" Grid.Row="0" Grid.Column="0" ItemsSource="{Binding}">
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type local:Group}" ItemsSource="{Binding Items}">
            <TextBlock Text="{Binding Path=Name}" />
        </HierarchicalDataTemplate>
        <DataTemplate DataType="{x:Type local:Entry}" >
            <TextBlock Text="{Binding Path=Name}" />
        </DataTemplate>
    </TreeView.Resources>
</TreeView>

And here's what that looks like:

Screenshot of Output

Rumney answered 16/12, 2009 at 6:19 Comment(9)
+1 from me - because you validated my own answer on both points ;)Rondo
@Gishu: What do you think of my alternative implementation (see my answer)? I prefer to use a return type of IEnumerable except when I really use an IList. Additionally, this method allows us to utilize the yield keyword to simplify the code.Hofer
@Matthew.. nice improvement. The best code is the code that you don't have to write :) +1Rumney
That answer saved me ton of time. Thank you very much Gishu for the helpEmanation
@Gishu, if you have the time, would you mind taking a look at my question here: HierarchicalDataTemplate and Many-to-Many with Payload Binding Issue ? I would appreciate it very much--seem to have a very good handle on the concepts involved. Thank you for reading :)Remittee
@Rumney Any idea about the implementing same in a Windows phone 8 application.here is question #23909273Citrine
Thanks for the great example! The only thing I'd suggest is to use an interface instead of object.Nabors
Instead of adding into List<object> it's better to use CompositeCollectionYankee
Not only helped my problem but taught me the reasons why. Thank you Gishu. I was struggling understanding the connection.Moorfowl
H
13

Here is an alternative implementation of Gishu's answer that returns an IEnumerable rather than an IList, and makes use of the yield keyword to simplify the code:

public class Group
{
    ...

    public IEnumerable<object> Items
    {
        get
        {
            foreach (var group in this.SubGroups)
                yield return group;
            foreach (var entry in this.Entries)
                yield return entry;
        }
    }
}
Hofer answered 4/8, 2011 at 14:37 Comment(1)
Thank you! I was getting stuck and this helped so much. I wasn't able to get both entries and groups being displayed correctly at the same time. This saved me a lot of time.Rosierosily
R
12

I think you are most of the way there... with a tiny bit of rework you should get this working fairly easily...

I would suggest you create a base abstract class (or an interface, whichever you prefer) and inherit/implement it for both the Group and Entry class...

That way, you can expose a property within your Group object

public ObservableCollection<ITreeViewItem> Children { get; set; }

^at this point, you can make a decision if this replaces your lists of SubGroups and Entries, or merely appends them together and returns them in the property getter...

Now all you need is to populate the Children collection with either Group or Entry objects, and the HierarchicalDataTemplate will render correctly when the objects are placed in the TreeView..

One final thought, if Entry is always the 'bottom level' of the tree (ie has no children) then you do not need to define a HierarchicalDataTemplate for the Entry object, a DataTemplate will suffice.

Hope this helps :)

Rondo answered 16/12, 2009 at 6:17 Comment(2)
A basic working example would help the newbies grasp the concept. But regardless excellent thought.Secretarygeneral
I took your advice and loaded up generic JSON using the ITreeViewItem with raw JSON as input. My answer can be seen here How to display JSON in WPF TreeViewSecretarygeneral
B
3

This post helped me out when looking for a solution for the same issue: XAML binding to CompositeCollection using MultiBinding and CompositeCollectionConverter.

Briannabrianne answered 7/3, 2012 at 8:27 Comment(1)
Whilst this may theoretically answer the question, it would be preferable to include the essential parts of the answer here, and provide the link for reference.Wheelbarrow

© 2022 - 2024 — McMap. All rights reserved.