My blog about application development on the .NET platform.

Adding right-aligned row numbers to a DataGridRowHeader in WPF

This post provides an example of how you can right-align or centre the text in a DataGridRowHeader in a DataGrid in WPF using Visual Studio 2012 or later. It also explains how you can display the correct row numbers in the DataGridRowHeader and automatically update these as you are adding or removing items to and from the DataGrid’s ItemsSource collection dynamically.

DataGrid.RowHeaderTemplate

The DataGrid control has a RowHeaderTemplate property that defines the DataTemplate for the DataGridRowHeader. A DataGridRowHeader is by default displayed to the left of the left-most data column on each row in the DataGrid. It doesn’t scroll horizontally with the rest of the columns and is usually used to display some additional information that associated with a specific row, for example a row number.

The RowHeaderTemplate DataTemplate doesn’t inherit the data context from the DataGridRow but since it gets added to the visual tree, you can use a binding with a RelativeSource to bind to any property of the DataContext of the parent DataGridRow element. For example, the following DataGrid displays a list of Country objects and has a RowHeaderTemplate that contains of a TextBlock that displays the value of the Continent property of each Country object:

<DataGrid ItemsSource="{Binding Countries}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Country" Binding="{Binding Name}"/>
    </DataGrid.Columns>
    <DataGrid.RowHeaderTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding DataContext.Continent, 
                        RelativeSource={RelativeSource AncestorType=DataGridRow}}"></TextBlock>
        </DataTemplate>
    </DataGrid.RowHeaderTemplate>
</DataGrid>
public class Country
{
    public string Name { get; set; }
    public string Continent { get; set; }
}

DataGrid.RowHeaderStyle

It should be mentioned that in case you only want do display a data bound value in the DataGridRowHeader, but still use the default appearance, you could define a RowHeaderStyle for the DataGrid that simply binds the Content property of the DataGridRowHeader to the source property instead of setting the DataGrid’s RowHeaderTemplate property to a DataTemplate. The following XAML markup will produce the same visual appearance of the DataGridRowHeader as the above markup does:

<DataGrid ItemsSource="{Binding Countries}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Country" Binding="{Binding Name}"/>
    </DataGrid.Columns>
    <DataGrid.RowHeaderStyle>
        <Style TargetType="{x:Type DataGridRowHeader}">
            <Setter Property="Content" Value="{Binding Continent}"/>
        </Style>
    </DataGrid.RowHeaderStyle>
</DataGrid>

Aligning

Now, if you for example want to right-align the contents of the DataGridRowHeader, the easiest way to do this is to override the default ControlTemplate for the DataGridRowHeader elements. You can generate the default template in Visual Studio 2012 or later by right-click on the DataGrid in design mode and select “Edit Additional Templates” -> “Edit RowHeaderStyle” -> “Edit a Copy”. Once you have given the new style resource a name and chosen where to generate it, you can then modify the generated XAML template according to your requirements.

The markup that Visual Studio generates for you may vary a bit between the different versions of Windows, but on Windows 8 all you have to do to right-align the contents of the DataGridRowHeader is to set the HorizontalAlignment property of the DataGridHeaderBorder element in the generated ControlTemplate to Right:

