ฉันจะจัดเรียงคอลเล็กชันที่สังเกตได้อย่างไร


97

ฉันมีชั้นเรียนต่อไปนี้:

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

Which I've put in an ObservableCollection :

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"));

Q : How do I sort it by key ?


Are you looking for a sorting implementation within the class or just any type of sorting will do?
o.k.w

Not sure how to understand that. Basically I just want to have it sorted, the collection isn't going to be very big (20 items max) so anything will do (most likely)
Maciek

See this for a WPF solution stackoverflow.com/questions/1945461/…
Gayot Fow

Look at the answers on this page: very clear indication of a broken API when it takes 22+ answers for some critical and basic functionality.
Gerry

คำตอบ:


21

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

Found this and founc it most helpfull. Is it LINQ that makes up the sortedOC var?
Jason94

9
Not a fan of this answer because it doesn't give you a sorted ObservableCollection.
xr280xr

63
-1 since it doesn't sort the ObservableCollection, but instead creates a new collection.
Kos

2
The updated code will work, but has O(n^2) time complexity. This can be improved to O(n*log(n)) by using BinarySearch instead of IndexOf.
William Morrison

2
Excellent solution! For the ones inheriting from ObservableCollection<T>, it's possible to use the protected MoveItem() method instead of using the RemoveAt and Insert methods. See also: referencesource.microsoft.com/#system/compmod/system/…
Herman Cordes

85

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();

7
this should be the answer
thumbmunkeys

1
Updated my answer above as it was the accepted one and addresses performance improvement over this answer which raises change notifications for everything in the collection
Andrew

3
Great answer. Any reason why you use return Utils.LogicalStringCompare(a.Title, b.Title); instead of return string.Compare(a.Title, b.Title);? @NeilW
Joe

2
@Joe, I needed to do a logical compare instead of a standard string compare, which is why I needed to write the extension in the first place. Logical string compare sorts numbers in strings properly, rather than ordering them like strings (1, 2, 20, 1000 instead of 1, 1000, 2, 20, etc.)
NielW

4
this is absolutely the way to go. I added an answer of my own extending this, allowing you to pass in a keySelector instead of using IComparable, like LINQ typically does.
Jonesopolis

39

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


2
Indeed, this blog is more useful. However, I am yet to find a decent answer to the question of having an observable collection that maintains its sorting as items are added and removed. I am going to write my own I think.
Stephen Drew

@Steve You can try this one.
Roman Starkov

Thanks for the link, I went with the extension method as this seemed the tidiest solution. Works a charm :D
pengibot

bw did anyone notice that the blog has a typo in the html file name (obversablecollection)? :P
laishiekai

1
@romkyns the answer was to extend ObservableCollection<T>. The GridView recognizes it just fine then. Then just hide it's methods, much like you do. I'll post a full solution when I have time.
Weston

25

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

More detail: http://jaider.net/2011-05-04/sort-a-observablecollection/


4
This clears the ObservableCollection then re-adds all the objects - so it's worth noting that if your UI is bound to the collection, you would not see animated changes, e.g. when items move
Carlos P

1
I don't sure why you have to display items moving around... e.g. you normally have ObservableCollection bound to ItemSource of the dropdowns and you don't see the collection at all. Also this operation of clear-out and fill-up is ultra fast... the "slow" one can be the sort that is already optimized. finally, you can modify this code to implement your move method, having the sortedlist and source the rest is easy.
Jaider

3
If you are bound to a drop-down then you wouldn't benefit from seeing items moving around, that's true. If you're bound to a ListBox though, then frameworks such as WPF or Silverlight or Windows Store Apps will provide useful visual feedback as objects in the collection are re-indexed.
Carlos P

While this is faster than the Move approach, this raises a number of Reset/Add events. The highest voted answer (Move approach) minimizes this and rightly raises Move events, that too only for the really moved ones.
nawfal

20

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.


6
that actually worked :D that's a way better solution than the other "overengineered" solution for such a simple task.
MushyPeas

Where did your blog go?
phoog

The problem with "transparent" things is that you can't see where to look when it does not work. Microsoft's documentation has a 100% transparent example, i.e. you can't see it at all.
Paul McCarthy

15

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

1
Thanks for posting this - as pointed out in the comments on Richie's blog, there are some worthwhile enhancements to this code; in particular using the 'Move' method of the source. I guess this would replace the Remove/Insert lines with source.Move(j-1, j);
Carlos P

2
This sort algorithm is not optimized en.wikipedia.org/wiki/Sorting_algorithm
Jaider

@Jaider Yes, it is optimized, just not for overall raw speed.
jv42

This raises a number of Remove/Add events (for every N i believe)..The highest voted answer minimizes this and rightly raises Move events, that too only for the really moved ones. The key here is to not do an in-place sort right away, instead sort it externally using OrderBy and then do a comparison to figure out actual change.
nawfal

11

@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);

2
For an extra chocolate cookie you can add a boolean parameter "Ascending" and a if(!Ascending) sorted.Reverse(); just before the for :D (and no need to -further- worry about memory, that Reverse method does not create any new objects, it's in-place reverse)
Sharky

According to my tests collection.Move(0,0) leads to an CollectionChanged event. Therefore it'd be an performance improvment to first check if a move is even required.
sa.he

10

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

8

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();

1
This is my preferred sorting way from all described here, unfortunately the Move method is not available in Silverlight 5.
Eduardo Brites

1
Im getting error 'Profiler.Profile.ProfileObject' cannot be used as type parameter 'T' in the generic type or method 'ObservableCollectionExtensions.Sort<T>(ObservableCollection<T>)'. There is no implicit reference conversion from 'Profiler.Profile.ProfileObject' to 'System.IComparable<Profiler.Profile.ProfileObject>
New Bee

1
@NewBee: This extension method specifies a generic constraint on T to be able to sort the elements in the collection. Sorting involves the concept of greater and less than and only you can define how ProfileObject is ordered. To use the extension method you need to implement IComparable<ProfileObject> on ProfileObject. Other alternatives are as noted specifying an IComparer<ProfileObject> or a Func<ProfileObject, ProfileObject, int> and change the sorting code accordingly.
Martin Liversage

4

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

2

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.


1
The link in this answer is to LGPL licensed code, so if you're Silverlight (can't dynamically link) or not open source be careful with that code.
yzorg

2

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

wouldn't it be better to update the original answer?
Nathan Hughes

No. It's already been upvoted more than any other answer. I'm not going to assume people would rather do it this way. Just thought I'd offer up another way of doing it, especially since there was a bounty on new answers.
NielW


1

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


1

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

1

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.


1

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.


1
You probably should not be using new on all those methods, if someone has a more generically typed instance those methods will not be called. Instead override every overridable method and change them as necessary or fallback on base.Method(...). You, for example, don't even need to worry about .Add because that internally uses .InsertItem, so if .InsertItem is overridden and adjusted, .Add will not mess with ordering.
H.B.

1

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

1

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

0

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

0

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

-3
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));

Oh I get it...Gayot wanted to give the bounty to the most downvoted answer lol
NielW

Never seen awarding bounty in sarcasm :)
nawfal
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.