Add a border around all children of a TreeViewItem
Asked Answered
M

2

8

I have a TreeView and I am trying to implement a style that will allow me to place a border around all the children of a particular node using the HierarchicalDataTemplate. An example of what I want is shown below:

enter image description here

The following code is what I have so far.

<HierarchicalDataTemplate DataType="{x:Type model:Node}" ItemsSource="{Binding Children, Mode=OneWay}">
     <StackPanel>
          <TextBlock Text="{Binding Name}"/>
     </StackPanel>
     <HierarchicalDataTemplate.ItemContainerStyle>
          <Style TargetType="{x:Type TreeViewItem}">
              //what goes in here???
          </Style>
     </HierarchicalDataTemplate.ItemContainerStyle>     
</HierarchicalDataTemplate>

What do I need to add to implement my border the way I want?

Mernamero answered 10/5, 2013 at 16:7 Comment(0)
R
12

To render a Border around the collection of children for a TreeViewItem we need to modify the Style for ItemContainerStyle of the TreeView

TreeViewItem Style by default uses a <ItemsPresenter x:Name="ItemsHost" /> to render it's children's content.

Children's Content in the default ItemContainerStyle is given by

<ItemsPresenter x:Name="ItemsHost"
                Grid.Row="1"
                Grid.Column="1"
                Grid.ColumnSpan="2" />

Now to test this I had a Collection with a bool named Type and just tried to render a Border when this bool was True

So I updated the ItemsPresenter to

<Border Grid.Row="1"
        Grid.Column="1"
        Grid.ColumnSpan="2"
        BorderThickness="1">
  <Border.Style>
    <Style TargetType="{x:Type Border}">
      <Setter Property="BorderBrush"
              Value="Transparent" />
      <Style.Triggers>
        <DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor,
                                       AncestorType={x:Type TreeViewItem}},
                                       Path=DataContext.Type}"
                      Value="True">
          <Setter Property="BorderBrush"
                  Value="Red" />
        </DataTrigger>
      </Style.Triggers>
    </Style>
  </Border.Style>
  <ItemsPresenter x:Name="ItemsHost"  />
</Border>

Which then rendered the following

enter image description here

You'll of course have to update the above Bindings to be based on your own cases of when you want the Border rendered.

In my case my Type variable was set to True for the Item with "1.1" as it's Header Content.

Reactant answered 10/5, 2013 at 17:57 Comment(17)
I will attempt your solution on Monday when I return to work. Thanks for the answerMernamero
Your solution places a border around each individual TreeViewItem. What I need is a border around the entire groups of TreeviewItemsMernamero
@Kazuo ye before you posting this image, I understood your question for having a border around each item. I've updated my answer to replicate your posted image. Hope that helpsReactant
Sorry, I am finding your answer confusing to follow. When looking at the last code snippet I do not know where the border is. Is that the border inside the HierarchicalDataTemplate from your first code snippet? Do I add your new code the ItemContainerStyle? Basically I do not know what to write and where.Mernamero
@Kazuo ill add the sample project I put together with a download link to my answer in a bit. This is just an ItemContainerStyle ehich is applied to the treeview xaml element directly.Reactant
@Kazuo I've added a download link at the bottom of the post you can get and have a look at for the implementation. It has 2 files MainWindow.xaml and Window1.xaml each with it's codebehind showing a slightly different usage using either just Resources or <HierarchicalDataTemplate.ItemContainerStyle>Reactant
I am still attempting to get your solution to work. To suit my needs all the xaml styling needs to be contained inside of the HierarchicalDataTemplate. I cannot be making any changes to the tree view itself. When get your solution working it had the same issue in that it put a border around each child. I had to read your download in notepad++ as I only have visual studio 2010. Cannot open your project to see your styling in actionMernamero
@Kazuo ok I've pretty much just completely trimmed down my answer to make it easier for you. I've attached a new download link(removed the old one) which has everything to do with this Style and it's Dependencies inside HierarchicalDataTemplate. This sln can be opened from VS2010 and should show the same output as the image above. Everything happens in MainWindow.xaml and it's code-behind. Hope that clarifies some stuff for youReactant
@Kazuo Also you don't have to open stuff in NotePad++ if you only have VS2010 all it takes is a search with "convert vs2012 sln to vs2010" and you land #12143883 which is exactly what I've done on the new download to make it VS2010 firendly.Reactant
I could not get this to work. When I open your example the border does indeed surround the lowest level child items but when it came to applying that style to my own I failed. The code from your example had to be changed as it was overwriting the style from my style library that is applied to all TreeViews. In the end I failed to take your example and change it successfully to make it fit my needs. All I needed a border around the children that did not involve changing any other aspect of the style. Ill list your answer as the accepted answer anyway as someone else my gain something from it.Mernamero
Well am sorry to hear that but the bottom line is to get Border around the children we need to edit the Template. If your having a base Style, you could try using the BasedOn property of the Style to derive from the base Style. We would still need to over-ride the template somewhere to get the behaviour your looking for. It's hard to give you an example without knowing what your base Style provides as functionalities.Reactant
when over-riding the template what is the bare minimum that needs to be applied to my HierarchicalDataTemplate to get the border if we use the basedon. I have over-ridden a template successfully before with a context menu and preserved its style but I am having a lot of trouble with this TreeViewItem for some reason.Mernamero
Unfortunately the "bare minimum" in this case is everything you found in the TreeView's ItemContainerStyle. Just think of it as this. TreeViewItem's by default are "rendered" in the view a particular way. Your requirements needs us to add something that which is not available by default. WPF is look-less thus we have over-riden the entire render to be 99% similar to what WPF has by default and added that Border around the "ItemsHost". There isn't a solution to just add a Border without replicating the default 99% of the Template which we don't want to modifyReactant
@Kazuo what are the things you have in the base Style for a TreeViewItem? is it just Triggers and Setter's to modify inbuilt DP's of the TreeViewitem? If so just adding a BasedOn in the ItemContainerStyle should give you a perfectly concatenated Style. if your however giving a modified template in the base Style for TreeViewItem's then this would not work.Reactant
From what I can see the TreeView styling in my app is a complete control template change. This may explain why I could not get this to work. I would have to actually change the base style which is not appropriate in this case as my question above applied to only one tree in the app.Mernamero
Well if you can't change the base Style, you need to copy your base template to this TreeView and add the Border just in this one(If your deriving a Style you would not need to copy common dependencies, you would need to only provide the TreeViewItem's template since everything else should be found through the parent scope's resources), or convince whoever you have to convince to modify the base Style and render a transparent Border everywhere but for this TreeView.Reactant
let us continue this discussion in chatMernamero
O
3

