Search code examples
c#wpfxamldata-bindingattached-properties

ListView GridViewColumnHeader with attached property and custom template


I'm trying to customize a ListView's GridViewColumnHeader on a column-by-column basis. I've been able to do this by manually setting each columns' GridViewColumnHeader as inline XAML (see the "Thing 2" column), which is OK but super repetitive, but I'd really rather have a reusable style with a control template with some attached properties (attempts are in the "Thing 1" column).

Basically, I need to style a unit of measurement text block, as well as a footer marker text block.

The attached properties appear to be set, but I haven't been able to figure out how to pull their values out in the template.

I realize that the GridColumnHeader isn't part of the visual tree, and that's likely the main hang-up here, but it seems so close!

The "Thing 1" column has the style set as inline XAML until I figure out how to get the attached property values. Ideally, it would be set in the control template like ExampleListViewHeader.

I've stripped out all the irrelevant code:

MainWindow.xaml

<Window.Resources>
        <x:Array x:Key="ExampleItems" Type="{x:Type local:ExampleItems}">
            <local:ExampleItems Cell1="Item 1-1" Cell2="Item 1-2" Cell3="Item 1-3" />
            <local:ExampleItems Cell1="Item 2-1" Cell2="Item 2-2" Cell3="Item 2-3" />
            <local:ExampleItems Cell1="Item 3-1" Cell2="Item 3-2" Cell3="Item 3-3" />
            <local:ExampleItems Cell1="Item 4-1" Cell2="Item 4-2" Cell3="Item 4-3" />
        </x:Array>

        <Style x:Key="ExampleListView" TargetType="{x:Type ListView}">
            <Setter Property="VerticalAlignment" Value="Top" />
            <Setter Property="HorizontalContentAlignment" Value="Stretch" />
            <Setter Property="BorderBrush" Value="Transparent" />
            <Setter Property="IsHitTestVisible" Value="False" />
            <Setter Property="Margin" Value="0, 0, 0, 5" />
        </Style>

        <Style x:Key="ExampleListViewHeader" TargetType="{x:Type GridViewColumnHeader}">
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type GridViewColumnHeader}">
                        <TextBlock Text="{TemplateBinding Content}" Width="{TemplateBinding Width}" Padding="2, 0" TextWrapping="Wrap" VerticalAlignment="Bottom"/>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Setter Property="FontFamily" Value="{DynamicResource PLC_Font}" />
            <Setter Property="FontSize" Value="{DynamicResource PLC_FontSize_Sub_1}" />
            <Setter Property="FontStyle" Value="Italic" />
            <Setter Property="Foreground" Value="Black" />
        </Style>
    </Window.Resources>
    
    <Grid>
        <ListView ItemsSource="{StaticResource ExampleItems}" Style="{StaticResource ExampleListView}">
            <ListView.View>
                <GridView>
                    <GridView.Columns>
                        <GridViewColumn Width="100"
                                        local:HeaderAttachedProperties.Marker="1"
                                        local:HeaderAttachedProperties.UofM=" (ft) "
                                        DisplayMemberBinding="{Binding Cell1}">
                            <GridViewColumnHeader>
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="Auto" />
                                        <ColumnDefinition Width="Auto" />
                                        <ColumnDefinition Width="Auto" />
                                    </Grid.ColumnDefinitions>

                                    <TextBlock Grid.Column="0" Text="Thing 1" VerticalAlignment="Bottom"/>
                                    <TextBlock Grid.Column="1" Text=" (ft) " FontSize="10" VerticalAlignment="Center"/>

                                    <!--<TextBlock Grid.Column="2" Text="{TemplateBinding local:HeaderAttachedProperties.Marker}"/>-->

                                    <!--<TextBlock Grid.Column="2" Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(local:HeaderAttachedProperties.Marker)}"/>-->
                                    
                                    <!--<TextBlock Grid.Column="2" Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type GridViewColumn}}, Path=(local:HeaderAttachedProperties.Marker)}"/>-->
                                    <!--<TextBlock Grid.Column="2" Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=GridViewColumn}, Path=(local:HeaderAttachedProperties.Marker)}"/>-->
                                    
                                </Grid>
                            </GridViewColumnHeader>
                        </GridViewColumn>

                        <GridViewColumn Width="100"
                                        DisplayMemberBinding="{Binding Cell2}">
                            <GridViewColumnHeader>
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="Auto" />
                                        <ColumnDefinition Width="Auto" />
                                        <ColumnDefinition Width="Auto" />
                                    </Grid.ColumnDefinitions>

                                    <TextBlock Grid.Column="0" Text="Thing 2" VerticalAlignment="Bottom"/>
                                    <TextBlock Grid.Column="1" Text=" (ft) " FontSize="10" VerticalAlignment="Center"/>

                                    <TextBlock Grid.Column="2" Text="*" FontSize="10" VerticalAlignment="Center"/>
                                </Grid>
                            </GridViewColumnHeader>
                        </GridViewColumn>
                        
                        <GridViewColumn Header="Thing 3" DisplayMemberBinding="{Binding Cell3}" HeaderContainerStyle="{StaticResource ExampleListViewHeader}" />
                    </GridView.Columns>
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>

