Tuesday, January 13, 2009

Horizontal stretch on TreeViewItems

An old problem I have faced popped up at one of new roles in the UK recently. It is the fairly simple requirements of having the header of an item in a Tree view fill all of the horizontal space available. You would think that like any other scenario in WPF you would set either the HorizontalAlignment on the TreeViewItem or the HorizontalContentAlignment on a parent entity like the TreeView itself. Well this doesn't work. There are various hacks to get around it that have been suggested in the community:

However these "hacks" don't address the underlying problem (hence why I am labelling them hacks).

Cause of the problem

If you explore the problem a little bit deeper you will find that the actual problem here is the way the Template for TreeViewItem controls has been defined in the two most popular themes (Luna for XP and Aero for Vista). For some quick background, a control is just a DependencyObject made up of C# (or any other .NET language) code. Its visual representation is composed in XAML by constructing a ControlTemplate and assigning it to the Control's Template property. A ControlTemplate is a layout that is composed of other primitive controls such as Button, Selector, Grid etc.

Lets take a look at a part of the Control Template for a TreeViewItem to find the problem: I have removed a lot of content that is not relevant to what our problems is.

<ControlTemplate TargetType="TreeViewItem">
            <ColumnDefinition Width="Auto" MinWidth="19" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition />
        <ToggleButton IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" ClickMode="Press" Name="Expander" />
        <Border x:Name="Bd" 
                    BorderThickness="1" BorderBrush="Red" 
                    Padding="{TemplateBinding Control.Padding}" 
                    Background="{TemplateBinding Panel.Background}" 
                    Grid.Column="1" >
            <ContentPresenter Content="{TemplateBinding HeaderedContentControl.Header}" ContentTemplate="{TemplateBinding HeaderedContentControl.HeaderTemplate}" ContentStringFormat="{TemplateBinding HeaderedItemsControl.HeaderStringFormat}" ContentSource="Header" Name="PART_Header" HorizontalAlignment="{TemplateBinding Control.HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
        <ItemsPresenter Name="ItemsHost" Grid.Column="1" Grid.Row="1" Grid.ColumnSpan="2" />

Things to note here is that the layout is controlled with a 3x2 Grid. Now notice that the Border that encapsulates the ContentPresenter (the place holder for where your values will live) is set to live in column 1 (remember 0 based index rules apply here). Also notice that the ItemsPresenter (the place holder for all the children of a TreeViewItem) is set to row 1 and column 1 & 2 (Grid.Column="1" Grid.ColumnSpan="2"). Ok, so now that we know that we look back up to the Grid and notice that the column definitions are such that Column 1 has Width="Auto" and Column 2 has Width="*". This effectively says column 1 can never effectively stretch.

Cell (0,0) Cell (1,0) Header via ContentPresenter Cell(2,0)
Cell(1,0) Cell(1,1) + Cell(2,1) Children items via ItemsPresenter

Why would the default template be like this?

Who knows?! I think that M$ have done an amazing job with the WPF framework in general, but there are a few things about the TreeView that are a little bit odd. It also doesn't help when M$ representatives constantly provided misleading information. By implementing the simple solution below the user gets same effect as the default control template, however they also get the flexibility of defining their own horizontal alignment


It is nice to know that the solution is not to bad. We cant just tweak the template property of the TreeViewItem, we must completely replace it. While this may seem like a big hammer for a little problem, my guess is that you were probably modifying the standard layout of a TreeViewItem substantially if you want it to stretch horizontally. So here is a starter template that you can use to replace the default template which I think is probably what most users would expect from the default template any way.

