Table of Contents
C# Tree View With Multi-Type Binding
I had an interesting problem where I need the leaves of the tree view to be different types, so in my tree view I have the following classes;
- Family
- Parent
- Child
- Pet
however notice that the Pet and Child classes are at the same level i.e. the leaf level.
Classes
Here are my classes
Family
public class Family : ViewModelBase { private ObservableCollection<Parent>? _parents; public ObservableCollection<Parent>? Parents { get { return _parents; } set { _parents = value; OnPropertyChanged(); } } private string _name; public string Name { get { return _name; } set { _name = value; OnPropertyChanged(); } } }
Parent
public class Parent : ViewModelBase { private string _name=string.Empty; public string Name { get { return _name; } set { _name = value; OnPropertyChanged(); } } private ObservableCollection<Child>? _children; public ObservableCollection<Child>? Children { get { return _children; } set { _children = value; OnPropertyChanged(); } } private ObservableCollection<Pet>? _pets; public ObservableCollection<Pet>? Pets { get { return _pets; } set { _pets = value; OnPropertyChanged(); } } }
Child
public class Child : ViewModelBase { private string _name; public string Name { get { return _name; } set { _name = value; OnPropertyChanged(); } } }
Pet
public class Pet : ViewModelBase { public string Name { get; set; } = string.Empty; }
View Model Base
public class ViewModelBase:INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
Family View Model
To create the data model we will create a View Model for the family and initialize the families observable collection.
using System.Collections.ObjectModel; namespace Family_Treeview { public class FamilyViewModel : ViewModelBase { public ObservableCollection<Family> Families { get; set; } public FamilyViewModel() { Families = new ObservableCollection<Family> { new Family { Parents = new ObservableCollection<Parent> { new Parent { Name = "Parent 1", Children = new ObservableCollection<Child> { new Child { Name = "Child 1" }, new Child { Name = "Child 2" } }, Pets = new ObservableCollection<Pet> { new Pet { Name = "Cat" }, new Pet { Name = "Dog" } } }, new Parent { Name = "Parent 2", Children = new ObservableCollection<Child> { new Child { Name = "Child 3" }, new Child { Name = "Child 4" } }, Pets = new ObservableCollection<Pet> { new Pet { Name = "Dog" } } } }, Name = "Family 1" }, new Family { Parents = new ObservableCollection<Parent> { new Parent { Name = "Parent 1", Children = new ObservableCollection<Child> { new Child { Name = "Child 1" }, new Child { Name = "Child 2" } }, Pets = new ObservableCollection<Pet> { new Pet { Name = "Cat" }, new Pet { Name = "Dog" } } }, new Parent { Name = "Parent 2", Children = new ObservableCollection<Child> { new Child { Name = "Child 3" }, new Child { Name = "Child 4" } }, Pets = new ObservableCollection<Pet> { new Pet { Name = "Dog" } } } }, Name = "Family 2" } }; } } }
Children And Pets Converter
To make this work we want to create a multi value converter that can take in an array of all objects below the parent and combine them into a single observable collection, this is what this converter does.
using System.Collections.ObjectModel; using System.Globalization; using System.Windows.Data; namespace Family_Treeview { public class ChildrenAndPetsConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { var children = values[0] as ObservableCollection<Child>; var pets = values[1] as ObservableCollection<Pet>; if (children == null || pets == null) return null; var combinedList = new ObservableCollection<object>(); foreach (var child in children) { combinedList.Add(child); } foreach (var pet in pets) { combinedList.Add(pet); } return combinedList; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }
Tree View Xaml
First thing we need to define a root node in Xaml terms its a header
<TreeView> <TreeViewItem Header="Families" FontSize="20">
Next we will bind the Tree View to the Families observable collection in the View Model.
<TreeView Name="FamilyTreeView2" ItemsSource="{Binding Families}" BorderThickness="0">
The Families observable collection contains a Family Class Which Contains a Parents Class and a Name Attribute for the Family Name.
<TreeView.Resources> <!-- DataTemplate for Family --> <HierarchicalDataTemplate DataType="{x:Type local:Family}" ItemsSource="{Binding Parents}"> <TextBlock Text="{Binding Name}" FontSize="20"/><!-- family Name --> </HierarchicalDataTemplate>
in the previous code block we set the item source, in this case we don’t know which item source to bind to since we have children and pets, so in this case we cant.
<!-- DataTemplate for Parent --> <HierarchicalDataTemplate DataType="{x:Type local:Parent}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" FontSize="20"/><!-- parent Name --> <TextBlock Text=" (Children)" Margin="5,0,0,0" FontSize="20"/> </StackPanel>
This is where the converter comes in, we pass both collections into the converter as a multi-binding to create a singular collection.
<HierarchicalDataTemplate.ItemsSource> <MultiBinding Converter="{StaticResource ChildrenAndPetsConverter}"> <Binding Path="Children"/> <Binding Path="Pets"/> </MultiBinding> </HierarchicalDataTemplate.ItemsSource> </HierarchicalDataTemplate>
Lastly we have to define two data templates so that we can bind to the correct attributes
<!-- DataTemplate for Child --> <DataTemplate DataType="{x:Type local:Child}"> <TextBlock Text="{Binding Name}" FontSize="20"/> </DataTemplate> <!-- DataTemplate for Pet --> <DataTemplate DataType="{x:Type local:Pet}"> <TextBlock Text="{Binding Name}" FontSize="20"/> </DataTemplate>
I Changed the attribute names for the Pet and Child class and as a result we have to change the names in the two data templates as shown below.
<!-- DataTemplate for Child --> <DataTemplate DataType="{x:Type local:Child}"> <TextBlock Text="{Binding ChildName}" FontSize="20"/> </DataTemplate> <!-- DataTemplate for Pet --> <DataTemplate DataType="{x:Type local:Pet}"> <TextBlock Text="{Binding PetName}" FontSize="20"/> </DataTemplate>
Completed Code.
<Window x:Class="Family_Treeview.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:Family_Treeview" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.Resources> <local:ChildrenAndPetsConverter x:Key="ChildrenAndPetsConverter"/> </Window.Resources> <Grid> <TreeView> <TreeViewItem Header="Families" FontSize="20"> <TreeView Name="FamilyTreeView2" ItemsSource="{Binding Families}" BorderThickness="0"> <TreeView.Resources> <!-- DataTemplate for Family --> <HierarchicalDataTemplate DataType="{x:Type local:Family}" ItemsSource="{Binding Parents}"> <TextBlock Text="{Binding Name}" FontSize="20"/> </HierarchicalDataTemplate> <!-- DataTemplate for Parent --> <HierarchicalDataTemplate DataType="{x:Type local:Parent}"> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" FontSize="20"/> <TextBlock Text=" (Children)" Margin="5,0,0,0" FontSize="20"/> </StackPanel> <HierarchicalDataTemplate.ItemsSource> <MultiBinding Converter="{StaticResource ChildrenAndPetsConverter}"> <Binding Path="Children"/> <Binding Path="Pets"/> </MultiBinding> </HierarchicalDataTemplate.ItemsSource> </HierarchicalDataTemplate> <!-- DataTemplate for Child --> <DataTemplate DataType="{x:Type local:Child}"> <TextBlock Text="{Binding Name}" FontSize="20"/> </DataTemplate> <!-- DataTemplate for Pet --> <DataTemplate DataType="{x:Type local:Pet}"> <TextBlock Text="{Binding Name}" FontSize="20"/> </DataTemplate> </TreeView.Resources> </TreeView> </TreeViewItem> </TreeView> </Grid> </Window>