如何对可观察到的集合进行排序?

我有以下课程:

[DataContract]
public class Pair<TKey, TValue> : INotifyPropertyChanged, IDisposable
{
public Pair(TKey key, TValue value)
{
Key = key;
Value = value;
}


#region Properties
[DataMember]
public TKey Key
{
get
{ return m_key; }
set
{
m_key = value;
OnPropertyChanged("Key");
}
}
[DataMember]
public TValue Value
{
get { return m_value; }
set
{
m_value = value;
OnPropertyChanged("Value");
}
}
#endregion


#region Fields
private TKey m_key;
private TValue m_value;
#endregion


#region INotifyPropertyChanged Members


public event PropertyChangedEventHandler PropertyChanged;


protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}


#endregion


#region IDisposable Members


public void Dispose()
{ }


#endregion
}

我把它放进了一个可观察的收藏:

ObservableCollection<Pair<ushort, string>> my_collection =
new ObservableCollection<Pair<ushort, string>>();


my_collection.Add(new Pair(7, "aaa"));
my_collection.Add(new Pair(3, "xey"));
my_collection.Add(new Pair(6, "fty"));

问: 如何按键排序?

155420 次浏览

Make a new class SortedObservableCollection, derive it from ObservableCollection and implement IComparable<Pair<ushort, string>>.

Sorting an observable and returning the same object sorted can be done using an extension method. For larger collections watch out for the number of collection changed notifications.

I have updated my code to improve performance (thanks to nawfal) and to handle duplicates which no other answers here do at time of writing. The observable is partitioned into a left sorted half and a right unsorted half, where each time the minimum item (as found in the sorted list) is shifted to the end of the sorted partition from the unsorted. Worst case O(n). Essentially a selection sort (See below for output).

public static void Sort<T>(this ObservableCollection<T> collection)
where T : IComparable<T>, IEquatable<T>
{
List<T> sorted = collection.OrderBy(x => x).ToList();


int ptr = 0;
while (ptr < sorted.Count - 1)
{
if (!collection[ptr].Equals(sorted[ptr]))
{
int idx = search(collection, ptr+1, sorted[ptr]);
collection.Move(idx, ptr);
}
            

ptr++;
}
}


public static int search<T>(ObservableCollection<T> collection, int startIndex, T other)
{
for (int i = startIndex; i < collection.Count; i++)
{
if (other.Equals(collection[i]))
return i;
}
    

return -1; // decide how to handle error case
}

usage: Sample with an observer (used a Person class to keep it simple)

    public class Person:IComparable<Person>,IEquatable<Person>
{
public string Name { get; set; }
public int Age { get; set; }
    

public int CompareTo(Person other)
{
if (this.Age == other.Age) return 0;
return this.Age.CompareTo(other.Age);
}
    

public override string ToString()
{
return Name + " aged " + Age;
}
    

public bool Equals(Person other)
{
if (this.Name.Equals(other.Name) && this.Age.Equals(other.Age)) return true;
return false;
}
}
    

