Monday, August 1, 2011

WPF - how to display different category in a TreeView using DataTemplateSelector

WPF TreeView typically displays hierarchical tree view by using HierarchicalDataTemplate. This HierarchicalDataTemplate allows you to display different types in different ways. For example, for class A type child node, one might want to display image and name while for class B child node, one might need to display name and size textblock. This usually can be archieved by using DataType attribute.

<HierarchicalDataTemplate x:Key="LargeAreaTemplate" DataType="{x:Type vm:LargeArea}" >
If one needs one hierarchical structure with consistent parent-child relationship, defining DataType in HierarchicalDataTemplate will do the trick. However, what if one needs two different hierarchies in a tree view? If two different categories (let's say,  for many-to-many relationship) should be shown in a give tree view, how can it be done with HierarchicalDataTemplate? The following picture shows an example.



There are 2 categories. By Area category shows Age child nodes. By Age category shows Area child nodes.












In a XAML, we can add different hierarchical data templates per each category. For [By Area] view, Area is parent and Age is child node type and vice versa for [By Age] view. If you look at below example, you can see that each category view has different data templates.


<Window x:Class="Tree.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="323" Width="253"
        xmlns:vm="clr-namespace:Tree"
        >
    
    <Window.Resources>
        <vm:MyTemplateSelector x:Key="MyTemplateSelector" />       
        
        <!-- AREA VIEW -->
        <HierarchicalDataTemplate x:Key="LargeAreaTemplate" DataType="{x:Type vm:LargeArea}" 
                                  ItemsSource="{Binding AgeCollection}"
                                  ItemTemplateSelector="{StaticResource MyTemplateSelector}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}" />
                <TextBlock Text=" : Size=" />
                <TextBlock Text="{Binding AreaSize}" />
            </StackPanel>
        </HierarchicalDataTemplate>
        <HierarchicalDataTemplate x:Key="SmallAreaTemplate" DataType="{x:Type vm:SmallArea}" 
                                  ItemsSource="{Binding AgeCollection}"
                                  ItemTemplateSelector="{StaticResource MyTemplateSelector}">            
            <StackPanel Orientation="Horizontal">                
                <TextBlock Text="{Binding Name}" />
                <TextBlock Text=" : Population=" />
                <TextBlock Text="{Binding Population}" />
            </StackPanel>            
        </HierarchicalDataTemplate>
        <HierarchicalDataTemplate x:Key="LeafAgeTemplate" DataType="{x:Type vm:Age}"                                   
                                  ItemTemplateSelector="{StaticResource MyTemplateSelector}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}" />
            </StackPanel>
        </HierarchicalDataTemplate>


        <!-- AGE VIEW -->
        <HierarchicalDataTemplate x:Key="AgeTemplate" DataType="{x:Type vm:Age}" 
                                  ItemsSource="{Binding AreaCollection}"
                                  ItemTemplateSelector="{StaticResource MyTemplateSelector}">               
            <StackPanel Orientation="Horizontal">                
                <TextBlock Text="{Binding Name}" />
            </StackPanel>
        </HierarchicalDataTemplate>
        <HierarchicalDataTemplate x:Key="LeafLargeAreaTemplate" DataType="{x:Type vm:LargeArea}"                                   
                                  ItemTemplateSelector="{StaticResource MyTemplateSelector}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}" />
                <TextBlock Text=" : Size=" />
                <TextBlock Text="{Binding AreaSize}" />
            </StackPanel>
        </HierarchicalDataTemplate>
        <HierarchicalDataTemplate x:Key="LeafSmallAreaTemplate" DataType="{x:Type vm:SmallArea}"                                   
                                  ItemTemplateSelector="{StaticResource MyTemplateSelector}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Name}" />
                <TextBlock Text=" : Population=" />
                <TextBlock Text="{Binding Population}" />
            </StackPanel>
        </HierarchicalDataTemplate>

        <!-- Style -->
        <Style TargetType="TreeViewItem">
            <Setter Property="IsExpanded" Value="True"/>
        </Style>                
    </Window.Resources>    
    
    <Grid Height="282" Width="230">
        <Button Content="By Area" Height="23" HorizontalAlignment="Left" Margin="20,20,0,0" 
                Name="btnArea" VerticalAlignment="Top" Width="90" Click="btnArea_Click" />
        <Button Content="By Age" Height="23" HorizontalAlignment="Right" Margin="0,20,26,0"
                Name="btnAge" VerticalAlignment="Top" Width="90" Click="btnAge_Click" />

        <TreeView Name="treeView1" HorizontalAlignment="Left" VerticalAlignment="Top" 
                  Margin="20,49,0,0" Width="184" Height="216" 
                  ItemsSource="{Binding Path=MyItems}"  
                  ItemTemplateSelector="{StaticResource MyTemplateSelector}">
        </TreeView>
    </Grid>    
</Window>

Since TreeView only selects one unique data template at a time for any given type, the sample above is using custom data template selector in ItemTemplateSelector. In a code behind file, the following class is defined.

public class MyTemplateSelector : DataTemplateSelector
   { 
      public override DataTemplate SelectTemplate(object item, DependencyObject container) 
      { 
         MethodInfo mi = container.GetType().GetMethod("FindResource") as MethodInfo;         
         if (mi != null)
         {
            if (MainWindow.ViewMode == ViewMode.ByArea)
            {
               switch (item.ToString())
               {
                  case "Tree.LargeArea":
                     return mi.Invoke(container, new object[] { "LargeAreaTemplate" }) as DataTemplate;
                  case "Tree.SmallArea":
                     return mi.Invoke(container, new object[] { "SmallAreaTemplate" }) as DataTemplate;
                  case "Tree.Age":
                     return mi.Invoke(container, new object[] { "LeafAgeTemplate" }) as DataTemplate;
               }
            }
            else
               switch (item.ToString())
               {
                  case "Tree.Age":
                     return mi.Invoke(container, new object[] { "AgeTemplate" }) as DataTemplate;
                  case "Tree.LargeArea":
                     return mi.Invoke(container, new object[] { "LeafLargeAreaTemplate" }) as DataTemplate;
                  case "Tree.SmallArea":
                     return mi.Invoke(container, new object[] { "LeafSmallAreaTemplate" }) as DataTemplate;
               }
         }      
         return null; 
      } 
   }


Basically what it does is to find appropriate data template and return it back to TreeView. If null is returned, TreeView searches for appropriate hierarchical data template for the DataType.

1 comment: