A TreeView, a HierarchicalDataTemplate, and a 2D collection walk into a bar ...

Bea has a handy post describing how to group items in a collection using a CollectionViewSource. I was looking at that, and a post on the WPF forum from markovuksanovic, and for fun created a version of Bea’s example that uses a 2D collection instead of the CollectionViewSource. (I found a bunch of HierarchicalDataTemplate examples using CollectionViewSource or XmlDataProvider, but couldn’t find any using nested collections.) Anyway, here’s the result …

First, by 2D collection, I mean a collection whose items are themselves collections, like a 2D array. The “parent” collection here is a collection of AnimalCategory objects, each of which has a category name and a collection of Animal objects for that category. So AnimalCategory looks like (all of this is in a namespace named “HierarchicalDataTemplateTest”):

namespace HierarchicalDataTemplateTest

{

    ...

    public class AnimalCategory

 

        private string _category;

        public string Category

        {

            get { return _category; }

  set { _category = value; }

        }

        private ObservableCollection<Animal> _animals;

        public ObservableCollection<Animal> Animals

        {

            get

            {

                if (_animals == null)

                    _animals = new ObservableCollection<Animal>();

                return _animals;

            }

        }

        public AnimalCategory()

        {

        }

        public AnimalCategory(

                    string category,

                    ObservableCollection<Animal> animals)

        {

            _category = category;

            _animals = animals;

        }

    }

    ...

}

 

… and Animal looks like:

namespace HierarchicalDataTemplateTest

{

    ...

    public class Animal

    {

        private string _name;

        public string Name

        {

            get { return _name; }

            set { _name = value; }

        }

        public Animal()

        {

        }

        public Animal(string name)

        {

            _name = name;

  }

    }

    ...

}

 

… and these get used in a sample Window application, whose code looks like:

public partial class Window1 : System.Windows.Window

{

    static public ObservableCollection<AnimalCategory> AnimalCategories

        = new ObservableCollection<AnimalCategory>();

    public Window1()

    {

        InitializeComponent();

        ObservableCollection<Animal> animals = new ObservableCollection<Animal>();

        animals.Add(new Animal("California Newt"));

        animals.Add(new Animal("Tomato Frog"));

        animals.Add(new Animal("Green Tree Frog"));

        AnimalCategories.Add( new AnimalCategory("Amphibians", animals) );

        animals = new ObservableCollection<Animal>();

        animals.Add(new Animal("Golden Silk Spider"));

        animals.Add(new Animal("Black Widow Spider"));

        AnimalCategories.Add(new AnimalCategory("Spiders", animals));

    }

 

That is, our Window1 has an collection named AnimalCategories of AnimalCategory objects, and each of those has a collection of Animal objects.

The markup in our Window displays these in a hierarchy – animals in their categories – using a TreeView. In this case, the TreeView is bound to the static AnimalCategories collection that we created in the above code:

<Window x:Class="HierarchicalDataTemplate.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    Title="HierarchicalDataTemplate" Height="300" Width="300"

    xmlns:local="clr-namespace:HierarchicalDataTemplateTest"

    >

  <!-- Create a TreeView, and have it source data from

       the AnimalCategories collection -->

  <TreeView ItemsSource="{x:Static local:Window1.AnimalCategories}">

    <!-- Specify the template that will display a node

         from AnimalCategories. I.e., one each for “Amphibians”

         and “Spiders” in this sample. It will get its nested

         items from the "Animals" property of each item -->

    <TreeView.ItemTemplate>

      <HierarchicalDataTemplate ItemsSource="{Binding Path=Animals}">

        <!-- Display the AnimalCategory by showing it's Category string -->

        <TextBlock FontWeight="Bold" Text="{Binding Path=Category}" />

        <!-- Specify the nested template for the individual Animal items

             that are within the AnimalCategories. E.g. “California Newt”, etc. -->

        <HierarchicalDataTemplate.ItemTemplate>

          <DataTemplate>

     <TextBlock Text="{Binding Path=Name}"/>

          </DataTemplate>

        </HierarchicalDataTemplate.ItemTemplate>

       

      </HierarchicalDataTemplate>

    </TreeView.ItemTemplate>

