22

I am looking to get the row number into the RowHeader of the WPF 4 DataGrid so it has an Excel-like column for the row numbers of the DataGrid.

The solution I've seen out there on the web suggests adding an index field to the business objects. This isn't really an option because the DataGrid will be getting resorted a lot and we don't want to have to keep track of changing these index fields constantly.

Thanks a lot

Greg Andora
  • 1,372
  • 2
  • 11
  • 17

5 Answers5

48

One way is to add them in the LoadingRow event for the DataGrid.

<DataGrid Name="DataGrid" LoadingRow="DataGrid_LoadingRow" ... />
void DataGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
    // Adding 1 to make the row count start at 1 instead of 0
    // as pointed out by daub815
    e.Row.Header = (e.Row.GetIndex() + 1).ToString(); 
}

Update
To get this to work with the .NET 3.5 DataGrid in WPF Toolkit a little modification is needed. The index is still generated correctly but the output fails when using virtualization. The following modification to the RowHeaderTemplate fixes this

<toolkit:DataGrid LoadingRow="DataGrid_LoadingRow">
    <toolkit:DataGrid.RowHeaderTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type toolkit:DataGridRow}},
                                      Path=Header}"/>
        </DataTemplate>
    </toolkit:DataGrid.RowHeaderTemplate>
</toolkit:DataGrid>

Edit 2012-07-05
If items are added or removed from the source list then the numbers get out of sync until the list is scrolled so LoadingRow is called again. Working around this issue is a little more complex and the best solution I can think of right now is to keep the LoadingRow solution above and also

  • Subscribe to dataGrid.ItemContainerGenerator.ItemsChanged
  • In the event handler, find all the child DataGridRows in the visual tree
  • Set the Header to the index for each DataGridRow

Here is an attached behavior which does this. Use it like this

<DataGrid ItemsSource="{Binding ...}"
          behaviors:DataGridBehavior.DisplayRowNumber="True">

DisplayRowNumber

public class DataGridBehavior
{
    #region DisplayRowNumber

    public static DependencyProperty DisplayRowNumberProperty =
        DependencyProperty.RegisterAttached("DisplayRowNumber",
                                            typeof(bool),
                                            typeof(DataGridBehavior),
                                            new FrameworkPropertyMetadata(false, OnDisplayRowNumberChanged));
    public static bool GetDisplayRowNumber(DependencyObject target)
    {
        return (bool)target.GetValue(DisplayRowNumberProperty);
    }
    public static void SetDisplayRowNumber(DependencyObject target, bool value)
    {
        target.SetValue(DisplayRowNumberProperty, value);
    }

    private static void OnDisplayRowNumberChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        DataGrid dataGrid = target as DataGrid;
        if ((bool)e.NewValue == true)
        {
            EventHandler<DataGridRowEventArgs> loadedRowHandler = null;
            loadedRowHandler = (object sender, DataGridRowEventArgs ea) =>
            {
                if (GetDisplayRowNumber(dataGrid) == false)
                {
                    dataGrid.LoadingRow -= loadedRowHandler;
                    return;
                }
                ea.Row.Header = ea.Row.GetIndex();
            };
            dataGrid.LoadingRow += loadedRowHandler;

            ItemsChangedEventHandler itemsChangedHandler = null;
            itemsChangedHandler = (object sender, ItemsChangedEventArgs ea) =>
            {
                if (GetDisplayRowNumber(dataGrid) == false)
                {
                    dataGrid.ItemContainerGenerator.ItemsChanged -= itemsChangedHandler;
                    return;
                }
                GetVisualChildCollection<DataGridRow>(dataGrid).
                    ForEach(d => d.Header = d.GetIndex());
            };
            dataGrid.ItemContainerGenerator.ItemsChanged += itemsChangedHandler;
        }
    }

    #endregion // DisplayRowNumber

    #region Get Visuals

    private static List<T> GetVisualChildCollection<T>(object parent) where T : Visual
    {
        List<T> visualCollection = new List<T>();
        GetVisualChildCollection(parent as DependencyObject, visualCollection);
        return visualCollection;
    }

    private static void GetVisualChildCollection<T>(DependencyObject parent, List<T> visualCollection) where T : Visual
    {
        int count = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < count; i++)
        {
            DependencyObject child = VisualTreeHelper.GetChild(parent, i);
            if (child is T)
            {
                visualCollection.Add(child as T);
            }
            if (child != null)
            {
                GetVisualChildCollection(child, visualCollection);
            }
        }
    }

    #endregion // Get Visuals
}
Fredrik Hedblad
  • 83,499
  • 23
  • 264
  • 266
  • 3
    The index starts at 0, so you should probably add 1. – kevindaub Jan 11 '11 at 23:29
  • 1
    Googling, you will find this answer offered in other forums as well and it is simply wrong. The conditions under which it may work are so restrictive as to make this 'solution' a joke. Like, if you add 5 items to your list and then never scroll it, never resize it, never add or remove items, never do anything that will affect redisplay of those items, then you are fine. The fact is that row index is an index in an internal collection of containers for row data (items). That has nothing to do with an index of an item (data) itself inside its collection. – Tony May 26 '12 at 13:16
  • @Tony: The only scenario I can see where this wouldn't work straight away is when you want something more displayed in the header. Then it would take a little more work. I've used this solution many times and it has nevered failed. What kind of problems do you have with it? – Fredrik Hedblad May 26 '12 at 15:00
  • @Meleak: all the numbers get wrong as soon as I scroll. Say I have 10 items, as I scroll to see 11-th it shows index 0, not 11. If I set EnableRowVirtualization="False" then it all works fine. Did other people just had this turned off by luck? – Tony May 26 '12 at 19:06
  • That's really weird. I've never had that problem with Virtualization, it should work just fine. I'll upload a sample – Fredrik Hedblad May 26 '12 at 19:39
  • I think I see where the difference is: .NET 4.0 DataGrid and WPFToolkit DataGrid (that I am using) behave differently in this respect.I will re-vote up. – Tony May 26 '12 at 19:44
  • @Tony: Added a sample project that shows row numbers with virtualization. Try if that sample project is working for you – Fredrik Hedblad May 26 '12 at 19:54
  • @Tony: Ah I see. Haven't used the 3.5 toolkit `DataGrid` in a while so I wasn't aware of that. I'll add that to the answer. – Fredrik Hedblad May 26 '12 at 19:57
  • @Meleak - I pushed it up twice now, first removing my down vote then up voting once again. The problem is simply that I was using WPFToolkit DataGrid. Even if I compiled it under .NET 4.0, it is not WPF 4.0 as title says. – Tony May 26 '12 at 19:58
  • @Tony: I'm going trough the source code for the toolkit `DataGrid` now to see if I can find the difference. I'll update if I can come up with a workaround. Thx for the upvote even though the answer didn't help you :) – Fredrik Hedblad May 26 '12 at 20:29
  • 1
    @Tony: See my update for how to get it to work with the Toolkit `DataGrid` – Fredrik Hedblad May 26 '12 at 20:47
  • 2
    Thanks alot, you just saved me a bunch of work. The solution via the behaviour is just perfect :) – basti Sep 19 '12 at 09:19
  • Make sure an attribute `HeadersVisibility` is set to `All` or `Row`. `Columns` and `None` will not display that. For example: `` – p__d Jun 18 '18 at 12:50
