Search code examples
c#wpftreeview

How to display two or more properties of an object in a TreeView in WPF, only one of which being the Hierarchical items?


I have an observable collection of nested objects which I am displaying in a TreeView in WPF.

public class NestedContainer
{
    public FXR1.FXContainer myContainer { get; set; }
    public string XID { get { return myContainer.XID; } }
    public FXR1.FXActionData ActionData { get { return myContainer.ActionData; } }
    public FXR1.FXModifier Modifier { get { return myContainer.Modifier; } }
    public List<NestedContainer> childContainers { get; set; } = new List<NestedContainer>();
}

I was successful in creating the nested structure that I want, using the following XAML code it displays in a TreeView like so:

<TreeView x:Name="FXNodeTree" Grid.Row="1" Grid.Column="1" ItemsSource="{Binding}">
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type local:NestedContainer}" ItemsSource="{Binding Path=childContainers}">
            <TextBlock Text="{Binding Path=XID}" />
        </HierarchicalDataTemplate>
    </TreeView.Resources>
</TreeView>

Current display example

However, I am also trying to display the ActionData and Modifier (if any) associated with each Container, as items in the TreeView located just below the Container's childContainers. These items do not have child containers of their own, so they are just additional pieces of info belonging to each Container. My initial thought after some research was to use Multibinding and bind ActionData and Modifier in addition to childContainers, but I was unable to find any similar examples of this so I am unsure if this is the ideal solution. My knowledge of WPF is very entry level, so I apologize if this is a very basic problem or if I'm missing an obvious solution. Any help is very much appreciated.


