Search code examples
c#wpftreeviewdatatemplatehierarchicaldatatemplate

Use TreeView hierarchy with HierarchicalDataTemplate from inside a DataTemplate


I have a WPF application which uses a TreeView, inside that TreeView there are multiple HierarchicalDataTemplates/DataTemplates for different types, each containing a ContentControl with a specific Template, like so:

TreeView
|- HierarchicalDataTemplate for Type a
|  |- ContentControl
|
|- DataTemplate for Type b
   |- ContentControl

The type b is built like this:

b
|-integer c
|-object d

d can be anything from an integer to a string, but it can also be a class containing a list. In that case I want to display the list of d using a HierarchicalDataTemplate inside the TreeView described above.

Is there a way to do that, or do I lose all connection to the hierarchy of the TreeView as soon as I enter the DataTemplate/ContentControl/Template?


Solution

  • For complex scenarios like this, you can implement a custom DataTemplateSelector. From your description I assume a data types like these for A and B, with properties for C and D:

    public class A
    {
    }
    
    public class B
    {
       public B(int c, object d)
       {
          C = c;
          D = d;
       }
    
       public int C { get; }
    
       public object D { get; }
    }
    

    You could create custom data templates for each type and purpose. For B, there would be both a DataTemplate for the regular types and a HierarchicalDataTemplate for when D is a collection:

    <TreeView ItemsSource="{Binding MyItems}">
       <TreeView.ItemTemplateSelector>
          <local:CustomDataTemplateSelector/>
       </TreeView.ItemTemplateSelector>
       <TreeView.Resources>
          <DataTemplate x:Key="ATemplate"
                        DataType="{x:Type local:A}">
             <TextBlock Text="This is an A."/>
          </DataTemplate>
          <DataTemplate x:Key="BTemplate" DataType="{x:Type local:B}">
             <StackPanel>
                <TextBlock Text="{Binding C}"/>
                <TextBlock Text="{Binding D}"/>
             </StackPanel>
          </DataTemplate>
          <HierarchicalDataTemplate x:Key="BHierarchicalTemplate"
                                    DataType="{x:Type local:B}"
                                    ItemsSource="{Binding D}">
             <TextBlock Text="{Binding C}"/>
          </HierarchicalDataTemplate>
       </TreeView.Resources>
    </TreeView>
    

    The x:Keys are needed to resolve to the data templates using the DataTemplateSelector. In this case, we would check if an item is of type A and use the ATemplate. If it is B, we check whether its template needs to be hierarchical or not by inspecting property D. If it is a collection - or in more general terms an IEnumerable, we use the hierarchical template. However, be aware that some types like string are enumerable, too, so we need to make a separate check.

    public class CustomDataTemplateSelector : DataTemplateSelector
    {
       private const string ATemplateName = "ATemplate";
       private const string BTemplateName = "BTemplate";
       private const string BHierarchicalTemplateName = "BHierarchicalTemplate";
    
       public override DataTemplate SelectTemplate(object item, DependencyObject container)
       {
          if (!(container is FrameworkElement frameworkElement))
             return base.SelectTemplate(item, container);
    
          // >= C# 6
          //switch (item)
          //{
          //   case A:
          //      return FindDataTemplate(frameworkElement, ATemplateName);
          //   case B b when b.D is string:
          //      return FindDataTemplate(frameworkElement, BTemplateName);
          //   case B b when b.D is IEnumerable:
          //      return FindDataTemplate(frameworkElement, BHierarchicalTemplateName);
          //   case B:
          //      return FindDataTemplate(frameworkElement, BTemplateName);
          //   default:
          //      return base.SelectTemplate(item, container);
          //}
          
          // >= C# 8
          return item switch
          {
             A => FindDataTemplate(frameworkElement, ATemplateName),
             B { D: string } => FindDataTemplate(frameworkElement, BTemplateName),
             B { D: IEnumerable } => FindDataTemplate(frameworkElement, BHierarchicalTemplateName),
             B => FindDataTemplate(frameworkElement, BTemplateName),
             _ => base.SelectTemplate(item, container)
          };
       }
    
          private static DataTemplate FindDataTemplate(FrameworkElement frameworkElement, string key)
       {
          return (DataTemplate)frameworkElement.FindResource(key);
       }
    }
    

    Whether you create constants for the template names or build the keys from the type names or expose properties to assign the data templates is up to your requirements.