nighthawk
nighthawk

Reputation: 842

WPF TreeView template with different classes

How to write XAML for WPF TreeView so when I bind ViewModel with following structure?

public class A
{
    int Id { get; set; }
    string Name { get; set; }
    public List<B> ItemsB { get; set; }
    public List<C> ItemsC { get; set; }
}

public class B
{
    int Id { get; set; }
    string Name { get; set; }
}

public class C
{
    int Id { get; set; }
    string Name { get; set; }
    public List<D> ItemsD { get; set; }
}
public class D
{
    int Id { get; set; }
    string Name { get; set; }
}

TreeView should look something like this:

-Item1 (class A)
    - Id: 1, Name: Item 1b (class B)
    - Id: 2, Name: Item 2b (class B)
    - Id: 1, Name: Item 3c (class C)
          - Id: 1, Name: Item 4d (class D)
-Item2 (class A)
    - Id: 2, Name: Item 1c (class C)
    - Id: 3, Name: Item 2b (class B)
    - Id: 3, Name: Item 3c (class C)
...

I've tried with HierarchicalDataTemplate and DataTemplate but all I can get is list of A items with only ItemsB listed as child nodes but never ItemsC. Its a bit complicated since we are mixing different objects (Bs and Cs) in same hierarchical level. All solutions I tried are something like this but I only get 1st level (A), never the child items.

How should XAML be set for this case?

Upvotes: 0

Views: 1276

Answers (2)

BionicCode
BionicCode

Reputation: 29028

If you don't want to change the tree structure (class structure):

<TreeView.Resources>
  <HierarchicalDataTemplate DataType="{x:Type A}" ItemsSource="{Binding ItemsB}">
    <StackPanel>
      <TextBlock Text="{Binding Id}" />
      <TreeView ItemsSource="{Binding ItemsC}" />
    </StackPanel>
  </HierarchicalDataTemplate>
  <DataTemplate DataType="{x:Type B}">
    <StackPanel>
      <TextBlock Text="{Binding Id}" />
    </StackPanel>
  </DataTemplate>
  <HierarchicalDataTemplate DataType="{x:Type C}" ItemsSource="{Binding ItemsD}">
    <StackPanel>
      <TextBlock Text="{Binding Id}" />
    </StackPanel>
  </HierarchicalDataTemplate>
  <DataTemplate DataType="{x:Type D}">
    <StackPanel>
      <TextBlock Text="{Binding Id}" />
    </StackPanel>
  </DataTemplate>
</TreeView.Resources>

The Style to adjust the appearance. It's the default TreeViewItem style taken from Microsoft Docs: TreeView Styles and Templates with a modified "Expander" positioning and removed border of the nested TreeView to maintain the default look. You can apply the Style (and its resources) locally by setting the TreeView.ItemContainerStyle property or by putting the Style into a ResourceDictionary within the scope of the target TreeView(e.g., App.xaml):

<!-- Remove the border of the nested TreeView -->
<Style TargetType="TreeView">
  <Setter Property="BorderThickness" Value="0" />
</Style>

<Color x:Key="GlyphColor">#FF444444</Color>
<Color x:Key="SelectedBackgroundColor">#FFC5CBF9</Color>
<Color x:Key="SelectedUnfocusedColor">#FFDDDDDD</Color>

<Style x:Key="ExpandCollapseToggleStyle"
       TargetType="ToggleButton">
  <Setter Property="Focusable"
          Value="False" />
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="ToggleButton">
        <Grid Width="15"
              Height="13"
              Background="Transparent">
          <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="CheckStates">
              <VisualState x:Name="Checked">
                <Storyboard>
                  <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                 Storyboard.TargetName="Collapsed">
                    <DiscreteObjectKeyFrame KeyTime="0"
                                            Value="{x:Static Visibility.Hidden}" />
                  </ObjectAnimationUsingKeyFrames>
                  <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                 Storyboard.TargetName="Expanded">
                    <DiscreteObjectKeyFrame KeyTime="0"
                                            Value="{x:Static Visibility.Visible}" />
                  </ObjectAnimationUsingKeyFrames>
                </Storyboard>
              </VisualState>
              <VisualState x:Name="Unchecked" />
              <VisualState x:Name="Indeterminate" />
            </VisualStateGroup>
          </VisualStateManager.VisualStateGroups>
          <Path x:Name="Collapsed"
                HorizontalAlignment="Left"
                VerticalAlignment="Center"
                Margin="1,1,1,1"
                Data="M 4 0 L 8 4 L 4 8 Z">
            <Path.Fill>
              <SolidColorBrush Color="{DynamicResource GlyphColor}" />
            </Path.Fill>
          </Path>
          <Path x:Name="Expanded"
                HorizontalAlignment="Left"
                VerticalAlignment="Center"
                Margin="1,1,1,1"
                Data="M 0 4 L 8 4 L 4 8 Z"
                Visibility="Hidden">
            <Path.Fill>
              <SolidColorBrush Color="{DynamicResource GlyphColor}" />
            </Path.Fill>
          </Path>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