<DataGrid ItemsSource="{Binding Countries}" AutoGenerateColumns="False">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Country" Binding="{Binding Name}"/>
    </DataGrid.Columns>
    <DataGrid.RowHeaderTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding DataContext.Continent, 
                        RelativeSource={RelativeSource AncestorType=DataGridRow}}"></TextBlock>
        </DataTemplate>
    </DataGrid.RowHeaderTemplate>
    <DataGrid.RowHeaderStyle>
        <Style TargetType="{x:Type DataGridRowHeader}">
            <!-- Override ControlTemplate -->
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type DataGridRowHeader}">
                        <Grid xmlns:Themes="clr-namespace:Microsoft.Windows.Themes;assembly=PresentationFramework.Aero2">
                            <Themes:DataGridHeaderBorder BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" IsPressed="{TemplateBinding IsPressed}" 
                                                                 IsHovered="{TemplateBinding IsMouseOver}" IsSelected="{TemplateBinding IsRowSelected}" 
                                                                 Orientation="Horizontal" Padding="{TemplateBinding Padding}" 
                                                                 SeparatorBrush="{TemplateBinding SeparatorBrush}" 
                                                                 SeparatorVisibility="{TemplateBinding SeparatorVisibility}"
                                                                 HorizontalAlignment="Right">
                                <StackPanel Orientation="Horizontal">
                                    <ContentPresenter ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" ContentStringFormat="{TemplateBinding ContentStringFormat}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
                                                              VerticalAlignment="Center"/>
                                    <Control SnapsToDevicePixels="False" Template="{Binding ValidationErrorTemplate, RelativeSource={RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type DataGridRow}}}">
                                        <Control.Visibility>
                                            <Binding Path="(Validation.HasError)" RelativeSource="{RelativeSource FindAncestor, AncestorLevel=1, AncestorType={x:Type DataGridRow}}">
                                                <Binding.Converter>
                                                    <BooleanToVisibilityConverter/>
                                                </Binding.Converter>
                                            </Binding>
                                        </Control.Visibility>
                                    </Control>
                                </StackPanel>
                            </Themes:DataGridHeaderBorder>
                            <Thumb x:Name="PART_TopHeaderGripper" VerticalAlignment="Top">
                                <Thumb.Style>
                                    <Style TargetType="{x:Type Thumb}">
                                        <Setter Property="Height" Value="8"/>
                                        <Setter Property="Background" Value="Transparent"/>
                                        <Setter Property="Cursor" Value="SizeNS"/>
                                        <Setter Property="Template">
                                            <Setter.Value>
                                                <ControlTemplate TargetType="{x:Type Thumb}">
                                                    <Border Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}"/>
                                                </ControlTemplate>
                                            </Setter.Value>
                                        </Setter>
                                    </Style>
                                </Thumb.Style>
                            </Thumb>
                            <Thumb x:Name="PART_BottomHeaderGripper" VerticalAlignment="Bottom">
                                <Thumb.Style>
                                    <Style TargetType="{x:Type Thumb}">
                                        <Setter Property="Height" Value="8"/>
                                        <Setter Property="Background" Value="Transparent"/>
                                        <Setter Property="Cursor" Value="SizeNS"/>
                                        <Setter Property="Template">
                                            <Setter.Value>
                                                <ControlTemplate TargetType="{x:Type Thumb}">
                                                    <Border Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}"/>
                                                </ControlTemplate>
                                            </Setter.Value>
                                        </Setter>
                                    </Style>
                                </Thumb.Style>
                            </Thumb>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </DataGrid.RowHeaderStyle>
</DataGrid>

In the above XAML markup, I have copied the generated ControlTemplate and defined it inline within the Style setter but you might as well put it in the -section of the DataGrid, Window or Application (App.xaml) and reference it using a StaticResource or DynamicResource markup extension as usual:

<DataGrid.RowHeaderStyle>
    <Style TargetType="{x:Type DataGridRowHeader}">
        <Setter Property="Template" Value="{StaticResource customDataGridRowHeaderTemplate}"/>
    </Style>
</DataGrid.RowHeaderStyle>

Row numbers

To be able to display the row numbers in the DataGridRowHeader, you could use a converter that binds to the DataGridRow element and calls its GetIndex() method:

namespace Mm.WpfApplication1
{
    internal class RowNumberConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            DataGridRow row = value as DataGridRow;
            if (row == null)
                throw new InvalidOperationException("This converter class can only be used with DataGridRow elements.");

            return row.GetIndex() + 1;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }
}

<DataGrid ItemsSource="{Binding Countries}" AutoGenerateColumns="False"
	xmlns:local="clr-namespace:Mm.WpfApplication1">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Country" Binding="{Binding Name}"/>
    </DataGrid.Columns>
    <DataGrid.Resources>
        <local:RowNumberConverter x:Key="converter"/>
    </DataGrid.Resources>
    <DataGrid.RowHeaderTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Path=., 
                        RelativeSource={RelativeSource AncestorType=DataGridRow},
                        Converter={StaticResource converter}}"></TextBlock>
        </DataTemplate>
    </DataGrid.RowHeaderTemplate>
    <DataGrid.RowHeaderStyle>
        ...
    </DataGrid.RowHeaderStyle>
</DataGrid>

The GetIndex() method returns the zero-based index of the DataGridRow’s data item within the Items collection of the DataGrid.

Virtualization

Note however that this approach will only work as expected when the DataGrid contains only a few rows, or if you set the VirtualizingStackPanel.VirtualizationMode attached property of the DataGrid to System.Windows.Controls.VirtualizationMode.Standard or completely disable the virtualization by for example setting the VirtualizingPanel.IsVirtualizing attached property of the DataGrid to false.

Virtualization basically means that only a subset of the visual elements for the data bound items, i.e. Country objects in this particular case, are displayed on the screen at the same time. DataGridRow elements will be added and removed from the visual tree as the user scrolls through the DataGrid.