<Style TargetType="TreeViewItem"
       BasedOn="{StaticResource {x:Type TreeViewItem}}">
  <Setter Property="HorizontalContentAlignment"
          Value="Center" />
  <Setter Property="Template">
      <ControlTemplate TargetType="TreeViewItem">
              <ColumnDefinition Width="Auto"
                                MinWidth="19" />
              <ColumnDefinition Width="*" />
              <RowDefinition Height="Auto" />
              <RowDefinition />
                             Note that the following do not work, but I believe the top 2 should?!
                             <ToggleButton IsChecked="{TemplateBinding IsExpanded}" ClickMode="Press" Name="Expander">
                             <ToggleButton IsChecked="{TemplateBinding Property=IsExpanded}" ClickMode="Press" Name="Expander">
                             <ToggleButton IsChecked="{TemplateBinding Path=IsExpanded}" ClickMode="Press" Name="Expander">
            <ToggleButton IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
                <Style TargetType="ToggleButton">
                  <Setter Property="UIElement.Focusable"
                          Value="false" />
                  <Setter Property="FrameworkElement.Width"
                          Value="16" />
                  <Setter Property="FrameworkElement.Height"
                          Value="16" />
                  <Setter Property="Control.Template">
                      <ControlTemplate TargetType="ToggleButton">
                        <Border Padding="5,5,5,5"
                          <Path Fill="#00FFFFFF"
                              <PathGeometry Figures="M0,0L0,6L6,0z" />
                              <RotateTransform Angle="135"
                                               CenterY="3" />
                          <Trigger Property="UIElement.IsMouseOver"
                            <Setter TargetName="ExpandPath"
                                    Value="#FF1BBBFA" />
                            <Setter TargetName="ExpandPath"
                                    Value="#00FFFFFF" />
                          <Trigger Property="ToggleButton.IsChecked"
                            <Setter TargetName="ExpandPath"
                                <RotateTransform Angle="180"
                                                 CenterY="3" />
                            <Setter TargetName="ExpandPath"
                                    Value="#FF595959" />
                            <Setter TargetName="ExpandPath"
                                    Value="#FF262626" />
            <Border x:Name="Bd"
                    BorderThickness="{TemplateBinding Border.BorderThickness}"
                    BorderBrush="{TemplateBinding Border.BorderBrush}"
                    Padding="{TemplateBinding Control.Padding}"
                    Background="{TemplateBinding Panel.Background}"
              <ContentPresenter x:Name="PART_Header"
                                Content="{TemplateBinding HeaderedContentControl.Header}"
                                ContentTemplate="{TemplateBinding HeaderedContentControl.HeaderTemplate}"
                                ContentStringFormat="{TemplateBinding HeaderedItemsControl.HeaderStringFormat}"
                                ContentTemplateSelector="{TemplateBinding HeaderedItemsControl.HeaderTemplateSelector}"
                                HorizontalAlignment="{TemplateBinding Control.HorizontalContentAlignment}"
                                SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
            <ItemsPresenter x:Name="ItemsHost"
                            Grid.Row="1" />
          <Trigger Property="TreeViewItem.IsExpanded"
            <Setter TargetName="ItemsHost"
                    Value="Collapsed" />
          <Trigger Property="ItemsControl.HasItems"
            <Setter TargetName="Expander"
                    Value="Hidden" />
          <Trigger Property="TreeViewItem.IsSelected"
            <Setter TargetName="Bd"
                    Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
            <Setter Property="TextElement.Foreground"
                    Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}" />
              <Condition Property="TreeViewItem.IsSelected"
                         Value="True" />
              <Condition Property="Selector.IsSelectionActive"
                         Value="False" />
            <Setter TargetName="Bd"
                    Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" />
            <Setter Property="TextElement.Foreground"
                    Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
          <Trigger Property="UIElement.IsEnabled"
            <Setter Property="TextElement.Foreground"
                    Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />

This now gives you a layout grid that looks more like this:

Cell (0,0) Cell (1,0) Header via ContentPresenter
Cell(1,0) Cell(1,1) + Cell(2,1) Children items via ItemsPresenter

Allowing you to choose to fill the space available by setting HorizontalContentAlignment on the TreeViewItem (probably via a style). Obviously you could align to the Left (default) or the other value such a Center, Right and Stretch.

I hope this helps anyone else out there that has found this stumbling block to nice layout on TreeView controls


Mike said...

thanks very much. I was trying to right justify controls in my treeView. I'd seen a couple of 'solutions' but not an explanation. thanks, Lee, for taking the time to explain the underlying problem.
much appreciated 8-)

Matheus Moreira said...

Lee, I'm using your solution and it works partially for the problem I have. Maybe you can help me.

I have to right align content in a treeview, but I'm using different templates for each level of the tree (one for leafs and one for other levels).

My UserControl has the following resources in its Resources section:

* Your TreeViewItem style
* One DataTemplate (for leaf nodes)
* One HierarchicalDataTemplate (for the other nodes)