9

Edit: Apparently scrolling changes the index so the binding won't work like that...

A (seemingly) clean templating solution:
Xaml:

<Window
    ...
    xmlns:local="clr-namespace:Test"
    DataContext="{Binding RelativeSource={RelativeSource Mode=Self}}">
    <Window.Resources>
        <local:RowToIndexConv x:Key="RowToIndexConv"/>
    </Window.Resources>
        <DataGrid ItemsSource="{Binding GridData}">
            <DataGrid.RowHeaderTemplate>
                <DataTemplate>
                    <TextBlock Margin="2" Text="{Binding RelativeSource={RelativeSource AncestorType=DataGridRow}, Converter={StaticResource RowToIndexConv}}"/>
                </DataTemplate>
            </DataGrid.RowHeaderTemplate>
        </DataGrid>
</Window>

Converter:

public class RowToIndexConv : IValueConverter
{

    #region IValueConverter Members

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        DataGridRow row = value as DataGridRow;
        return row.GetIndex() + 1;
    }

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

    #endregion
}
H.B.
  • 166,899
  • 29
  • 327
  • 400
  • Unfortunately this solution doesn't work, you'll get the right row numbers to begin with, but when you start scrolling, sorting etc. they start to fail – Fredrik Hedblad Jan 11 '11 at 23:52
  • Unfortunately, Meleak is correct. The row numbers in the RowHeader get resorted with the rows. +1 though for the converter. I was having trouble figuring out how to grab the index of the rows. – Greg Andora Jan 12 '11 at 01:33
  • Oh, i didn't know scrolling changes the index, i've never used DataGrid in a project before; and there i though you wouldn't need messy code-behind, too bad... – H.B. Jan 12 '11 at 01:36
3

All of this approaches will not work if you add or remove rows. You should refresh row indexes in such cases. Look at this behavior:

public static class DataGridBehavior
{
    #region RowNumbers property

    public static readonly DependencyProperty RowNumbersProperty =
        DependencyProperty.RegisterAttached("RowNumbers", typeof (bool), typeof (DataGridBehavior), 
        new FrameworkPropertyMetadata(false, OnRowNumbersChanged));

    private static void OnRowNumbersChanged(DependencyObject source, DependencyPropertyChangedEventArgs args)
    {
        DataGrid grid = source as DataGrid;
        if (grid == null)
            return;
        if ((bool)args.NewValue)
        {
            grid.LoadingRow += onGridLoadingRow;
            grid.UnloadingRow += onGridUnloadingRow;
        }
        else
        {
            grid.LoadingRow -= onGridLoadingRow;
            grid.UnloadingRow -= onGridUnloadingRow;
        }
    }