static void Main(string[] args)
{
Console.WriteLine("adding items...");
var observable = new ObservableCollection<Person>()
{
new Person {Name = "Katy", Age = 51},
new Person {Name = "Jack", Age = 12},
new Person {Name = "Bob", Age = 13},
new Person {Name = "Alice", Age = 39},
new Person {Name = "John", Age = 14},
new Person {Name = "Mary", Age = 41},
new Person {Name = "Jane", Age = 20},
new Person {Name = "Jim", Age = 39},
new Person {Name = "Sue", Age = 5},
new Person {Name = "Kim", Age = 19}
};
    

//what do observers see?
            

    

observable.CollectionChanged += (sender, e) =>
{
Console.WriteLine(
e.OldItems[0] + " move from " + e.OldStartingIndex + " to " + e.NewStartingIndex);
int i = 0;
foreach (var person in sender as ObservableCollection<Person>)
{
if (i == e.NewStartingIndex)
{
Console.Write("(" + (person as Person).Age + "),");
}
else
{
Console.Write((person as Person).Age + ",");
}
                

i++;
}


Console.WriteLine();
};

Details of sorting progress showing how the collection is pivoted:

Sue aged 5 move from 8 to 0
(5),51,12,13,39,14,41,20,39,19,
Jack aged 12 move from 2 to 1
5,(12),51,13,39,14,41,20,39,19,
Bob aged 13 move from 3 to 2
5,12,(13),51,39,14,41,20,39,19,
John aged 14 move from 5 to 3
5,12,13,(14),51,39,41,20,39,19,
Kim aged 19 move from 9 to 4
5,12,13,14,(19),51,39,41,20,39,
Jane aged 20 move from 8 to 5
5,12,13,14,19,(20),51,39,41,39,
Alice aged 39 move from 7 to 6
5,12,13,14,19,20,(39),51,41,39,
Jim aged 39 move from 9 to 7
5,12,13,14,19,20,39,(39),51,41,
Mary aged 41 move from 9 to 8
5,12,13,14,19,20,39,39,(41),51,

The Person class implements both IComparable and IEquatable the latter is used to minimise the changes to the collection so as to reduce the number of change notifications raised

  • EDIT Sorts same collection without creating a new copy *

To return an ObservableCollection, call .ToObservableCollection on *sortedOC* using e.g. [this implementation][1].

**** orig answer - this creates a new collection **** You can use linq as the doSort method below illustrates. A quick code snippet: produces

3:xey 6:fty 7:aaa

Alternatively you could use an extension method on the collection itself

var sortedOC = _collection.OrderBy(i => i.Key);


private void doSort()
{
ObservableCollection<Pair<ushort, string>> _collection =
new ObservableCollection<Pair<ushort, string>>();


_collection.Add(new Pair<ushort,string>(7,"aaa"));
_collection.Add(new Pair<ushort, string>(3, "xey"));
_collection.Add(new Pair<ushort, string>(6, "fty"));


var sortedOC = from item in _collection
orderby item.Key
select item;


foreach (var i in sortedOC)
{
Debug.WriteLine(i);
}


}


public class Pair<TKey, TValue>
{
private TKey _key;


public TKey Key
{
get { return _key; }
set { _key = value; }
}
private TValue _value;


public TValue Value
{
get { return _value; }
set { _value = value; }
}
    

public Pair(TKey key, TValue value)
{
_key = key;
_value = value;


}


public override string ToString()
{
return this.Key + ":" + this.Value;
}
}

Do you need to keep your collection sorted at all times? When retrieving the pairs, do you need them to be always sorted, or it's only for a few times (maybe just for presenting)? How big do you expect your collection to be? There are a lot of factors that can help you decide witch method to use.

If you need the collection to be sorted at all times, even when you insert or delete elements and insertion speed is not a problem maybe you should implement some kind of SortedObservableCollection like @Gerrie Schenck mentioned or check out this implementation.

If you need your collection sorted just for a few times use:

my_collection.OrderBy(p => p.Key);

This will take some time to sort the collection, but even so, it might be the best solution depending on what your doing with it.

One way would be to convert it to a List and then call Sort(), providing a comparison delegate. Something like:-

(untested)

my_collection.ToList().Sort((left, right) => left == right ? 0 : (left > right ? -1 : 1));

I found a relevant blog entry that provides a better answer than the ones here:

http://kiwigis.blogspot.com/2010/03/how-to-sort-obversablecollection.html

UPDATE

The ObservableSortedList that @romkyns points out in the comments automatically maintains sort order.

Implements an observable collection which maintains its items in sorted order. In particular, changes to item properties that result in order changes are handled correctly.

However note also the remark

May be buggy due to the comparative complexity of the interface involved and its relatively poor documentation (see https://stackoverflow.com/a/5883947/33080).

You can use this simple method:

public static void Sort<TSource, TKey>(this Collection<TSource> source, Func<TSource, TKey> keySelector)
{
List<TSource> sortedList = source.OrderBy(keySelector).ToList();
source.Clear();
foreach (var sortedItem in sortedList)
source.Add(sortedItem);
}

You can sort like this:

_collection.Sort(i => i.Key);

I liked the bubble sort extension method approach on "Richie"'s blog above, but I don't necessarily want to just sort comparing the entire object. I more often want to sort on a specific property of the object. So I modified it to accept a key selector the way OrderBy does so you can choose which property to sort on:

    public static void Sort<TSource, TKey>(this ObservableCollection<TSource> source, Func<TSource, TKey> keySelector)
{
if (source == null) return;


Comparer<TKey> comparer = Comparer<TKey>.Default;


for (int i = source.Count - 1; i >= 0; i--)
{
for (int j = 1; j <= i; j++)
{
TSource o1 = source[j - 1];
TSource o2 = source[j];
if (comparer.Compare(keySelector(o1), keySelector(o2)) > 0)
{
source.Remove(o1);
source.Insert(j, o1);
}
}
}
}

Which you would call the same way you would call OrderBy except it will sort the existing instance of your ObservableCollection instead of returning a new collection:

ObservableCollection<Person> people = new ObservableCollection<Person>();
...


people.Sort(p => p.FirstName);

To improve a little bit the extension method on xr280xr answer I added an optional bool parameter to determine whether the sorting is descending or not. I also included the suggestion made by Carlos P in the comment to that answer. Please see below.

public static void Sort<TSource, TKey>(this ObservableCollection<TSource> source, Func<TSource, TKey> keySelector, bool desc = false)
{
if (source == null) return;


Comparer<TKey> comparer = Comparer<TKey>.Default;


for (int i = source.Count - 1; i >= 0; i--)
{
for (int j = 1; j <= i; j++)
{
TSource o1 = source[j - 1];
TSource o2 = source[j];
int comparison = comparer.Compare(keySelector(o1), keySelector(o2));
if (desc && comparison < 0)
source.Move(j, j - 1);
else if (!desc && comparison > 0)
source.Move(j - 1, j);
}
}
}

A variation is where you sort the collection in place using a selection sort algorithm. Elements are moved into place using the Move method. Each move will fire the CollectionChanged event with NotifyCollectionChangedAction.Move (and also PropertyChanged with property name Item[]).

This algorithm has some nice properties:

  • The algorithm can be implemented as a stable sort.
  • The number of items moved in the collection (e.g. CollectionChanged events fired) is almost always less than other similar algorithms like insertion sort and bubble sort.

The algorithm is quite simple. The collection is iterated to find the smallest element which is then moved to the start of the collection. The process is repeated starting at the second element and so on until all elements have been moved into place. The algorithm is not terribly efficient but for anything you are going to display in a user interface it shouldn't matter. However, in terms of the number of move operations it is quite efficient.

Here is an extension method that for simplicity requires that the elements implement IComparable<T>. Other options are using an IComparer<T> or a Func<T, T, Int32>.

public static class ObservableCollectionExtensions {


public static void Sort<T>(this ObservableCollection<T> collection) where T : IComparable<T> {
if (collection == null)
throw new ArgumentNullException("collection");


for (var startIndex = 0; startIndex < collection.Count - 1; startIndex += 1) {
var indexOfSmallestItem = startIndex;
for (var i = startIndex + 1; i < collection.Count; i += 1)
if (collection[i].CompareTo(collection[indexOfSmallestItem]) < 0)
indexOfSmallestItem = i;
if (indexOfSmallestItem != startIndex)
collection.Move(indexOfSmallestItem, startIndex);
}
}


}

Sorting a collection is simply a matter of invoking the extension method:

var collection = new ObservableCollection<String>(...);
collection.Sort();
var collection = new ObservableCollection<int>();


collection.Add(7);
collection.Add(4);
collection.Add(12);
collection.Add(1);
collection.Add(20);


// ascending
collection = new ObservableCollection<int>(collection.OrderBy(a => a));


// descending
collection = new ObservableCollection<int>(collection.OrderByDescending(a => a));

What the heck, I'll throw in a quickly-cobbled-together answer as well...it looks a bit like some other implementations here, but I'll add it anywho:

(barely tested, hopefully I'm not embarassing myself)

Let's state some goals first (my assumptions):

1) Must sort ObservableCollection<T> in place, to maintain notifications, etc.

2) Must not be horribly inefficient (i.e., something close to standard "good" sorting efficiency)

public static class Ext
{
public static void Sort<T>(this ObservableCollection<T> src)
where T : IComparable<T>
{
// Some preliminary safety checks
if(src == null) throw new ArgumentNullException("src");
if(!src.Any()) return;


// N for the select,
// + ~ N log N, assuming "smart" sort implementation on the OrderBy
// Total: N log N + N (est)
var indexedPairs = src
.Select((item,i) => Tuple.Create(i, item))
.OrderBy(tup => tup.Item2);
// N for another select
var postIndexedPairs = indexedPairs
.Select((item,i) => Tuple.Create(i, item.Item1, item.Item2));
// N for a loop over every element
var pairEnum = postIndexedPairs.GetEnumerator();
pairEnum.MoveNext();
for(int idx = 0; idx < src.Count; idx++, pairEnum.MoveNext())
{
src.RemoveAt(pairEnum.Current.Item1);
src.Insert(idx, pairEnum.Current.Item3);
}
// (very roughly) Estimated Complexity:
// N log N + N + N + N
// == N log N + 3N
}
}

This simple extension worked beautifully for me. I just had to make sure that MyObject was IComparable. When the sort method is called on the observable collection of MyObjects, the CompareTo method on MyObject is called, which calls my Logical Sort method. While it doesn't have all the bells and whistles of the rest of the answers posted here, it's exactly what I needed.

static class Extensions
{
public static void Sort<T>(this ObservableCollection<T> collection) where T : IComparable
{
List<T> sorted = collection.OrderBy(x => x).ToList();
for (int i = 0; i < sorted.Count(); i++)
collection.Move(collection.IndexOf(sorted[i]), i);
}
}


public class MyObject: IComparable
{
public int CompareTo(object o)
{
MyObject a = this;
MyObject b = (MyObject)o;
return Utils.LogicalStringCompare(a.Title, b.Title);
}


public string Title;


}
.
.
.
myCollection = new ObservableCollection<MyObject>();
//add stuff to collection
myCollection.Sort();

None of these answers worked in my case. Either because it screws up binding, or requires so much additional coding that it's kind of a nightmare, or the answer is just broken. So, here's yet another simpler answer i thought. It's a lot less code and it remains the same observable collection with an additional this.sort type of method. Let me know if there's some reason I shouldn't be doing it this way (efficiency etc.)?

public class ScoutItems : ObservableCollection<ScoutItem>
{
public void Sort(SortDirection _sDir, string _sItem)
{
//TODO: Add logic to look at _sItem and decide what property to sort on
IEnumerable<ScoutItem> si_enum = this.AsEnumerable();


if (_sDir == SortDirection.Ascending)
{
si_enum = si_enum.OrderBy(p => p.UPC).AsEnumerable();
} else
{
si_enum = si_enum.OrderByDescending(p => p.UPC).AsEnumerable();
}


foreach (ScoutItem si in si_enum)
{
int _OldIndex = this.IndexOf(si);
int _NewIndex = si_enum.ToList().IndexOf(si);
this.MoveItem(_OldIndex, _NewIndex);
}
}
}

...Where ScoutItem is my public class. Just seemed a lot simpler. Added benefit: it actually works and doesn't mess with bindings or return a new collection etc.

Alright, since I was having issues getting ObservableSortedList to work with XAML, I went ahead and created SortingObservableCollection. It inherits from ObservableCollection, so it works with XAML and I've unit tested it to 98% code coverage. I've used it in my own apps, but I won't promise that it is bug free. Feel free to contribute. Here is sample code usage:

var collection = new SortingObservableCollection<MyViewModel, int>(Comparer<int>.Default, model => model.IntPropertyToSortOn);


collection.Add(new MyViewModel(3));
collection.Add(new MyViewModel(1));
collection.Add(new MyViewModel(2));
// At this point, the order is 1, 2, 3
collection[0].IntPropertyToSortOn = 4; // As long as IntPropertyToSortOn uses INotifyPropertyChanged, this will cause the collection to resort correctly

It's a PCL, so it should work with Windows Store, Windows Phone, and .NET 4.5.1.

I would like to Add to NeilW's answer. To incorporate a method that resembles the orderby. Add this method as an extension:

public static void Sort<T>(this ObservableCollection<T> collection, Func<T,T> keySelector) where T : IComparable
{
List<T> sorted = collection.OrderBy(keySelector).ToList();
for (int i = 0; i < sorted.Count(); i++)
collection.Move(collection.IndexOf(sorted[i]), i);
}

And use like:

myCollection = new ObservableCollection<MyObject>();


//Sorts in place, on a specific Func<T,T>
myCollection.Sort(x => x.ID);

@NielW's answer is the way to go, for real in-place sorting. I wanted to add an slightly altered solution that allows you bypass having to use IComparable:

static class Extensions
{
public static void Sort<TSource, TKey>(this ObservableCollection<TSource> collection, Func<TSource, TKey> keySelector)
{
List<TSource> sorted = collection.OrderBy(keySelector).ToList();
for (int i = 0; i < sorted.Count(); i++)
collection.Move(collection.IndexOf(sorted[i]), i);
}
}

now you can call it like most any LINQ method:

myObservableCollection.Sort(o => o.MyProperty);

This worked for me, found it long time ago somewhere.

// SortableObservableCollection
public class SortableObservableCollection<T> : ObservableCollection<T>
{
public SortableObservableCollection(List<T> list)
: base(list)
{
}


public SortableObservableCollection()
{
}


public void Sort<TKey>(Func<T, TKey> keySelector, System.ComponentModel.ListSortDirection direction)
{
switch (direction)
{
case System.ComponentModel.ListSortDirection.Ascending:
{
ApplySort(Items.OrderBy(keySelector));
break;
}
case System.ComponentModel.ListSortDirection.Descending:
{
ApplySort(Items.OrderByDescending(keySelector));
break;
}
}
}


public void Sort<TKey>(Func<T, TKey> keySelector, IComparer<TKey> comparer)
{
ApplySort(Items.OrderBy(keySelector, comparer));
}


private void ApplySort(IEnumerable<T> sortedItems)
{
var sortedItemsList = sortedItems.ToList();


foreach (var item in sortedItemsList)
{
Move(IndexOf(item), sortedItemsList.IndexOf(item));
}
}
}

Usage:

MySortableCollection.Sort(x => x, System.ComponentModel.ListSortDirection.Ascending);

This is what I do with OC extensions:

    /// <summary>
/// Synches the collection items to the target collection items.
/// This does not observe sort order.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source">The items.</param>
/// <param name="updatedCollection">The updated collection.</param>
public static void SynchCollection<T>(this IList<T> source, IEnumerable<T> updatedCollection)
{
// Evaluate
if (updatedCollection == null) return;


// Make a list
var collectionArray = updatedCollection.ToArray();


// Remove items from FilteredViewItems not in list
source.RemoveRange(source.Except(collectionArray));


// Add items not in FilteredViewItems that are in list
source.AddRange(collectionArray.Except(source));
}


/// <summary>
/// Synches the collection items to the target collection items.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source">The source.</param>
/// <param name="updatedCollection">The updated collection.</param>
/// <param name="canSort">if set to <c>true</c> [can sort].</param>
public static void SynchCollection<T>(this ObservableCollection<T> source,
IList<T> updatedCollection, bool canSort = false)
{
// Synch collection
SynchCollection(source, updatedCollection.AsEnumerable());


// Sort collection
if (!canSort) return;


// Update indexes as needed
for (var i = 0; i < updatedCollection.Count; i++)
{
// Index of new location
var index = source.IndexOf(updatedCollection[i]);
if (index == i) continue;


// Move item to new index if it has changed.
source.Move(index, i);
}
}

WPF provides live sorting out-of-the-box using the ListCollectionView class...

public ObservableCollection<string> MyStrings { get; set; }
private ListCollectionView _listCollectionView;
private void InitializeCollection()
{
MyStrings = new ObservableCollection<string>();
_listCollectionView = CollectionViewSource.GetDefaultView(MyStrings)
as ListCollectionView;
if (_listCollectionView != null)
{
_listCollectionView.IsLiveSorting = true;
_listCollectionView.CustomSort = new
CaseInsensitiveComparer(CultureInfo.InvariantCulture);
}
}

Once this initialization is complete, there's nothing more to do. The advantage over a passive sort is that the ListCollectionView does all the heavy lifting in a way that's transparent to the developer. New items are automatically placed in their correct sort order. Any class that derives from IComparer of T is suitable for the custom sort property.

See ListCollectionView for the documentation and other features.

My current answer already has the most votes, but I found a better, and more modern, way of doing this.

class MyObject
{
public int id { get; set; }
public string title { get; set; }
}


ObservableCollection<MyObject> myCollection = new ObservableCollection<MyObject>();


//add stuff to collection
// .
// .
// .


myCollection = new ObservableCollection<MyObject>(
myCollection.OrderBy(n => n.title, Comparer<string>.Create(
(x, y) => (Utils.Utils.LogicalStringCompare(x, y)))));

I needed to be able to sort by multiple things not just one. This answer is based on some of the other answers but it allows for more complex sorting.

static class Extensions
{
public static void Sort<T, TKey>(this ObservableCollection<T> collection, Func<ObservableCollection<T>, TKey> sort)
{
var sorted = (sort.Invoke(collection) as IOrderedEnumerable<T>).ToArray();
for (int i = 0; i < sorted.Count(); i++)
collection.Move(collection.IndexOf(sorted[i]), i);
}
}

When you use it, pass in a series of OrderBy/ThenBy calls. Like this:

Children.Sort(col => col.OrderByDescending(xx => xx.ItemType == "drive")
.ThenByDescending(xx => xx.ItemType == "folder")
.ThenBy(xx => xx.Path));

I learned a lot from the other solutions, but I found a couple of problems. First, some depend on IndexOf which tends to be pretty slow for large lists. Second, my ObservableCollection had EF entities and using the Remove seemed to corrupt some of the foreign key properties. Maybe I'm doing something wrong.

Regardless, A Move can be used instead Remove/Insert, but that causes some issues with the performance fix.

To fix the performance problem, I create a dictionary with the IndexOf sorted values. To keep the dictionary up-to-date and to preserve the entity properties, use a swap implemented with two moves instead of one as implemented in other solutions.

A single move shifts the indexes of the elements between the locations, which would invalidate the IndexOf dictionary. Adding a second move to implement a swap restores locations.

public static void Sort<TSource, TKey>(this ObservableCollection<TSource> collection, Func<TSource, TKey> keySelector)
{
List<TSource> sorted = collection.OrderBy(keySelector).ToList();
Dictionary<TSource, int> indexOf = new Dictionary<TSource, int>();


for (int i = 0; i < sorted.Count; i++)
indexOf[sorted[i]] = i;


int idx = 0;
while (idx < sorted.Count)
if (!collection[idx].Equals(sorted[idx])) {
int newIdx = indexOf[collection[idx]]; // where should current item go?
collection.Move(newIdx, idx); // move whatever's there to current location
collection.Move(idx + 1, newIdx); // move current item to proper location
}
else {
idx++;
}
}