通过C#对ObservableCollection<string>进行排序

89

我有以下的 ObservableCollection<string>。我需要对它进行字母顺序排序

private ObservableCollection<string> _animals = new ObservableCollection<string>
{
    "Cat", "Dog", "Bear", "Lion", "Mouse",
    "Horse", "Rat", "Elephant", "Kangaroo", "Lizard", 
    "Snake", "Frog", "Fish", "Butterfly", "Human", 
    "Cow", "Bumble Bee"
};

我尝试使用_animals.OrderByDescending,但我不知道如何正确使用它。

_animals.OrderByDescending(a => a.<what_is_here_?>);

我该如何做到这一点?


可能是重复问题:https://dev59.com/bnLYa4cB1Zd3GeqPTyHD,https://dev59.com/NG025IYBdhLWcg3wyJCV。 - Sergey Vyacheslavovich Brunov
请看这里:https://dev59.com/m2865IYBdhLWcg3wIa7M#27382401 - NoWar
大多数涉及Move操作的答案在集合中存在重复项时无法正确工作。请参考以下链接以获取正确的实现方式:https://dev59.com/WnI-5IYBdhLWcg3wQV8Z#1945701/ - nawfal
15个回答

153

介绍

基本上,如果需要显示已排序的集合,请考虑使用 CollectionViewSource 类:将其 Source 属性分配(“绑定”)到源集合 - ObservableCollection<T> 类的一个实例。

这样做的想法是 CollectionViewSource 类提供了 CollectionView 类的实例
这是原始(源)集合的一种“投影”,但应用了排序、过滤等操作。

参考资料:

实时排序

WPF 4.5 引入了 CollectionViewSource 的“实时排序”功能。

参考资料:

解决方案

如果仍然需要对 ObservableCollection<T> 类的一个实例进行排序,可以按照以下步骤完成:
ObservableCollection<T> 类本身没有排序方法。但是,可以重新创建集合以排序项:

// Animals property setter must raise "property changed" event to notify binding clients.
// See INotifyPropertyChanged interface for details.
Animals = new ObservableCollection<string>
    {
        "Cat", "Dog", "Bear", "Lion", "Mouse",
        "Horse", "Rat", "Elephant", "Kangaroo",
        "Lizard", "Snake", "Frog", "Fish",
        "Butterfly", "Human", "Cow", "Bumble Bee"
    };
...
Animals = new ObservableCollection<string>(Animals.OrderBy(i => i));

额外细节

请注意,OrderBy()OrderByDescending()方法(以及其他LINQ扩展方法)不会修改源集合!它们会创建一个新的序列(即实现IEnumerable<T>接口的类的新实例)。因此,需要重新创建集合。


1
CollectionViewSource 应该尽可能地使用,但 Xamarin Forms 没有这个功能,所以不可能实现。使用 OrderBy 的第二种解决方案是不可行的,因为假设 UI 正在监听可观察集合,因为它实现了 INotifyCollectionChanged。如果创建一个新实例,那还有什么意义呢?UI 将进行完全刷新。 - Christian Findlay
在UWP中,CollectionViewSource没有未来。这是一种死胡同的模式。 - Quark Soup
@Quarkly UWP 推荐使用什么解决方案? - dgellow
我一直在使用LINQ对可观察集合进行排序。对于动画,我实际上还进行了额外的工作,对比旧的集合顺序和新的集合顺序,然后执行最小的RemoveAt/InsertAt操作以将它们放置在正确的顺序中。这是很多额外的工作,但它使动画看起来更加自然。 - Quark Soup

69

方法

我会采用的方法是,从ObservableCollection<>开始构建一个List<>,通过它的Sort()方法进行排序(更多信息请参见MSDN),当List<>排序完成后,使用Move()方法重新排序ObservableCollection<>

代码

public static void Sort<T>(this ObservableCollection<T> collection, Comparison<T> comparison)
{
    var sortableList = new List<T>(collection);
    sortableList.Sort(comparison);

    for (int i = 0; i < sortableList.Count; i++)
    {
        collection.Move(collection.IndexOf(sortableList[i]), i);
    }
}

测试

public void TestObservableCollectionSortExtension()
{
    var observableCollection = new ObservableCollection<int>();
    var maxValue = 10;

    // Populate the list in reverse mode [maxValue, maxValue-1, ..., 1, 0]
    for (int i = maxValue; i >= 0; i--)
    {
        observableCollection.Add(i);
    }

    // Assert the collection is in reverse mode
    for (int i = maxValue; i >= 0; i--)
    {
        Assert.AreEqual(i, observableCollection[maxValue - i]);
    }

    // Sort the observable collection
    observableCollection.Sort((a, b) => { return a.CompareTo(b); });

    // Assert elements have been sorted
    for (int i = 0; i < maxValue; i++)
    {
        Assert.AreEqual(i, observableCollection[i]);
    }
}