ExampleItems

public class ExampleItems
{
    public string Cell1 { get; set; }
    public string Cell2 { get; set; }
    public string Cell3 { get; set; }
}

HeaderAttachedProperties

public class HeaderAttachedProperties : DependencyObject
{
    public static readonly DependencyProperty MarkerProperty = DependencyProperty.RegisterAttached(
        name: "Marker",
        propertyType: typeof(string),
        ownerType: typeof(HeaderAttachedProperties),
        defaultMetadata: new PropertyMetadata(""));

    public static string GetMarker(DependencyObject pDependencyObject)
    {
        return (string)pDependencyObject.GetValue(MarkerProperty);
    }

    public static void SetMarker(DependencyObject pDependencyObject, string pValue)
    {
        pDependencyObject.SetValue(MarkerProperty, pValue);
    }

    public static readonly DependencyProperty UofMProperty = DependencyProperty.RegisterAttached(
        name: "UofM",
        propertyType: typeof(string),
        ownerType: typeof(HeaderAttachedProperties),
        defaultMetadata: new PropertyMetadata(""));

    public static string GetUofM(DependencyObject pDependencyObject)
    {
        return (string)pDependencyObject.GetValue(UofMProperty);
    }

    public static void SetUofM(DependencyObject pDependencyObject, string pValue)
    {
        pDependencyObject.SetValue(UofMProperty, pValue);
    }
}

