如何使用稳定排序对DataGrid进行排序?

9

我有一个WPF数据网格,可以通过单击列标题来对其进行排序。它可以工作,但不稳定。如何使其进行稳定排序?

我的意思是,如果我有这个表:

Class    | Student    | Grade
-----------------------------
Art      | James      |  A
Art      | Amy        |  B
Art      | Charlie    |  A
Science  | James      |  D
Science  | Amy        |  A
Science  | Charlie    |  C
History  | James      |  B
History  | Amy        |  A
History  | Charlie    |  C

如果按学生排序,它会像你期望的那样工作:

Class    | Student    | Grade
-----------------------------
Art      | Amy        |  B
Science  | Amy        |  A
History  | Amy        |  A
Art      | Charlie    |  A
Science  | Charlie    |  C
History  | Charlie    |  C
Art      | James      |  A
Science  | James      |  D
History  | James      |  B

但是,如果我现在按类别排序:
Class    | Student    | Grade
-----------------------------
Art      | James      |  A
Art      | Amy        |  B
Art      | Charlie    |  A
History  | James      |  B
History  | Amy        |  A
History  | Charlie    |  C
Science  | James      |  D
Science  | Amy        |  A
Science  | Charlie    |  C

它破坏了学生的排序顺序(不稳定排序)。我想要的是稳定排序,它可以保留顺序。
Class    | Student    | Grade
-----------------------------
Art      | Amy        |  B
Art      | Charlie    |  A
Art      | James      |  A
History  | Amy        |  A
History  | Charlie    |  C
History  | James      |  B
Science  | Amy        |  A
Science  | Charlie    |  C
Science  | James      |  D

似乎默认情况下应该这样工作,或者至少应该是一个切换开关。有人有什么建议吗?@Eirik的想法是shift-clicking可以实现,这表明了这种行为已经存在。但是,我真正想要的是在没有任何修改器的情况下就能像那样工作。它不应该成为“按这个排序,然后再按这个排序,然后再按这个排序”的原因,而应该是将算法交换为另一种算法的情况。
请参见:http://en.wikipedia.org/wiki/Sorting_algorithm#Stability

你是想在后台代码中实现这个功能,还是希望有人知道如何将 Shift+单击行为设为默认行为? - Grubsnik
@Grubsnik 我希望有人知道如何更改排序算法,因为我认为 shift-click 方法是一个主要的 hack。然而,它确实能够完成工作。 - TarkaDaal
2个回答

9
当您按住Shift键并单击列时,应该能够按多个列进行排序。 尝试单击班级列,然后按住Shift键并单击学生列。
这是在代码后端添加排序的解决方案:
private void myDataGridPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
    DependencyObject dep = (DependencyObject)e.OriginalSource;

    while ((dep != null) && !(dep is DataGridColumnHeader))
    {
        dep = VisualTreeHelper.GetParent(dep);
    }

    if (dep == null)
        return;

    if (dep is DataGridColumnHeader)
    {
        DataGridColumnHeader columnHeader = dep as DataGridColumnHeader;

        ICollectionView view = CollectionViewSource.GetDefaultView((sender as DataGrid).ItemsSource);

        if (columnHeader.Content.Equals("Class") || columnHeader.Content.Equals("Student"))
        {
            view.SortDescriptions.Clear();
            view.SortDescriptions.Add(new SortDescription("Class", ListSortDirection.Ascending));
            view.SortDescriptions.Add(new SortDescription("Student", ListSortDirection.Ascending));
        }
    }
}

为了使其工作,您必须禁用标准排序。一种方法是停止排序事件,就像这样:

private void myDataGridSorting(object sender, DataGridSortingEventArgs e)
{
    e.Handled = true;
}

编辑:在阅读了hbarck的评论后,我再次仔细阅读了你的问题,发现自己漏掉了一些部分。如果你修改这段代码:

if (columnHeader.Content.Equals("Class") || columnHeader.Content.Equals("Student"))
{
    view.SortDescriptions.Clear();
    view.SortDescriptions.Add(new SortDescription("Class", ListSortDirection.Ascending));
    view.SortDescriptions.Add(new SortDescription("Student", ListSortDirection.Ascending));
}

转换为:

if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
    view.SortDescriptions.Clear();
}

view.SortDescriptions.Insert(0, new SortDescription(columnHeader.Content.ToString(), ListSortDirection.Ascending));

您将拥有稳定的排序。点击“学生”按学生排序,然后点击“班级”按班级和学生排序。 如果在单击时按住ctrl键,则会在按列单击进行排序之前清除先前的排序。


1
@TarkaDaal,那你如何“取消”之前的排序呢?比如你想先按班级和学生排序,但之后你想按班级和成绩排序。你需要实现一种设置新列首先排序的方法,例如在单击时按住Shift键... ;) - Eirik
你不需要这样做。如果你想按照班级排序,然后再按照成绩排序,只需点击班级,然后再点击成绩即可。学生的顺序并不重要。这不是关于多列排序,而是尽可能保留之前的顺序。 - TarkaDaal
@TarkaDaal编辑了答案并提供了在代码后端进行排序的解决方案。 - Eirik
1
你的代码中还缺少什么:为了实现 OP 想要的功能,你不应该清除 SortDescriptions,而是应该首先移除最近点击的排序列的任何 SortDescription(如果有的话),然后在列表开头插入一个新的 SortDescription。这样,你就始终将其他排序作为较低优先级排序保留。 - hbarck
@TarkaDaal 在阅读了hbarck的评论后,我再次阅读了您的问题和评论,似乎错过了一些东西。我编辑了答案,并添加了一个解决方案,以在用户单击多个列时保留排序。 - Eirik
显示剩余2条评论

2

我已经成功使用自定义比较器实现了稳定的排序,但这种方法感觉有点像大规模的黑客攻击...

我使用ListCollectionView的CustomSort属性来设置我的自定义比较器,这需要在实例化时将集合传递给它。

private void Sorting(IEnumerable collection)
{
    var view = CollectionViewSource.GetDefaultView(collection) as ListCollectionView;

    if (view != null)
    {
        view.CustomSort = new StableComparer(collection);
    }
}

在我的自定义比较器中,我在Compare方法中使用集合,只是在常规比较返回零(它们相同或具有相同的值)时回退到项索引。
public class StableComparer : IComparer
{
    public IEnumerable Collection { get; set; }

    public StableComparer(IEnumerable collection)
    {
        Collection = collection;
    }

    public int Compare(object x, object y)
    {
        IComparable x_Comparable = x as IComparable;
        IComparable y_Comparable = y as IComparable;

        if (x_Comparable != null && y_Comparable != null)
        {
            var comparison = x_Comparable.CompareTo(y_Comparable);

            // A zero value means x and y are equivalent for sorting, and they could
            //  be rearranged by an unstable sorting algorithm
            if (comparison == 0 && Collection != null)
            {
                // IndexOf is an extension method for IEnumerable (not included)
                var x_Index = Collection.IndexOf(x);
                var y_Index = Collection.IndexOf(y);

                // By comparing their indexes in the original collection, we get to
                //  preserve their relative order
                if (x_Index != -1 && y_Index != -1)
                    comparison = x_Index.CompareTo(y_Index);
            }

            return comparison;
        }

        return 0;
    }
}

我还在测试中,所以不能保证这种方法始终有效...一个问题是如何保持Comparer内的Collection属性更新。或者支持两个排序方向(我正在处理,应该不难)。或者检查性能如何。

但我认为这个想法很清晰;虽然有点hacky,就像我说的那样。


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