##注意事项 本文仅为概念验证,演示如何对ObservableCollection<>进行排序而不会破坏项目的绑定。排序算法有改进和验证的空间(正如此处所指出的那样,需要进行索引检查)。


3
根据MSDN,"ObservableCollection.IndexOf"方法是“O(n)操作,其中n是Count”,我怀疑"ObservableCollection.Move"方法调用"ObservableCollection.RemoveAt"和"ObservableCollection.Insert"方法,每个方法也是(同样根据MSDN)“O(n)操作,其中n是Count”。 - Tom
2
如果运行时间很重要,我认为最好使用已排序的List重新创建"ObservableCollection"(即使用"ObservableCollection<T>(List<T>)"构造函数)。就像我刚才在"D_Learning"对"Sergey Brunov"上面的回答中所评论的那样:"如果您(例如在使用MVVM模式时)使用属性(在实现了"INotifyPropertyChanged"接口的类内部)将"ObservableCollection"设置为新的排序版本,且Setter调用接口的"PropertyChanged"事件,则绑定不会断开。" - Tom
11
绑定不会被断开,但是对于任何项目事件的所有订阅(在代码中)都将被取消。此外,从头重新创建集合后,绑定的UI元素(无论是列表视图还是其他)可能需要重新创建所有的可视项,可能需要消耗更多的时间和/或内存(3 * O(n)迭代)。所以,最终正确的答案始终是相同的:这取决于具体情况。当您不能或不想重建列表时,这只是一种替代方法。 - Marco
1
如果您需要处理大数据集上的重新排序,ObservableCollection 可能不是最佳选择(当您需要保持绑定时)。CollectionViewSource 可能更适合这种情况... - Marco
7
我必须强调:CollectionViewSource 没有未来可言。它在 UWP 中不受支持,如果现在依赖它,你将会陷入过时的设计中。由于我曾因此问题而受挫,所以如果你想将你的代码未来迁移到 Windows 10,那么请坚持使用 ObservableCollection - Quark Soup
显示剩余7条评论

20

我看了这些,正在整理它们,然后它就像上面一样断开了绑定。 虽然比你们大多数人的方法更简单,但我想它似乎能实现我的目标。

public static ObservableCollection<string> OrderThoseGroups( ObservableCollection<string> orderThoseGroups)
    {
        ObservableCollection<string> temp;
        temp =  new ObservableCollection<string>(orderThoseGroups.OrderBy(p => p));
        orderThoseGroups.Clear();
        foreach (string j in temp) orderThoseGroups.Add(j);
        return orderThoseGroups;



    }

1
这远比其他解决方案优越。#1 它不会破坏绑定,#2 它不依赖于过时的模式CollectionViewSource,#3 它简单易懂,#4 它相比于“移动项目”解决方案速度惊人地快。 - Quark Soup
4
尽管这似乎比移动所有物品要快得多,但它也会触发不同的事件(删除和添加而不是移动)。这可能不是问题,具体取决于使用情况,但一定要记住这一点。 - Tim Pohlmann
@TimPohlmann - “Move” 触发事件,因此使用“Move”算法的差异是 O 操作,而使用清除和添加算法则是 O + 1 操作。 - Quark Soup
2
@DonaldAirey 我是在谈论事件被触发时使用不同的事件参数内容这一事实。根据你的事件处理程序监听什么,这可能会引起一些问题。 - Tim Pohlmann

17

这个扩展方法可以消除对整个列表进行排序的需求。

相反,它会将每个新项插入到其应该在的位置上。因此,列表始终保持排序状态。

事实证明,当许多其他方法由于集合更改时缺少通知而失败时,这种方法非常有效且速度较快。

下面的代码应该是无懈可击的; 它已在大规模生产环境中经过广泛测试。

使用方法:

// Call on dispatcher.
ObservableCollection<MyClass> collectionView = new ObservableCollection<MyClass>();
var p1 = new MyClass() { Key = "A" }
var p2 = new MyClass() { Key = "Z" }
var p3 = new MyClass() { Key = "D" }
collectionView.InsertInPlace(p1, o => o.Key);
collectionView.InsertInPlace(p2, o => o.Key);
collectionView.InsertInPlace(p3, o => o.Key);
// The list will always remain ordered on the screen, e.g. "A, D, Z" .
// Insertion speed is Log(N) as it uses a binary search.

并且扩展方法:

/// <summary>
/// Inserts an item into a list in the correct place, based on the provided key and key comparer. Use like OrderBy(o => o.PropertyWithKey).
/// </summary>
public static void InsertInPlace<TItem, TKey>(this ObservableCollection<TItem> collection, TItem itemToAdd, Func<TItem, TKey> keyGetter)
{
    int index = collection.ToList().BinarySearch(keyGetter(itemToAdd), Comparer<TKey>.Default, keyGetter);
    collection.Insert(index, itemToAdd);
}