When virtualization is enabled, which it is by default, and the VirtualizingStackPanel.VirtualizationMode attached property is set to its default value of System.Windows.Controls.VirtualizationMode.Recycling, the visual item containers will be reused to improve the scrolling performance and this may (or rather will) cause the row numbers to get mixed up as you scroll vertically through the records in the DataGrid. This is illustrated in the picture below:


If you want display the row numbers correctly but still keep the DataGrid’s default virtualization and recycling behaviour, you could handle the LoadingRow event for the DataGrid to set the Header property of the DataGridRow to the row number and then bind to this property in the RowHeaderTemplate:

private void OnLoadingRow(object sender, DataGridRowEventArgs e)
{
    e.Row.Header = (e.Row.GetIndex() + 1).ToString();
}
<DataGrid ItemsSource="{Binding Countries}" AutoGenerateColumns="False" LoadingRow="OnLoadingRow">
...
<DataGrid.RowHeaderTemplate>
    <DataTemplate>
        <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType=DataGridRow},
                    Path=Header}"/>
    </DataTemplate>
</DataGrid.RowHeaderTemplate>
...

Dynamically added/removed items

If the DataGrid is bound to an ObservableCollection<T> and you are adding or removing items to/from it dynamically at runtime, the row numbers won’t get updated until you start to scroll and the LoadingRow event is fired again. To fix this, you could hook up an event handler to the ItemsChanged event of the ItemContainerGenerator of the DataGrid in the view and reset the Header property of all DataGridRow elements that are currently in the visual tree when this event occurs:

public class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new ViewModel();
        dataGrid.ItemContainerGenerator.ItemsChanged += ItemContainerGenerator_ItemsChanged;
    }

    private void ItemContainerGenerator_ItemsChanged(object sender, ItemsChangedEventArgs e)
    {
        //get all DataGridRow elements in the visual tree
        IEnumerable<DataGridRow> rows = FindVisualChildren<DataGridRow>(test);
        foreach (DataGridRow row in rows)
        {
            row.Header = (row.GetIndex() + 1).ToString();
        }
    }

    private static IEnumerable<T> FindVisualChildren<T>(DependencyObject dependencyObject)
        where T : DependencyObject
    {
        if (dependencyObject != null)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObject); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(dependencyObject, i);
                if (child != null && child is T)
                {
                    yield return (T)child;
                }

                foreach (T childOfChild in FindVisualChildren<T>(child))
                {
                    yield return childOfChild;
                }
            }
        }
    }
}

The ItemsChanged event will occur whenever an item in the source collection of the DataGrid is added, removed or moved and the FindVisualChildren<T> method in the code above uses the System.Windows.Media.VisualTreeHelper class to find all elements of a specific type that are children of the passed in Sytem.Window.DependencyObject. DependencyObject is a common base class in WPF from which many controls, including the DataGrid, inherit from.

The name “dataGrid” refers to the x:Name attribute of the DataGrid control that you should remember to specify in the XAML markup:

<DataGrid x:Name="dataGrid" ItemsSource="{Binding Countries}" ...>

See also

DataGrid.RowHeaderTemplate Property: http://msdn.microsoft.com/en-us/library/system.windows.controls.datagrid.rowheadertemplate(v=vs.110).aspx (MSDN)
DataGrid.RowHeaderStyle Property: http://msdn.microsoft.com/en-us/library/system.windows.controls.datagrid.rowheaderstyle(v=vs.110).aspx (MSDN)
RelativeSource MarkupExtension: http://msdn.microsoft.com/en-us/library/ms743599(v=vs.110).aspx (MSDN)
IValueConverter Interface: http://msdn.microsoft.com/en-us/library/system.windows.data.ivalueconverter(v=vs.110).aspx (MSDN)
DataGridRow.GetIndex Method: http://msdn.microsoft.com/en-us/library/system.windows.controls.datagridrow.getindex(v=vs.110).aspx (MSDN)
Optimizing Performance: Controls: http://msdn.microsoft.com/en-us/library/cc716879(v=vs.110).aspx (MSDN)
ObservableCollection<T> Class: http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx (MSDN)


3 Comments on “Adding right-aligned row numbers to a DataGridRowHeader in WPF”

  1. Sergey says:

    Hello! Prompt how to display row numbers DataGrid using MVVM pattern?

  2. Juraj says:

    Please, as Sergey has asked, how to display row numbers using MVVM pattern in clean way?

  3. Yorgi says:

    I think it’s ok to set row number in code behind when you are using MVVM as it’s a view’s logic.

    But if you really don’t like that idea you can make use of Interactivity library to expose event as command.
    http://social.technet.microsoft.com/wiki/contents/articles/18199.event-handling-in-an-mvvm-wpf-application.aspx


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s