  </TreeView>

</Window>

 

(Note that the "xmlns:local='clr-namespace:HierarchicalDataTemplateTest'" is a reference to the CLR namespace that holds the Animal and AnimalCategory classes.)

 

The end result is:

 

 

 

 

Just to make it more interesting, let’s play with some different options. Recall that TreeView is an ItemsControl. ItemsControl can get its items from the ItemsSource property, as the above example shows. But ItemsControl also has its own built-in collection, which is the Items property. So instead of the code creating a special collection for the AnimalCategories objects, it could just add the AnimalCategory objects to the Items property. That is, make this change in the markup (the yellow part is new):

<TreeView ItemsSource="{x:Static local:Window1.AnimalCategories}" Name="TreeView1">

… and update Window1.xaml.cs to use the TreeView’s Items collection:

public partial class Window1 : System.Windows.Window

{

    static public ObservableCollection<AnimalCategory> AnimalCategories

        = new ObservableCollection<AnimalCategory>();

    public Window1()

    {

        InitializeComponent();

        ObservableCollection<Animal> animals = new ObservableCollection<Animal>();

        animals.Add(new Animal("California Newt"));

        animals.Add(new Animal("Tomato Frog"));

        animals.Add(new Animal("Green Tree Frog"));

        AnimalCategories.Add( new AnimalCategory("Amphibians", animals) );

        TreeView1.Items.Add(new AnimalCategory("Amphibians", animals));

        animals = new ObservableCollection<Animal>();

        animals.Add(new Animal("Golden Silk Spider"));

        animals.Add(new Animal("Black Widow Spider"));

        AnimalCategories.Add(new AnimalCategory("Spiders", animals));

        TreeView1.Items.Add(new AnimalCategory("Spiders", animals));

   }

}

… and you get the same application.

 

Finally, one last sample. The above code is still “new”-ing a lot of objects, and creating objects is what Xaml was invented for. So reduce the Window1 code to its minimal form:

public partial class Window1 : System.Windows.Window

{

    static public ObservableCollection<AnimalCategory> AnimalCategories

        = new ObservableCollection<AnimalCategory>();

    public Window1()

    {

        InitializeComponent();

        ObservableCollection<Animal> animals = new ObservableCollection<Animal>();

        animals.Add(new Animal("California Newt"));

        animals.Add(new Animal("Tomato Frog"));

        animals.Add(new Animal("Green Tree Frog"));

        TreeView1.Items.Add(new AnimalCategory("Amphibians", animals));

        animals = new ObservableCollection<Animal>();

        animals.Add(new Animal("Golden Silk Spider"));

        animals.Add(new Animal("Black Widow Spider"));

        TreeView1.Items.Add(new AnimalCategory("Spiders", animals));

    }

}

… and create those animals in the Xaml instead (updated lines in yellow):

<Window x:Class="HierarchicalDataTemplate.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    Title="HierarchicalDataTemplate" Height="300" Width="300"

    xmlns:local="clr-namespace:HierarchicalDataTemplateTest"

    >

  <!-- Create a TreeView, and have it source data from

       the AnimalCategories collection -->

  <TreeView Name="TreeView1">

    <!-- Specify the template that will display a node

         from AnimalCategories. It will get its nested

         items from the "Animals" property of each item -->

    <TreeView.ItemTemplate>

      <HierarchicalDataTemplate ItemsSource="{Binding Path=Animals}">

        <!-- Display the AnimalCategory by showing it's Category string -->

        <TextBlock FontWeight="Bold" Text="{Binding Path=Category}" />

        <!-- Specify the nested template for the individual Animal items

             that are within the AnimalCategory items. -->

        <HierarchicalDataTemplate.ItemTemplate>

          <DataTemplate>

            <TextBlock Text="{Binding Path=Name}"/>

          </DataTemplate>

        </HierarchicalDataTemplate.ItemTemplate>

       

  </HierarchicalDataTemplate>

    </TreeView.ItemTemplate>

    <local:AnimalCategory Category="Amphibians">

      <local:AnimalCategory.Animals>

        <local:Animal Name="California Newt" />

        <local:Animal Name="Tomato Frog" />

        <local:Animal Name="Green Tree Frog" />

      </local:AnimalCategory.Animals>

    </local:AnimalCategory>

    <local:AnimalCategory Category="Spiders">

      <local:AnimalCategory.Animals>

        <local:Animal Name="Golden Silk Spider" />

        <local:Animal Name="Black Widow Spider" />

      </local:AnimalCategory.Animals>

    </local:AnimalCategory>

  </TreeView>

       

</Window>

Note here that the <AnimalCategory> tags are directly under the <TreeView> tag; since content of a <TreeView> goes automatically to the Items property, this markup is equivalent to the previous “TreeView.Items.Add…” code.

 

hdt.jpg