二分搜索扩展方法:

/// <summary>
/// Binary search.
/// </summary>
/// <returns>Index of item in collection.</returns> 
/// <notes>This version tops out at approximately 25% faster than the equivalent recursive version. This 25% speedup is for list
/// lengths more of than 1000 items, with less performance advantage for smaller lists.</notes>
public static int BinarySearch<TItem, TKey>(this IList<TItem> collection, TKey keyToFind, IComparer<TKey> comparer, Func<TItem, TKey> keyGetter)
{
    if (collection == null)
    {
        throw new ArgumentNullException(nameof(collection));
    }

    int lower = 0;
    int upper = collection.Count - 1;

    while (lower <= upper)
    {
        int middle = lower + (upper - lower) / 2;
        int comparisonResult = comparer.Compare(keyToFind, keyGetter.Invoke(collection[middle]));
        if (comparisonResult == 0)
        {
            return middle;
        }
        else if (comparisonResult < 0)
        {
            upper = middle - 1;
        }
        else
        {
            lower = middle + 1;
        }
    }

    // If we cannot find the item, return the item below it, so the new item will be inserted next.
    return lower;
}

15

这是一个 ObservableCollection<T>,它在变化时自动排序,仅在必要时触发排序,并且仅触发单一移动集合变更操作。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;

namespace ConsoleApp4
{
  using static Console;

  public class SortableObservableCollection<T> : ObservableCollection<T>
  {
    public Func<T, object> SortingSelector { get; set; }
    public bool Descending { get; set; }
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
      base.OnCollectionChanged(e);
      if (SortingSelector == null 
          || e.Action == NotifyCollectionChangedAction.Remove
          || e.Action == NotifyCollectionChangedAction.Reset)
        return;
                      
      var query = this
        .Select((item, index) => (Item: item, Index: index));
      query = Descending
        ? query.OrderByDescending(tuple => SortingSelector(tuple.Item))
        : query.OrderBy(tuple => SortingSelector(tuple.Item));

      var map = query.Select((tuple, index) => (OldIndex:tuple.Index, NewIndex:index))
       .Where(o => o.OldIndex != o.NewIndex);

      using (var enumerator = map.GetEnumerator())
       if (enumerator.MoveNext())
          Move(enumerator.Current.OldIndex, enumerator.Current.NewIndex);


    }
  }

  
  //USAGE
  class Program
  {
    static void Main(string[] args)
    {
      var xx = new SortableObservableCollection<int>() { SortingSelector = i => i };
      xx.CollectionChanged += (sender, e) =>
       WriteLine($"action: {e.Action}, oldIndex:{e.OldStartingIndex},"
         + " newIndex:{e.NewStartingIndex}, newValue: {xx[e.NewStartingIndex]}");

      xx.Add(10);
      xx.Add(8);
      xx.Add(45);
      xx.Add(0);
      xx.Add(100);
      xx.Add(-800);
      xx.Add(4857);
      xx.Add(-1);

      foreach (var item in xx)
        Write($"{item}, ");
    }
  }
}

输出:

action: Add, oldIndex:-1, newIndex:0, newValue: 10
action: Add, oldIndex:-1, newIndex:1, newValue: 8
action: Move, oldIndex:1, newIndex:0, newValue: 8
action: Add, oldIndex:-1, newIndex:2, newValue: 45
action: Add, oldIndex:-1, newIndex:3, newValue: 0
action: Move, oldIndex:3, newIndex:0, newValue: 0
action: Add, oldIndex:-1, newIndex:4, newValue: 100
action: Add, oldIndex:-1, newIndex:5, newValue: -800
action: Move, oldIndex:5, newIndex:0, newValue: -800
action: Add, oldIndex:-1, newIndex:6, newValue: 4857
action: Add, oldIndex:-1, newIndex:7, newValue: -1
action: Move, oldIndex:7, newIndex:1, newValue: -1
-800, -1, 0, 8, 10, 45, 100, 4857,

降序似乎被实现为反向。不知道测试是如何得出正确的输出的。 - Thom Hubers
2
好的解决方案,在实时中运行良好!少数评论:降序被反转了(需要修复);对于不想安装 NuGet 用于元组的人,只需将“(Item: item, Index: index));”替换为“new {Index = index, Item = item});”,将“(OldIndex: tuple.Index, NewIndex: index))”替换为“new {OldIndex = tuple.Index,NewIndex = index})”。 - uzrgm
1
不错。如果集合始终保持排序,我们也可以跳过“删除”通知吗? - Simon Mourier
1
非常好的解决方案,谢谢! - Roman
1
这是一个真正的天才解决方案,我很喜欢它! - Weissu

