0

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?

nighthawk
  • 773
  • 7
  • 30
  • It would be straightforward without class B, and A only having ItemsC. The ItemsD member in C could just be null or empty. – Clemens Jul 23 '20 at 12:41
  • Then all boils down to a single class X with an Id, a Name and a `List Items` property. – Clemens Jul 23 '20 at 12:42
  • Can't modify structure, it has to be like that. Its location which has different types of devices, A has devices of type B and C (B and C have same base class if it helps). – nighthawk Jul 23 '20 at 12:47
  • You can probably wrap A into a class with a property like this: https://stackoverflow.com/a/3673232/1136211 – Clemens Jul 23 '20 at 13:10

2 Answers2

1

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>
BionicCode
  • 1
  • 4
  • 28
  • 44
  • Combined it with solution from above - works but it doesn't look great. Will try to improve. – nighthawk Jul 25 '20 at 11:35
  • What do you mean by it doesn't look good? Styling? You have to pretty it up. It was just an example on how to build a proper tree structure, allowing different data types. – BionicCode Jul 25 '20 at 11:37
  • Also note that "*solution from above*" makes no sense. Users of this site can control the order in which answers are listed. – Clemens Jul 25 '20 at 11:40
  • I've messed up replies, I used BionicCode answer and one @Clemens wrote was good idea/inspiration what to do next. Yes, styling is and issue but I think it can be improved. – nighthawk Jul 26 '20 at 08:43
  • I have added the modified default `Style` of the `TreeViewItem` to adjust the positioning of the item expander. A `Style` that targets `TreeView` removes the border of the nested `TreeView`. Now the complete `TreeView` will look like expected. – BionicCode Jul 26 '20 at 09:47
1

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>
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • Used this solution, it's kinda ugly (because of nested TreeView) but its ok as stopgap solution until we devise a prettier one. Or if someone has an idea how to make it looking like default treeview, please write. – nighthawk Jul 25 '20 at 11:34
  • You probably wanted to write this comment to the other answer, There is no nested TreeView here. – Clemens Jul 25 '20 at 11:38
  • Yes, I mixed them up but anyway it was useful answer, gave me idea what to do next. – nighthawk Jul 26 '20 at 08:39