Feb 3, 2015

Horizontal stretch on TreeViewItems

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">
    <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

0 comments:

Post a Comment

Nam Le © 2014 - Designed by Templateism.com, Distributed By Templatelib