WPF 4数据表格:将行号放入行标头

22

我想把行号添加到WPF 4 DataGrid的行头中,以便为DataGrid的行数创建类似Excel的列。

网络上提供的解决方法建议将索引字段添加到业务对象中。但这不是一个真正的选择,因为DataGrid会经常被重新排序,我们不想不断跟踪更改这些索引字段。

非常感谢。

5个回答

48
一种方法是在 DataGrid 的 LoadingRow 事件中添加它们。
<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(); 
}

更新
要使这个功能在WPF Toolkit中与.NET 3.5 DataGrid一起使用,需要进行一些修改。索引仍然可以正确生成,但在使用虚拟化时输出会失败。对RowHeaderTemplate进行以下修改可以解决此问题。

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

编辑 2012-07-05
如果从源列表中添加或删除项目,则数字将不同步,直到滚动列表时再次调用LoadingRow。解决这个问题有点复杂,我现在能想到的最好的解决方案是保持上面的LoadingRow解决方案,并且

  • 订阅 dataGrid.ItemContainerGenerator.ItemsChanged
  • 在事件处理程序中,在可视树中查找所有子DataGridRows
  • 为每个DataGridRow设置标题索引

这是一个可以实现此功能的附加行为。使用方法如下:

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

3
索引从0开始,因此您可能需要加1。 - kevindaub
1
通过谷歌搜索,你会发现其他论坛也提供了这个答案,但它是错误的。它可能起作用的条件非常严格,以至于这个“解决方案”就像一个笑话。比如,如果你向列表中添加了5个项目,然后从未滚动它,从未调整它的大小,从未添加或删除项目,从未执行任何会影响这些项目重新显示的操作,那么你就没问题了。事实上,行索引是行数据(项目)的内部容器集合中的索引。这与其集合内部的项(数据)本身的索引无关。 - Tony
@Tony:我能想到的唯一场景是当你想要在头部展示更多东西时,它就不能立即工作。那么需要进行更多的工作。我已经使用了这个解决方案很多次,它从来没有失败过。你对它有什么问题? - Fredrik Hedblad
1
@Tony:请看我的更新,了解如何让它与工具包“DataGrid”一起正常工作。 - Fredrik Hedblad
2
非常感谢,你刚刚帮我省了很多工作。通过行为的解决方案真是太完美了 :) - basti
显示剩余8条评论

9

编辑:显然滚动会改变索引,因此绑定不会像那样起作用...

一个(看起来)清晰的模板解决方案:
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>

转换器:

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
}

不幸的是,这个解决方案不起作用。你一开始会得到正确的行号,但是当你开始滚动、排序等操作时,它们就会开始失效。 - Fredrik Hedblad
不幸的是,Meleak是正确的。RowHeader中的行号会随着行的重新排序而重新排序。对于转换器来说,+1。我一直在努力弄清楚如何获取行的索引。 - Greg Andora
哦,我不知道滚动会改变索引,我以前从未在项目中使用过DataGrid;我还以为你不需要混乱的代码后台,太糟糕了... - H.B.

3
所有这些方法都无法解决添加或删除行的问题。在这种情况下,您应该刷新行索引。看看这个示例:
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
}

当我更改所选记录的索引时,这种行为对我有效。 - Eduards

1

@Fredrik Hedblad的答案对我有用。谢谢!

我添加了另一个属性来获取“偏移”值,以便DataGrid可以从0或1(或设置的任何值)开始。

要使用此行为,请注意'b'是命名空间。

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

修改后的类:
/// <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
}

什么是WPFUtil? - W.M.
1
WPFUtil似乎是Hedblads的“获取可视化区域”重构为另一个类。 - Lakedaimon

0

LoadingRowEvent是由以下内容触发的:

ICollectionView view = CollectionViewSource.GetDefaultView(_dataGrid.ItemsSource);
view.Refresh();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接