<Style x:Key="TreeViewItemFocusVisual">
  <Setter Property="Control.Template">
    <Setter.Value>
      <ControlTemplate>
        <Border>
          <Rectangle Margin="0,0,0,0"
                     StrokeThickness="5"
                     Stroke="Black"
                     StrokeDashArray="1 2"
                     Opacity="0" />
        </Border>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

<Style x:Key="{x:Type TreeViewItem}"
       TargetType="{x:Type TreeViewItem}">
  <Setter Property="Background"
          Value="Transparent" />
  <Setter Property="HorizontalContentAlignment"
          Value="{Binding Path=HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
  <Setter Property="VerticalContentAlignment"
          Value="{Binding Path=VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
  <Setter Property="Padding"
          Value="1,0,0,0" />
  <Setter Property="Foreground"
          Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
  <Setter Property="FocusVisualStyle"
          Value="{StaticResource TreeViewItemFocusVisual}" />
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type TreeViewItem}">
        <Grid>
          <Grid.ColumnDefinitions>
            <ColumnDefinition MinWidth="19"
                              Width="Auto" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
          </Grid.ColumnDefinitions>
          <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
          </Grid.RowDefinitions>
          <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="SelectionStates">
              <VisualState x:Name="Selected">
                <Storyboard>
                  <ColorAnimationUsingKeyFrames Storyboard.TargetName="Bd"
                                                Storyboard.TargetProperty="(Panel.Background).
            (SolidColorBrush.Color)">
                    <EasingColorKeyFrame KeyTime="0"
                                         Value="{StaticResource SelectedBackgroundColor}" />
                  </ColorAnimationUsingKeyFrames>
                </Storyboard>
              </VisualState>
              <VisualState x:Name="Unselected" />
              <VisualState x:Name="SelectedInactive">
                <Storyboard>
                  <ColorAnimationUsingKeyFrames Storyboard.TargetName="Bd"
                                                Storyboard.TargetProperty="(Panel.Background).
            (SolidColorBrush.Color)">
                    <EasingColorKeyFrame KeyTime="0"
                                         Value="{StaticResource SelectedUnfocusedColor}" />
                  </ColorAnimationUsingKeyFrames>
                </Storyboard>
              </VisualState>
            </VisualStateGroup>
            <VisualStateGroup x:Name="ExpansionStates">
              <VisualState x:Name="Expanded">
                <Storyboard>
                  <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
                                                 Storyboard.TargetName="ItemsHost">
                    <DiscreteObjectKeyFrame KeyTime="0"
                                            Value="{x:Static Visibility.Visible}" />
                  </ObjectAnimationUsingKeyFrames>
                </Storyboard>
              </VisualState>
              <VisualState x:Name="Collapsed" />
            </VisualStateGroup>
          </VisualStateManager.VisualStateGroups>

          <!-- Adjust the positioning of the item expander -->
          <ToggleButton x:Name="Expander"
                        Style="{StaticResource ExpandCollapseToggleStyle}"
                        ClickMode="Press"
                        VerticalAlignment="Top"
                        Margin="0,2,0,0"
                        IsChecked="{Binding IsExpanded, 
    RelativeSource={RelativeSource TemplatedParent}}" />

          <Border x:Name="Bd"
                  Grid.Column="1"
                  Background="{TemplateBinding Background}"
                  BorderBrush="{TemplateBinding BorderBrush}"
                  BorderThickness="{TemplateBinding BorderThickness}"
                  Padding="{TemplateBinding Padding}">
            <ContentPresenter x:Name="PART_Header"
                              ContentSource="Header"
                              HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" />
          </Border>
          <ItemsPresenter x:Name="ItemsHost"
                          Grid.Row="1"
                          Grid.Column="1"
                          Grid.ColumnSpan="2"
                          Visibility="Collapsed" />
        </Grid>
        <ControlTemplate.Triggers>
          <Trigger Property="HasItems"
                   Value="false">
            <Setter TargetName="Expander"
                    Property="Visibility"
                    Value="Hidden" />
          </Trigger>
          <MultiTrigger>
            <MultiTrigger.Conditions>
              <Condition Property="HasHeader"
                         Value="false" />
              <Condition Property="Width"
                         Value="Auto" />
            </MultiTrigger.Conditions>
            <Setter TargetName="PART_Header"
                    Property="MinWidth"
                    Value="75" />
          </MultiTrigger>
          <MultiTrigger>
            <MultiTrigger.Conditions>
              <Condition Property="HasHeader"
                         Value="false" />
              <Condition Property="Height"
                         Value="Auto" />
            </MultiTrigger.Conditions>
            <Setter TargetName="PART_Header"
                    Property="MinHeight"
                    Value="19" />
          </MultiTrigger>
        </ControlTemplate.Triggers>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