Looks like WPF team thought that nobody will need this functionality so they haven't included any border around ItemsPresenter in TreeViewItem template, so you are going to have to change TreeViewItem template and add border around ItemsPresenter.

You can view default TreeViewItem style/template definition by downloading WPF themes XAML dictionaries. Links are provided here.

Here is a complete XAML of a working solution:

<Window x:Class="WpfApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:model="clr-namespace:WpfApplication">

    <Window.DataContext>
        <x:ArrayExtension Type="{x:Type model:Node}">
            <model:Node Name="Root">
                <model:Node.Children>
                    <model:Node Name="Child 1" HasChildrenBorder="True">
                        <model:Node.Children>
                            <model:Node Name="Child 1.1"/>
                            <model:Node Name="Child 1.2"/>
                            <model:Node Name="Child 1.3"/>
                        </model:Node.Children>
                    </model:Node>
                    <model:Node Name="Child 2"/>
                </model:Node.Children>
            </model:Node>
        </x:ArrayExtension>
    </Window.DataContext>

    <TreeView ItemsSource="{Binding}">

        <TreeView.Resources>

            <!--This part is extracted from Areo.NormalColor.xaml WPF Theme which you can download from locations explained here: https://mcmap.net/q/826100/-where-can-i-download-microsoft-39-s-standard-wpf-themes-from-closed/4158681#4158681-->
            <PathGeometry x:Key="TreeArrow">
                <PathGeometry.Figures>
                    <PathFigureCollection>
                        <PathFigure IsFilled="True"
                                    StartPoint="0 0"
                                    IsClosed="True">
                            <PathFigure.Segments>
                                <PathSegmentCollection>
                                    <LineSegment Point="0 6"/>
                                    <LineSegment Point="6 0"/>
                                </PathSegmentCollection>
                            </PathFigure.Segments>
                        </PathFigure>
                    </PathFigureCollection>
                </PathGeometry.Figures>
            </PathGeometry>

            <Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}">
                <Setter Property="Focusable" Value="False"/>
                <Setter Property="Width" Value="16"/>
                <Setter Property="Height" Value="16"/>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type ToggleButton}">
                            <Border Width="16" 
                                    Height="16" 
                                    Background="Transparent" 
                                    Padding="5,5,5,5">
                                <Path x:Name="ExpandPath" 
                                      Fill="Transparent" 
                                      Stroke="#FF989898" 
                                      Data="{StaticResource TreeArrow}">
                                    <Path.RenderTransform>
                                        <RotateTransform 
                                            Angle="135" 
                                            CenterX="3" 
                                            CenterY="3"/>
                                    </Path.RenderTransform>
                                </Path>
                            </Border>
                            <ControlTemplate.Triggers>
                                <Trigger Property="IsMouseOver" Value="True">
                                    <Setter TargetName="ExpandPath" Property="Stroke" Value="#FF1BBBFA"/>
                                    <Setter TargetName="ExpandPath" Property="Fill" Value="Transparent"/>
                                </Trigger>
                                <Trigger Property="IsChecked" Value="True">
                                    <Setter TargetName="ExpandPath" Property="RenderTransform">
                                        <Setter.Value>
                                            <RotateTransform 
                                                Angle="180" 
                                                CenterX="3" 
                                                CenterY="3"/>
                                        </Setter.Value>
                                    </Setter>
                                    <Setter TargetName="ExpandPath" Property="Fill" Value="#FF595959"/>
                                    <Setter TargetName="ExpandPath" Property="Stroke" Value="#FF262626"/>
                                </Trigger>
                            </ControlTemplate.Triggers>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>

            <Style TargetType="{x:Type TreeViewItem}" BasedOn="{StaticResource {x:Type TreeViewItem}}">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type TreeViewItem}">
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition MinWidth="19" Width="Auto"/>
                                    <ColumnDefinition Width="Auto"/>
                                    <ColumnDefinition Width="Auto"/>
                                </Grid.ColumnDefinitions>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="Auto"/>
                                    <RowDefinition/>
                                </Grid.RowDefinitions>
                                <ToggleButton x:Name="Expander"
                                              Style="{StaticResource ExpandCollapseToggleStyle}"
                                              IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
                                              ClickMode="Press"/>
                                <Border Name="Bd"
                                        Grid.Column="1"
                                        Background="{TemplateBinding Background}"
                                        BorderBrush="{TemplateBinding BorderBrush}"
                                        BorderThickness="{TemplateBinding BorderThickness}"
                                        Padding="{TemplateBinding Padding}"
                                        SnapsToDevicePixels="True">
                                    <ContentPresenter x:Name="PART_Header"
                                                      ContentSource="Header"
                                                      HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                                      SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
                                </Border>
                                <Border Name="ItemsHostBd" 
                                        Grid.Row="1" 
                                        Grid.Column="1" 
                                        Grid.ColumnSpan="2">
                                    <ItemsPresenter x:Name="ItemsHost"/>
                                </Border>
                            </Grid>
                            <ControlTemplate.Triggers>
                                <Trigger Property="IsExpanded" Value="False">
                                    <Setter TargetName="ItemsHostBd" Property="Visibility" Value="Collapsed"/>
                                </Trigger>
                                <Trigger Property="HasItems" Value="False">
                                    <Setter TargetName="Expander" Property="Visibility" Value="Hidden"/>
                                </Trigger>
                                <Trigger Property="IsSelected" Value="True">
                                    <Setter TargetName="Bd" Property="Background" 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 TargetName="Bd" Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
                                    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
                                </MultiTrigger>
                                <Trigger Property="IsEnabled" Value="False">
                                    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                                </Trigger>

                                <!-- This part is customized to work with HasChildrenBorder property from data-bound object. -->
                                <DataTrigger Binding="{Binding HasChildrenBorder}" Value="True">
                                    <Setter TargetName="ItemsHostBd" Property="BorderBrush" Value="Red"/>
                                    <Setter TargetName="ItemsHostBd" Property="BorderThickness" Value="1"/>
                                </DataTrigger>

                            </ControlTemplate.Triggers>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>

            <HierarchicalDataTemplate DataType="{x:Type model:Node}" ItemsSource="{Binding Children}">
                <TextBlock Text="{Binding Name}"/>
            </HierarchicalDataTemplate>

        </TreeView.Resources>

    </TreeView>
</Window>

Here is how Node class is defined:

using System.Collections.ObjectModel;

namespace WpfApplication
{
    public class Node
    {
        public string Name { get; set; }
        public ObservableCollection<Node> Children { get; set; }

        public bool HasChildrenBorder { get; set; }

        public Node()
        {
            this.Children = new ObservableCollection<Node>();
        }
    }
}
Oecology answered 19/5, 2013 at 23:0 Comment(3)
Thank you for the answer but I do not wish to have my classes contain any data relating to how they should be presented in UI. The best solution in my case would be a xaml only one.Mernamero
Property HasChildrenBorder was used just as an example to show that concept works. You can put there any other property for which TreeViewItem children should be bordered. Then you should just change DataTrigger.Binding in TreeViewItem's ControlTemplate.Oecology
Ah, my mistake. Thank you for the answer. I have found a solution to my problem with the help from another poster.Mernamero

© 2022 - 2024 — McMap. All rights reserved.