Solution

  • You do not necessarily need a ControlTemplate. You can use a DataTemplate instead. In order to bind the attached properties on GridViewColumn, you can use the Column property of GridViewColumnHeader.

    <Style x:Key="ListViewHeaderStyle" TargetType="{x:Type GridViewColumnHeader}">
       <Setter Property="FontFamily" Value="{DynamicResource PLC_Font}" />
       <Setter Property="FontSize" Value="{DynamicResource PLC_FontSize_Sub_1}" />
       <Setter Property="FontStyle" Value="Italic" />
       <Setter Property="Foreground" Value="Black" />
    </Style>
    
    <GridViewColumn Width="100"
                    local:HeaderAttachedProperties.Marker="1"
                    local:HeaderAttachedProperties.UofM=" (ft) "
                    DisplayMemberBinding="{Binding Cell1}"
                    HeaderContainerStyle="{StaticResource ListViewHeaderStyle}">
       <GridViewColumn.HeaderTemplate>
          <DataTemplate>
             <Grid>
                <Grid.ColumnDefinitions>
                   <ColumnDefinition Width="Auto" />
                   <ColumnDefinition Width="Auto" />
                   <ColumnDefinition Width="Auto" />
                </Grid.ColumnDefinitions>
                <TextBlock Grid.Column="0" Text="Thing 1" VerticalAlignment="Bottom"/>
                <TextBlock Grid.Column="1" Text="{Binding Column.(local:HeaderAttachedProperties.UofM), RelativeSource={RelativeSource AncestorType={x:Type GridViewColumnHeader}}}" FontSize="10" VerticalAlignment="Center"/>
                <TextBlock Grid.Column="2" Text="{Binding Column.(local:HeaderAttachedProperties.Marker), RelativeSource={RelativeSource AncestorType={x:Type GridViewColumnHeader}}}"/>
             </Grid>
          </DataTemplate>
       </GridViewColumn.HeaderTemplate>
    </GridViewColumn>
    

    If you parameterize the first TextBlock, too, you could extract and reuse it.

    <DataTemplate x:Key="ListViewHeaderTemplate">
       <Grid>
          <Grid.ColumnDefinitions>
             <ColumnDefinition Width="Auto" />
             <ColumnDefinition Width="Auto" />
             <ColumnDefinition Width="Auto" />
          </Grid.ColumnDefinitions>
          <TextBlock Grid.Column="0" Text="{Binding Column.(local:HeaderAttachedProperties.Anything), RelativeSource={RelativeSource AncestorType={x:Type GridViewColumnHeader}}}" FontSize="10" VerticalAlignment="Center"/>
          <TextBlock Grid.Column="1" Text="{Binding Column.(local:HeaderAttachedProperties.UofM), RelativeSource={RelativeSource AncestorType={x:Type GridViewColumnHeader}}}" FontSize="10" VerticalAlignment="Center"/>
          <TextBlock Grid.Column="2" Text="{Binding Column.(local:HeaderAttachedProperties.Marker), RelativeSource={RelativeSource AncestorType={x:Type GridViewColumnHeader}}}"/>
       </Grid>
    </DataTemplate>
    
    <GridViewColumn Width="100"
                    local:HeaderAttachedProperties.Marker="1"
                    local:HeaderAttachedProperties.UofM=" (ft) "
                    DisplayMemberBinding="{Binding Cell1}"
                    HeaderTemplate="{StaticResource ListViewHeaderTemplate}"
                    HeaderContainerStyle="{StaticResource ListViewHeaderStyle}">
    </GridViewColumn>
    

    You can of course change the ControlTemplate instead, but you should be careful, because the it defines the visual appearance and states of the control. In your current template, you lose most of the states.

    <Style x:Key="ListViewHeader" TargetType="{x:Type GridViewColumnHeader}">
       <Setter Property="Template">
          <Setter.Value>
             <ControlTemplate TargetType="{x:Type GridViewColumnHeader}">
                <Grid>
                   <Grid.ColumnDefinitions>
                      <ColumnDefinition Width="Auto" />
                      <ColumnDefinition Width="Auto" />
                      <ColumnDefinition Width="Auto" />
                   </Grid.ColumnDefinitions>
                   <TextBlock Grid.Column="0" Text="{Binding Column.(local:HeaderAttachedProperties.Anything), RelativeSource={RelativeSource TemplatedParent}}" FontSize="10" VerticalAlignment="Center"/>
                   <TextBlock Grid.Column="1" Text="{Binding Column.(local:HeaderAttachedProperties.UofM), RelativeSource={RelativeSource TemplatedParent}}" FontSize="10" VerticalAlignment="Center"/>
                   <TextBlock Grid.Column="2" Text="{Binding Column.(local:HeaderAttachedProperties.Marker), RelativeSource={RelativeSource TemplatedParent}}"/>
                </Grid>
             </ControlTemplate>
          </Setter.Value>
       </Setter>
       <Setter Property="FontFamily" Value="{DynamicResource PLC_Font}" />
       <Setter Property="FontSize" Value="{DynamicResource PLC_FontSize_Sub_1}" />
       <Setter Property="FontStyle" Value="Italic" />
       <Setter Property="Foreground" Value="Black" />
    </Style>
    
    <GridViewColumn Width="100"
                    local:HeaderAttachedProperties.Marker="1"
                    local:HeaderAttachedProperties.UofM=" (ft) "
                    DisplayMemberBinding="{Binding Cell1}">
       <GridViewColumnHeader Style="{StaticResource ListViewHeader}"/>
    </GridViewColumn>
    

    I omitted the TextWrapping and VerticalAlignment properties, reintroduce them where intended.