The TreeView is declared this way:

ItemTemplateSelector="{StaticResource local:MyTemplateSelector}"

MyTemplateSelector selects either the DataTemplate or the HierarchicalDataTemplate based on properties of the rendered item.

When I used only my templates, the items where rendered the way I expected, except for the right alignment. With the inclusion of your style, the alignment works but my items aren't rendered the way they are supposed to be. Could you help me, please?

Lee Campbell said...

Matheus, can you share some of your code and maybe give myself and the readers a more detailed explaination of what is going wrong? Is the wrong data template being shown, it it not aligned properly, maybe it is not being shown at all?

Im sure we can help ;-)

Matheus Moreira said...

Hello, Lee. Here is the XAML for my UserControl. I'm sending only the parts that I think are relevants because the code is a bit lengthy. Of course I can send more details if necessary.

<UserContro x:Class="...">
<Style... /> <!-- this is the style you wrote about -->

<d:AccountLedgerTemplateSelector x:Key="AccountLedgerTemplateSelector" />
<d:TransactionsVisibilityConverter x:Key="TransactionsVisibilityConverter" />

<DataTemplate x:Key="AccountTemplate">...</DataTemplate> <!-- This template is used to render leaf nodes -->

<HierarchicalDataTemplate x:Key="AccountLedgerTemplate" ItemsSource="{Binding ChildrenRecords}">...</HierarchicalDataTemplate> <!-- This template is used to render the other nodes -->

ItemTemplateSelector="{StaticResource AccountLedgerTemplateSelector}" />

AccountLedgerTemplateSelector selects either AccountTemplate or AccountLedgerTemplate based on a property of the rendered item (an instance of a class I created, AccountLedgerRecord). AccountTemplate is used for leaf nodes and renders account name (left aligned), account balance (right aligned on the same line) and a data table (WPF Toolkit) on the next line. AccountLedgerTemplate renders only the account name and balance.

I want the balance information of each node and the data table of leaf nodes to be right aligned. Your style solved the first half of the problem because it was possible to use all horizontal space available to render the tree view items. The problem is that when I use the style, my data templates are ignored and the ToString() method is used to render the items ("DWIMBS.WebService.AccountLedgerRecord").

On the code behind side, the tree view is populated using the ItemsSource property: trvAccountLedger.ItemsSource = records;.

I could send some screen shots to clarify things better.

Thanks very much.

Lee Campbell said...
This comment has been removed by the author.
Lee Campbell said...

Sorry Matheus,
There is a missing line in the Template
ContentTemplateSelector="{TemplateBinding HeaderedItemsControl.HeaderTemplateSelector}"

I will update the post to include it :-)

Matheus Moreira said...

Hi, Lee.

I think that tomorrow my manager will be very releived with this simple solution. :) I'll test it and send the results to you.


Chris said...

Thanks a ton, was trying to do this with no luck with all the same Hacks you had linked to. Yours is the only one that actually feels like a maintainable solution.

Anonymous said...


But this gives the Vista visual style even in other environments?


Lee Campbell said...

If you want to have a different look and feel, then apply the same process I have in the post but do so with the template from the Theme you are targeting. You can get the original template from tools such as Blend or the fantastic free tool "ShowMeTheTemplate".


Juan Diego said...

I didnt like the idea of editing the whole template of the treeview, so I decided a far simplest way to do it, just by creating a datatemplate for my object which has a itemscontrol which has a binding to the childs.

then I did a itemcontrol.

Of course, all of this inspired in the explanation of Lee.

Thanks once again.

Stephen Wrighton said...

Thanks! This solution solved my problem perfectly!

Anonymous said...

With this solution, has anyone experienced when the treeview width expands the treeviewitem expands with it; but when the treeview width decreases the treeviewitem does not?

Anonymous said...

I've also experienced the problem with the treeviewitem not decreasing in size, but it seems to only occur when I also have to apply the textbox hack in order for textboxes to respect the parent container width when entering a lot of text:

<Border Name="placeholderSectionBorder" Grid.Column="1" />
<TextBox Grid.Column="1" TextWrapping="Wrap" Width="{Binding ElementName=placeholderSectionBorder, Path=ActualWidth}">