14

我创建了一个对ObservableCollection的扩展方法

public static void MySort<TSource,TKey>(this ObservableCollection<TSource> observableCollection, Func<TSource, TKey> keySelector)
    {
        var a = observableCollection.OrderBy(keySelector).ToList();
        observableCollection.Clear();
        foreach(var b in a)
        {
            observableCollection.Add(b);
        }
    }

它似乎是有效的,而且您不需要实现IComparable


2
虽然这似乎比移动所有项目要快得多,但它也会触发不同的事件(删除和添加而不是移动)。这可能不是问题,具体取决于用例,但绝对是需要记在心里的事情。 - Tim Pohlmann

4
myObservableCollection.ToList().Sort((x, y) => x.Property.CompareTo(y.Property));

很抱歉告诉你,这并不会对ObservableCollection产生任何改变。 - undefined

4
OrderByDescending的参数是一个返回用于排序的键的函数。在您的情况下,该键是字符串本身。
var result = _animals.OrderByDescending(a => a);

如果您想按长度排序,例如,您需要编写以下代码:
var result = _animals.OrderByDescending(a => a.Length);

4
请注意,在调用OrderByDescending()方法后,集合中项目的顺序不会改变 - Sergey Vyacheslavovich Brunov
2
由于@Sergey Brunov所解释的原因,它无法工作。您需要将结果保存在另一个变量中。 - manji

3
_animals.OrderByDescending(a => a.<what_is_here_?>);

如果动物是一个动物对象列表,您可以使用属性对列表进行排序。
public class Animal
{
    public int ID {get; set;}
    public string Name {get; set;}
    ...
}

ObservableCollection<Animal> animals = ...
animals = animals.OrderByDescending(a => a.Name);

5
请注意,在调用OrderByDescending()方法后,集合中的项目顺序不会被修改 - Sergey Vyacheslavovich Brunov
@SergeyBrunov:没错,你应该这样说:animals = animals.OrderByDescending(a => a.Name); - Bram Van Strydonck
1
我遇到了“无法将ObservableCollection转换为OrderedObservableCollection”的错误。 - poudigne
1
这不起作用。无法将有序集合分配到ObservableCollection<T>,因为排序后的类型将为IOrderedEnumerable<T>,您将收到错误信息。 - M. Pipal
2
animals = new ObservableCollection<Animal>(animals.OrderByDescending(a => a.Name));这样可以避免IOrderedEnumerable错误。 - Travis
@Travis 不错-它确实可以消除错误,但在WPF中,你必须绑定到ObservableCollection,我尝试了这个方法,但它停止识别我的集合-它停止允许我向集合中添加东西并在GUI中反映出更改。绑定断开了。更好的方法可能是将排序后的集合分配给另一个集合,然后将其作为ObservableCollection<Animal>而不是IOrderedEnumerable<T>添加回到animals中,因为它需要进行转换。不过我选择了Marco的解决方案。 - vapcguy

1
/// <summary>
/// Sorts the collection.
/// </summary>
/// <typeparam name="T">The type of the elements of the collection.</typeparam>
/// <param name="collection">The collection to sort.</param>
/// <param name="comparison">The comparison used for sorting.</param>
public static void Sort<T>(this ObservableCollection<T> collection, Comparison<T> comparison = null)
{
    var sortableList = new List<T>(collection);
    if (comparison == null)
        sortableList.Sort();
    else
        sortableList.Sort(comparison);

    for (var i = 0; i < sortableList.Count; i++)
    {
        var oldIndex = collection.IndexOf(sortableList[i]);
        var newIndex = i;
        if (oldIndex != newIndex)
            collection.Move(oldIndex, newIndex);
    }
}

这个解决方案基于Marco的回答。我在他的解决方案中遇到了一些问题,因此通过仅在索引实际改变时调用Move来进行了改进。这应该提高性能并解决相关问题。

1
对于任何合理的数据集来说,速度太慢了。 - Quark Soup
@DonaldAirey 公平地说,你找到更好的解决方案了吗? - Tim Pohlmann
我有一个相当复杂的MVVM交易簿,其中包含500行。该簿使用虚拟化技术,大量绑定到底层视图模型。唯一不同之处是一个能够即时排序,而另一个有明显的滞后。 - Quark Soup
@Quarkly 尽管这种方法较慢,但在我看来更正确。当您执行类似John答案的排序操作时,它会引发Reset事件,然后是一堆Adds。而这会引发Move事件... - nawfal
1
@TimPohlmann List<T>.Sort是不稳定的。请参阅https://en.wikipedia.org/wiki/Category:Stable_sorts获取更多信息。 - nawfal
显示剩余4条评论

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