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:
- http://blogs.msdn.com/jpricket/archive/2008/08/05/wpf-a-stretching-treeview.aspx
- http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/0ff69337-3a03-4235-b48c-d80321c21d54/
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"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" MinWidth="19" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition /> </Grid.RowDefinitions> <ToggleButton IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}" ClickMode="Press" Name="Expander" /> <Border x:Name="Bd" HorizontalAlignment="Stretch" BorderThickness="1" BorderBrush="Red" Padding="{TemplateBinding Control.Padding}" Background="{TemplateBinding Panel.Background}" SnapsToDevicePixels="True" 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}" /> </Border> <ItemsPresenter Name="ItemsHost" Grid.Column="1" Grid.Row="1" Grid.ColumnSpan="2" /> </Grid>
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
Solution
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"> <Setter.Value> <ControlTemplate TargetType="TreeViewItem"> <StackPanel> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" MinWidth="19" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition /> </Grid.RowDefinitions> <!-- 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}}" ClickMode="Press" Name="Expander"> <ToggleButton.Style> <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"> <Setter.Value> <ControlTemplate TargetType="ToggleButton"> <Border Padding="5,5,5,5" Background="#00FFFFFF" Width="16" Height="16"> <Path Fill="#00FFFFFF" Stroke="#FF989898" Name="ExpandPath"> <Path.Data> <PathGeometry Figures="M0,0L0,6L6,0z" /> </Path.Data> <Path.RenderTransform> <RotateTransform Angle="135" CenterX="3" CenterY="3" /> </Path.RenderTransform> </Path> </Border> <ControlTemplate.Triggers> <Trigger Property="UIElement.IsMouseOver" Value="True"> <Setter TargetName="ExpandPath" Property="Shape.Stroke" Value="#FF1BBBFA" /> <Setter TargetName="ExpandPath" Property="Shape.Fill" Value="#00FFFFFF" /> </Trigger> <Trigger Property="ToggleButton.IsChecked" Value="True"> <Setter TargetName="ExpandPath" Property="UIElement.RenderTransform"> <Setter.Value> <RotateTransform Angle="180" CenterX="3" CenterY="3" /> </Setter.Value> </Setter> <Setter TargetName="ExpandPath" Property="Shape.Fill" Value="#FF595959" /> <Setter TargetName="ExpandPath" Property="Shape.Stroke" Value="#FF262626" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </ToggleButton.Style> </ToggleButton> <Border x:Name="Bd" HorizontalAlignment="Stretch" BorderThickness="{TemplateBinding Border.BorderThickness}" BorderBrush="{TemplateBinding Border.BorderBrush}" Padding="{TemplateBinding Control.Padding}" Background="{TemplateBinding Panel.Background}" SnapsToDevicePixels="True" Grid.Column="1"> <ContentPresenter x:Name="PART_Header" Content="{TemplateBinding HeaderedContentControl.Header}" ContentTemplate="{TemplateBinding HeaderedContentControl.HeaderTemplate}" ContentStringFormat="{TemplateBinding HeaderedItemsControl.HeaderStringFormat}" ContentTemplateSelector="{TemplateBinding HeaderedItemsControl.HeaderTemplateSelector}" ContentSource="Header" HorizontalAlignment="{TemplateBinding Control.HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" /> </Border> <ItemsPresenter x:Name="ItemsHost" Grid.Column="1" Grid.Row="1" /> </Grid> </StackPanel> <ControlTemplate.Triggers> <Trigger Property="TreeViewItem.IsExpanded" Value="False"> <Setter TargetName="ItemsHost" Property="UIElement.Visibility" Value="Collapsed" /> </Trigger> <Trigger Property="ItemsControl.HasItems" Value="False"> <Setter TargetName="Expander" Property="UIElement.Visibility" Value="Hidden" /> </Trigger> <Trigger Property="TreeViewItem.IsSelected" Value="True"> <Setter TargetName="Bd" Property="Panel.Background" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" /> <Setter Property="TextElement.Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}" /> </Trigger> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="TreeViewItem.IsSelected" Value="True" /> <Condition Property="Selector.IsSelectionActive" Value="False" /> </MultiTrigger.Conditions> <Setter TargetName="Bd" Property="Panel.Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" /> <Setter Property="TextElement.Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" /> </MultiTrigger> <Trigger Property="UIElement.IsEnabled" Value="False"> <Setter Property="TextElement.Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style>
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
12 comments:
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-)
Mike
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:
<TreeView
x:Name="trvAccountLedger"
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?
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 ;-)
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="...">
<UserControl.Resources>
<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 -->
</UserControl.Resources>
...
...
<TreeView
x:Name="trvAccountLedger"
ItemTemplateSelector="{StaticResource AccountLedgerTemplateSelector}" />
</UserControl>
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.
Sorry Matheus,
There is a missing line in the Template
ContentTemplateSelector="{TemplateBinding HeaderedItemsControl.HeaderTemplateSelector}"
I will update the post to include it :-)
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.
Thanks!
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.
Neat!
But this gives the Vista visual style even in other environments?
/Jens
/Jens,
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".
Lee
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.
Thanks! This solution solved my problem perfectly!
Post a Comment