Unfortunately, I need the textbox to stretch and wrap properly and I have not yet figured out a workaround.

Artiom said...

Hi lee, thx for ur post, I used ur style in my blog, (http://mypronotes.blogspot.com/) hope you are not agains. I wrote link to your post as a source.

Lee Campbell said...

Great to see that this post is still helping people out. Feel free to reuse and link back @Artiom.

Yves Darmaillac said...

Hello and thank you very much for theese explanations. Its good to understand how things work and may be valuable information to find solutions to many problems. Personally, I found a different uggly but very simple hack :

Where :

public class OptionWidthConverter : IValueConverter
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
double width = 0;
TreeViewItem parent = value as TreeViewItem;

width = parent.ActualWidth - 30;

return width;

public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
throw new InvalidOperationException();


Lee Campbell said...

Certainly is an ugly hack, Yves!
I hope you don't resort to it for the sake of the poor developer who may have to maintain it later.

Thanks for the feedback, hope it helped.

Robert said...

I had the same problem in Silverlight 5, which drove me nuts while trying to find a solution.

Your solution for WPF set me off in the right direction, though I could not make out what was the essence of the whole Template you had posted.

So I had to experiment a while until I found what was really needed to solve my problem. It came down to just a couple of changed lines.

So here it is. ( I had to replace the sharp brackets by straight ones here, the code tag was not accepted either. Maybe you can restore them.)

- Copy the ControlTemplates in Blend for TreeViewItem and TreeView.

- For TreeViewItem, just change the ColumnDefinitions and ItemsPresenter along your line of thought:
[ColumnDefinition Width="15"/]
[!--[ColumnDefinition Width="Auto"/]--]
[ColumnDefinition Width="*"/]

[!--[ItemsPresenter x:Name="ItemsHost" Grid.ColumnSpan="2" Grid.Column="1" Grid.Row="1" Visibility="Collapsed"/]--]
[ItemsPresenter x:Name="ItemsHost" Grid.ColumnSpan="1" Grid.Column="1" Grid.Row="1" Visibility="Collapsed"/]

- For TreeView, just change the HorizontalScrollBarVisibility of the ScrollViewer.
[!--[ScrollViewer x:Name="ScrollViewer" BorderBrush="Transparent" BorderThickness="0" Background="{x:Null}" HorizontalScrollBarVisibility="Auto" IsTabStop="False" TabNavigation="Once" VerticalScrollBarVisibility="Auto"]--]
[ScrollViewer x:Name="ScrollViewer" BorderBrush="Transparent" BorderThickness="0" Background="{x:Null}" HorizontalScrollBarVisibility="Disabled" IsTabStop="False" TabNavigation="Once" VerticalScrollBarVisibility="Auto"]

And that was it!
(I hope this is all readable.)

It is frustrating that such a logical feature is not already built in by Microsoft and takes such effort to fix on our own.

Ron said...

Lee, I found your solution while working on a TreeView related issue on Silverlight 5.
The problem I have is that the mouse wheel scrolling does not work if the cursor is on the empty space between the items in the tree and the vertical scrollbar. I have a TreeView defined in XAML

In the code behind I add the TreeViewItems.

I have tried to use your solution by editing the TreeViewItem template. I commented out the middle column and changed the ColumSpan for the ItemsPresenter tab.
I am setting the template on the Load event of the TreeViewItem that I add.The TreeViewItem has a checkbox and text inside a StackPanel. I have set the HorizontalContentAlignment to Stretch for the TreeViewPanel.

I thought that this would stretch the TreeViewItem contents to fill the column and that will allow the scrolling to work. But it is not working. Any thoughts on what I can be doing wrong?


Anonymous said...
This comment has been removed by a blog administrator.
Anonymous said...

In the solution, you have
Setter Property="HorizontalContentAlignment" Value="Center"

This makes all the content centered. but the comment was that the default is Left.

Anyway, I wonder if that line should be removed.
I removed it and things works as expected for me.

Anonymous said...

Now can you help me attach a context menu the nodes (for each of my HierarchicalDataTemplate and DataTemplate)

I would like it so that the context menu opens no matter where on the line I select.

This would be consistent with selection and highlighting. Since they work for the entire line, so to should the context menu.


Lee Campbell said...

If you dont want it centered then sure remove the setter doing this. If you want it left aligned, then you possibly dont need any of this code and can just use the default? (unless you want to control the background color?)

Lee Campbell said...

Without you specifying what your problem, or what solutions you have tried to use to add a context menu to your application, I can only assume you have not tried anything yet.
Here might be a helpful link for adding a context menu in wpf

Anonymous said...

Lee, Thank you for taking an interest in my question.

lmgtfy ... nice :-)