Solution

  • If I understood you correctly, you want to display additional information of each node data.

    For this purpose, you should override the default ControlTeplate of the TreeViewItem. This way you can inject a second content host below the items host (ItemsPresenter).
    If you introduce an attached property to hold a DataTemplate that describes the additional content you can avoid to subclass TreeViewItem. This attached property can be defined on any suitable type. This example chooses the MainWindow.

    To avoid memory leaks, the binding source must always implement INotifyPropertyChanged or preferably implement properties as dependency properties. Even if the property values won't change.

    The following example shows a fragment of the extracted TreeViewItem default Style. It misses other referenced resources in order to compact the example.
    You can extract the complete Style using the XAML designer and then paste the Style for the TreeViewItem from below to replace the extracted Style of the TreeViewItem.
    I have annotated the modified parts to help spotting the required modifications.

    MainWindow.xanl.cs

    partial class MainWindow : Window
    {
      public static DataTemplate GetExtendedContentItemTemplate(DependencyObject attachingElement) 
        => (DataTemplate)attachingElement.GetValue(ExtendedContentItemTemplateProperty);
    
      public static void SetExtendedContentItemTemplate(DependencyObject attachingElement, DataTemplate value) 
        => attachingElement.SetValue(ExtendedContentItemTemplateProperty, value);
    
      public static readonly DependencyProperty ExtendedContentItemTemplateProperty = DependencyProperty.RegisterAttached(
        "ExtendedContentItemTemplate", 
        typeof(DataTemplate), 
        typeof(MainWindow), 
        new PropertyMetadata(default));
    }
    

    MainWindow.xaml

    <Window>
      <Window.Resources>
        
        <!-- The example DataTemplate for the extended content -->
        <DataTemplate x:Key="ExtendedDataContentTemplate"
                      DataType="NestedContainer">
          <StackPanel>
            <TextBlock Text="{Binding ActionData}" />
            <TextBlock Text="{Binding Modifier}" />
          </StackPanel>
        </DataTemplate>
    
        <!-- 
             The fragment of the complete TreeViewItem style.
             The first setter that sets the attached 'ExtendedContentItemTemplate' property
             and the ControlTemplate that extends the layout to add the ContentControl 
             for the additional content are of special interest here.
        -->
        <Style TargetType="{x:Type TreeViewItem}">
    
          <!-- Assign the DataTemplate for the extended content to the attached property -->
          <Setter Property="local:MainWindow.ExtendedContentItemTemplate"
                  Value="{StaticResource ExtendedDataContentTemplate}" />
    
          <Setter Property="Background"
                  Value="Transparent" />
          <Setter Property="HorizontalContentAlignment"
                  Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}" />
          <Setter Property="VerticalContentAlignment"
                  Value="{Binding 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" /> <!-- Default header row -->
                    <RowDefinition /> <!-- Default child items row -->
                    <RowDefinition Height="Auto" /> <!-- New extra content row -->
                  </Grid.RowDefinitions>
                  <ToggleButton x:Name="Expander"
                                ClickMode="Press"
                                IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource Mode=TemplatedParent}}"
                                Style="{StaticResource ExpandCollapseToggleStyle}" />
                  <Border x:Name="Bd"
                          Background="{TemplateBinding Background}"
                          BorderBrush="{TemplateBinding BorderBrush}"
                          BorderThickness="{TemplateBinding BorderThickness}"
                          Grid.Column="1"
                          Padding="{TemplateBinding Padding}"
                          SnapsToDevicePixels="true">
                    <ContentPresenter x:Name="PART_Header"
                                      ContentSource="Header"
                                      HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                  </Border>
                  <ItemsPresenter x:Name="ItemsHost"
                                  Grid.Column="1"
                                  Grid.ColumnSpan="2"
                                  Grid.Row="1" />
        
                  <!-- 
                       The host for the extra content below the child items.
                       For this purpose we have to introduce a third row to the Grid.
                       The Content is the data item itself (in this case 'NestedContainer').
                       The ContentTemplate binds to the attached property ExtendedContentItemTemplate 
                       that is set on the TreeViewItem via the current Style (see style setter above).
                  --> 
                  <ContentControl Grid.Row="2"
                                  Grid.Column="1"
                                  Content="{TemplateBinding DataContext}"
                                  ContentTemplate="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(local:MainWindow.ExtendedContentItemTemplate)}" />
                </Grid>
    
                <ControlTemplate.Triggers>
                  <Trigger Property="IsExpanded"
                           Value="false">
                    <Setter Property="Visibility"
                            TargetName="ItemsHost"
                            Value="Collapsed" />
                  </Trigger>
                  <Trigger Property="HasItems"
                           Value="false">
                    <Setter Property="Visibility"
                            TargetName="Expander"
                            Value="Hidden" />
                  </Trigger>
                  <Trigger Property="IsSelected"
                           Value="true">
                    <Setter Property="Background"
                            TargetName="Bd"
                            Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
                    <Setter Property="Foreground"
                            Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}" />
                  </Trigger>
                  <MultiTrigger>
                    <MultiTrigger.Conditions>
                      <Condition Property="IsSelected"
                                 Value="true" />
                      <Condition Property="IsSelectionActive"
                                 Value="false" />
                    </MultiTrigger.Conditions>
                    <Setter Property="Background"
                            TargetName="Bd"
                            Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}" />
                    <Setter Property="Foreground"
                            Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightTextBrushKey}}" />
                  </MultiTrigger>
                  <Trigger Property="IsEnabled"
                           Value="false">
                    <Setter Property="Foreground"
                            Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
                  </Trigger>
                </ControlTemplate.Triggers>
              </ControlTemplate>
            </Setter.Value>
          </Setter>
          <Style.Triggers>
            <Trigger Property="VirtualizingPanel.IsVirtualizing"
                     Value="true">
              <Setter Property="ItemsPanel">
                <Setter.Value>
                  <ItemsPanelTemplate>
                    <VirtualizingStackPanel />
                  </ItemsPanelTemplate>
                </Setter.Value>
              </Setter>
            </Trigger>
          </Style.Triggers>
        </Style>
      </Window.Resources>
    
      <TreeView ItemsSource="{Binding}">
        <TreeView.Resources>
          <HierarchicalDataTemplate DataType="NestedContainer"
                                    ItemsSource="{Binding ChildContainers}">
            <TextBlock Text="{Binding XID}" />
          </HierarchicalDataTemplate>
        </TreeView.Resources>
      </TreeView>
    </Window>