    private static void refreshDataGridRowNumbers(object sender)
    {
        DataGrid grid = sender as DataGrid;
        if (grid == null)
            return;

        foreach (var item in grid.Items)
        {
            var row = (DataGridRow)grid.ItemContainerGenerator.ContainerFromItem(item);
            if (row != null)
                row.Header = row.GetIndex() + 1;
        }
    }

    private static void onGridUnloadingRow(object sender, DataGridRowEventArgs e)
    {
        refreshDataGridRowNumbers(sender);
    }

    private static void onGridLoadingRow(object sender, DataGridRowEventArgs e)
    {
        refreshDataGridRowNumbers(sender);
    }

    [AttachedPropertyBrowsableForType(typeof(DataGrid))]
    public static void SetRowNumbers(DependencyObject element, bool value)
    {
        element.SetValue(RowNumbersProperty, value);
    }

    public static bool GetRowNumbers(DependencyObject element)
    {
        return (bool) element.GetValue(RowNumbersProperty);
    }

    #endregion
}
sedovav
  • 1,986
  • 1
  • 17
  • 28
1

@Fredrik Hedblad's answer works for me. Thanks!

I added another property to get an "offset" value so the DataGrid can start from either 0 or 1 (or whatever is set).

To use the behavior, (note 'b' is the namespace)

<DataGrid ItemsSource="{Binding ...}"
      b:DataGridBehavior.DisplayRowNumberOffset="1"
      b:DataGridBehavior.DisplayRowNumber="True">

The modified classes:

/// <summary>
/// Collection of DataGrid behavior
/// </summary>
public static class DataGridBehavior
{
    #region DisplayRowNumberOffset

    /// <summary>
    /// Sets the starting value of the row header if enabled
    /// </summary>
    public static DependencyProperty DisplayRowNumberOffsetProperty =
        DependencyProperty.RegisterAttached("DisplayRowNumberOffset",
                                            typeof(int),
                                            typeof(DataGridBehavior),
                                            new FrameworkPropertyMetadata(0, OnDisplayRowNumberOffsetChanged));

    public static int GetDisplayRowNumberOffset(DependencyObject target)
    {
        return (int)target.GetValue(DisplayRowNumberOffsetProperty);
    }

    public static void SetDisplayRowNumberOffset(DependencyObject target, int value)
    {
        target.SetValue(DisplayRowNumberOffsetProperty, value);
    }

    private static void OnDisplayRowNumberOffsetChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        DataGrid dataGrid = target as DataGrid;
        int offset = (int)e.NewValue;

        if (GetDisplayRowNumber(target))
        {
            WPFUtil.GetVisualChildCollection<DataGridRow>(dataGrid).
                    ForEach(d => d.Header = d.GetIndex() + offset);
        }
    }

    #endregion

    #region DisplayRowNumber

    /// <summary>
    /// Enable display of row header automatically
    /// </summary>
    /// <remarks>
    /// Source: 
    /// </remarks>
    public static DependencyProperty DisplayRowNumberProperty =
        DependencyProperty.RegisterAttached("DisplayRowNumber",
                                            typeof(bool),
                                            typeof(DataGridBehavior),
                                            new FrameworkPropertyMetadata(false, OnDisplayRowNumberChanged));

    public static bool GetDisplayRowNumber(DependencyObject target)
    {
        return (bool)target.GetValue(DisplayRowNumberProperty);
    }

    public static void SetDisplayRowNumber(DependencyObject target, bool value)
    {
        target.SetValue(DisplayRowNumberProperty, value);
    }

    private static void OnDisplayRowNumberChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        DataGrid dataGrid = target as DataGrid;
        if ((bool)e.NewValue == true)
        {
            int offset = GetDisplayRowNumberOffset(target);

            EventHandler<DataGridRowEventArgs> loadedRowHandler = null;
            loadedRowHandler = (object sender, DataGridRowEventArgs ea) =>
            {
                if (GetDisplayRowNumber(dataGrid) == false)
                {
                    dataGrid.LoadingRow -= loadedRowHandler;
                    return;
                }
                ea.Row.Header = ea.Row.GetIndex() + offset;
            };
            dataGrid.LoadingRow += loadedRowHandler;

            ItemsChangedEventHandler itemsChangedHandler = null;
            itemsChangedHandler = (object sender, ItemsChangedEventArgs ea) =>
            {
                if (GetDisplayRowNumber(dataGrid) == false)
                {
                    dataGrid.ItemContainerGenerator.ItemsChanged -= itemsChangedHandler;
                    return;
                }
                WPFUtil.GetVisualChildCollection<DataGridRow>(dataGrid).
                    ForEach(d => d.Header = d.GetIndex() + offset);
            };
            dataGrid.ItemContainerGenerator.ItemsChanged += itemsChangedHandler;
        }
    }

    #endregion // DisplayRowNumber
}
T.Yu
  • 11
  • 1
0

LoadingRowEvent is triggered by this:

ICollectionView view = CollectionViewSource.GetDefaultView(_dataGrid.ItemsSource);
view.Refresh();
fancyPants
  • 50,732
  • 33
  • 89
  • 96