Recommended

You should definitely change the class structure and introduce a common base type:

interface IDevice
{
  int Id { get; set; }
  List<IDevice> Items { get; set; }
}

class A : IDevice
{
  public A()
  {
    this.Items = new List<IDevice> { new B(), new C() };
  }
}

class B : IDevice
{}

You can now add every type that implements IDevice to the child collection, even mixed. Simply add a HierachricalDataTemplate for each implementation of IDevice:

<TreeView.Resources>
  <HierarchicalDataTemplate DataType="{x:Type A}" ItemsSource="{Binding Items}">
    <TextBlock Text="{Binding Id}" />
  </HierarchicalDataTemplate>
  <HierarchicalDataTemplate DataType="{x:Type B}" ItemsSource="{Binding Items}">
    <TextBlock Text="{Binding Id}" />
  </HierarchicalDataTemplate>
  <HierarchicalDataTemplate DataType="{x:Type C}" ItemsSource="{Binding Items}">
    <TextBlock Text="{Binding Id}" />
  </HierarchicalDataTemplate>
  <HierarchicalDataTemplate DataType="{x:Type D}" ItemsSource="{Binding Items}">
    <TextBlock Text="{Binding Id}" />
  </HierarchicalDataTemplate>
</TreeView.Resources>

Upvotes: 1

Clemens
Clemens

Reputation: 128147

You could combine the ItemsB and ItemsC collections of class A into a CompositeCollection.

Since HierarchicalDataTemplate.ItemsSource is not a collection, but a BindingBase it seems not possible to do this directly in XAML. You may however write a Binding Converter:

public class ClassAItemsConverter : IValueConverter
{
    public object Convert(
        object value, Type targetType, object parameter, CultureInfo culture)
    {
        var a = (A)value;

        return new CompositeCollection
        {
            new CollectionContainer() { Collection = a.ItemsB },
            new CollectionContainer() { Collection = a.ItemsC }
        };
    }

    public object ConvertBack(
        object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }
}

The templates would look like this:

<Window.Resources>
    <local:ClassAItemsConverter x:Key="AItemsConverter"/>

    <HierarchicalDataTemplate DataType="{x:Type local:A}"
        ItemsSource="{Binding Converter={StaticResource AItemsConverter}}">
        <TextBlock Text="{Binding Name}"/>
    </HierarchicalDataTemplate>

    <HierarchicalDataTemplate DataType="{x:Type local:C}"
        ItemsSource="{Binding ItemsD}">
        <TextBlock Text="{Binding Name}"/>
    </HierarchicalDataTemplate>

    <DataTemplate DataType="{x:Type local:B}">
        <TextBlock Text="{Binding Name}"/>
    </DataTemplate>

    <DataTemplate DataType="{x:Type local:D}">
        <TextBlock Text="{Binding Name}"/>
    </DataTemplate>
</Window.Resources>

Upvotes: 1

Related Questions