marxlaml
marxlaml

Reputation: 351

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?

Upvotes: -1

Views: 80

Answers (1)

thatguy
thatguy

Reputation: 22119

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.

Upvotes: 1

Related Questions