Hmmm... now to specify the problem...


Given the following hiearchical structure (Hopefully my ASCII art is clear enough).

- Group 1
Leaf 1A
Leaf 1B
+ Group 2

1) If I select "Group 1", the default TreeView will only highlight the "Group 1" label.
Leaf 1A
Leaf 1B
+ Group 2

Whereas your solution will highlight both the item label _and the entire 'line' beside it_

Leaf 1A
Leaf 1B
+ Group 2

2) If I double click on the "Group 2" text, the default TreeView will open it.
however, if I double click to the right of "Group 2", the default TreeView will not open it.

Your solution will allow me to double click the item label _or anywhere on the 'line' beside it_ to open it.

So I am using your solution. Thank you.

__The Problem __

Now I want to attach a Context Menu to the items of the list. One context menu will apply to Group items and another context menu will apply to Leaf items.

I would like to be able to right click on the item label, _or anywhere on the 'line' beside it_ to get the context menu.
For example:

Leaf 1A | Group menu |
Leaf 1B -------------
+ Group 2

I know how to attach a Context Menu to the TreeView
TreeView ...
TreeView.ContextMenu ...

I know how to attach a context Menu to a control in the HierarchicalDataTemplate and DataTemplate as follows:
Grid ...
Grid.ContextMenu ...

DataTemplate ...
TextBlock ...

However, the context menus only pop up when I right-click on the item text. I.e. not _anywhere on the 'line' beside it_.

I am hoping that is a clearer explanation of the problem.

It almost seems like I need to attach the context menu to the _parent_ of the DataTempate.


jl.rey said...

I tried this an saw no change in behavior.

Maybe I am using it wrong.

I will try summarize.

<UserControl ... /&rt;
<Style argetType="TreeViewItem"
BasedOn="{StaticResource {x:Type TreeViewItem}}"&rt;
... Your template ...
<DocPanel LastChild=fill&rt;
<-- Tree view is last child --&rt;
ItemSource={Bindig ...}
<Style TargetType="{x:Type TreeViewItem}" &rt;
<? Note 1 ?&rt;
... set bindings to MVVM model ...
<? Note 2 ?&rt;

ItemsSource="{Binding Children}"&rt;

<Grid Name="gridItem"
<ColumnDefinition Width="Auto" /&rt;
<ColumnDefinition Width="*" /&rt;
Source="{Binding Path=Image, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" /&rt;

<xctk:RichTextBox <-- This is the RichTextBox from Extended WPF Toolkit - 1.9.0 --&rt;
Text="{Binding Name}"
<jlr:TextFormatterRTF /&rt;

<Style TargetType={xType TreeView

The bindings work but the rtf box is less the MinWidth, or less than 1 character if minwidt is omitted.

Note 1: I also attempted to name the style and assign here as a named style.
Was unable to load this control something about a null reference.

Note 2: Attempted to put the Setter Property template portion of your example here.
Did run but no effect.

How am I to use this example???

Lee Campbell said...

Sorry, it is really hard to read your sample code @jl.rey.
Maybe you can post it as a gist on gist.github.com and then share the link?
On initial investigation, I would hazard to guess that it is that you create a style in your resources, but then overwrite it in your TreeView.ItemContainierStyle.

jl.rey said...

Thanks for the reply.

I am trying to get simplify this problem. It seems to have something to do with RichTextBox default width not being set.

Paul Efect said...

I came here from Stackoverflow, expecting a quick and elegant fix.
Boy, oh boy, was I mistaken.

That is a quite a lot of xaml for something as idiotic as treeviewitem.width ?!

However, I sincerely thank you for your time putting this together. You have no idea how delighted I was to paste and just... enjoy.

Kaveesh Dashora said...

Hi Lee,

Your solution works like a charm. But with this the content before the text is not getting highlighted i.e the expandcollapse buttons etc.

Can you help with this.