How do I sort an observable collection?
Asked Answered
P

22

110

I have a following class :

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

Pastiness answered 22/12, 2009 at 10:21 Comment(5)
Are you looking for a sorting implementation within the class or just any type of sorting will do?Butcher
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)Pastiness
See this for a WPF solution #1945961Hyrup
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.Lactary
Possible duplicate of Sort ObservableCollection<string> through C#Huda
P
24

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;
    }
}
Parliament answered 22/12, 2009 at 11:7 Comment(11)
Found this and founc it most helpfull. Is it LINQ that makes up the sortedOC var?Weinrich
Not a fan of this answer because it doesn't give you a sorted ObservableCollection.Organic
-1 since it doesn't sort the ObservableCollection, but instead creates a new collection.Beset
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.Domineca
Last downvote Feb 2015 odd - care to expand further? I addressed 'doesn't sort the ObservableCollection' ages ago ......Parliament
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/…Tavia
This is horribly slow. Also, why compute sorted.IndexOf(t) when you are iterating the very same collection? You could just have another counter to hold the current index (a for loop would make this simpler).Cazares
@nawfal, you still need to search sorted cached items to find the correct sorted position if the item doesn't match the current position in the observable. We are only swapping the positions of the items of the observable to minimise change notifications to the ui. This solution was 5 years old and not fully optimised as a better partioning solution probably exists, further BinarySearch is faster instead of IndexOf as @W Morrison notes and @H Cordes suggests MoveItem. Feel free to add a performant Answer yourself. How large is the observable you are trying to sort?Parliament
@Parliament See this: dotnetfiddle.net/Y5MH2D. It raises change notification only 5 times (x2 if you count both remove and add) where it should at least raise 7 times. Or consider this case with duplicates: dotnetfiddle.net/1TeYcO. Good luck to finish running it :)Cazares
@nawfal, yup the perf. was awful I have updated my code. Your method there doesn't sort the collection with duplicates either (at least it runs unlike my original though!) if you inspect the result. I have added duplicate handling code to my answer.Parliament
@Parliament excellent. I overlooked the duplicates part!. Thank you :)Cazares
R
98

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();
Rhombus answered 2/5, 2013 at 18:13 Comment(8)
This appears to be the only answer that actually sorts the original list, and does so without remove/add items.Prunelle
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 collectionParliament
Great answer. Any reason why you use return Utils.LogicalStringCompare(a.Title, b.Title); instead of return string.Compare(a.Title, b.Title);? @NeilWDiley
@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.)Rhombus
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.Confucianism
+1 for keeping it simple, modifying the original collection and still firing the collectionchanged event!Neuroticism
KISS compatible, but had to add T to " : IComparable<T>" in the method headBrio
@Rhombus this wouldnt work when you have duplicate entries. Andrew's solution is better.Cazares
A
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://mcmap.net/q/196340/-inotifycollectionchanged-added-item-does-not-appear-at-given-index-39-0-39).

Afterclap answered 23/3, 2011 at 6:12 Comment(10)
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.Roofing
@Steve You can try this one.Wednesday
Thanks for the link, I went with the extension method as this seemed the tidiest solution. Works a charm :DHolley
bw did anyone notice that the blog has a typo in the html file name (obversablecollection)? :PTopsyturvy
@romkyns FYI, the ObservableSortedList doesn't seem to play nice with a Windows Store / Windows 8 GridView's ItemsSource. The GridView doesn't attach to the ObservableSortedList's CollectionChanged event handler, so even though it tries to fire the event that an item was moved, nobody is listening, so it's not reflected in the view.Charinile
@Charinile Curious... if you find how to fix it, please do send the details my way (or fork & send pull request).Wednesday
@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.Charinile
@romkyns see my answer to this question. I posted full source. It's also available on NuGet :)Charinile
To fully implement a generic sortable observable collection is a lot of work - CollectionViews used together with ObservableCollections may be better as they do different things the former normally is where sorting happensParliament
I can confirm that the ObservableSortedList contains a memory leak. The event handlers aren't properly cleaned up.Jarlen
H
33

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);
Heartrending answered 4/5, 2011 at 21:35 Comment(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 moveInefficacy
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.Heartrending
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.Inefficacy
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.Cazares
H
26

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.

Hyrup answered 4/6, 2016 at 19:36 Comment(3)
that actually worked :D that's a way better solution than the other "overengineered" solution for such a simple task.Obliging
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.Untie
Looked for the same thing but for Winui 3. CommunityToolKit.Winui.Ui Package has a simmilar concept with the AdvancedCollectionView.Denudate
P
24

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;
    }
}
Parliament answered 22/12, 2009 at 11:7 Comment(11)
Found this and founc it most helpfull. Is it LINQ that makes up the sortedOC var?Weinrich
Not a fan of this answer because it doesn't give you a sorted ObservableCollection.Organic
-1 since it doesn't sort the ObservableCollection, but instead creates a new collection.Beset
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.Domineca
Last downvote Feb 2015 odd - care to expand further? I addressed 'doesn't sort the ObservableCollection' ages ago ......Parliament
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/…Tavia
This is horribly slow. Also, why compute sorted.IndexOf(t) when you are iterating the very same collection? You could just have another counter to hold the current index (a for loop would make this simpler).Cazares
@nawfal, you still need to search sorted cached items to find the correct sorted position if the item doesn't match the current position in the observable. We are only swapping the positions of the items of the observable to minimise change notifications to the ui. This solution was 5 years old and not fully optimised as a better partioning solution probably exists, further BinarySearch is faster instead of IndexOf as @W Morrison notes and @H Cordes suggests MoveItem. Feel free to add a performant Answer yourself. How large is the observable you are trying to sort?Parliament
@Parliament See this: dotnetfiddle.net/Y5MH2D. It raises change notification only 5 times (x2 if you count both remove and add) where it should at least raise 7 times. Or consider this case with duplicates: dotnetfiddle.net/1TeYcO. Good luck to finish running it :)Cazares
@nawfal, yup the perf. was awful I have updated my code. Your method there doesn't sort the collection with duplicates either (at least it runs unlike my original though!) if you inspect the result. I have added duplicate handling code to my answer.Parliament
@Parliament excellent. I overlooked the duplicates part!. Thank you :)Cazares
O
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);
Organic answered 11/6, 2012 at 18:2 Comment(4)
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);Inefficacy
This sort algorithm is not optimized en.wikipedia.org/wiki/Sorting_algorithmHeartrending
@Heartrending Yes, it is optimized, just not for overall raw speed.Tchao
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.Cazares
C
14

@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);
Confucianism answered 27/7, 2015 at 16:56 Comment(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)Viminal
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.Lathan
G
11

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);
Gringo answered 21/3, 2015 at 9:2 Comment(0)
E
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();
Exclave answered 17/1, 2013 at 2:26 Comment(3)
This is my preferred sorting way from all described here, unfortunately the Move method is not available in Silverlight 5.Udine
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>Transilluminate
@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.Exclave
A
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);
            }
        }
    }
Auk answered 22/9, 2012 at 0:37 Comment(0)
R
4

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)))));
Rhombus answered 7/9, 2016 at 16:7 Comment(2)
wouldn't it be better to update the original answer?Foreshow
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.Rhombus
R
3

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.

Raggletaggle answered 22/12, 2009 at 11:10 Comment(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.Balbriggan
M
2

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);
Malm answered 5/10, 2015 at 13:3 Comment(0)
C
1

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

Clouse answered 22/12, 2009 at 10:24 Comment(0)
E
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));
Eoin answered 22/12, 2009 at 11:13 Comment(0)
H
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
    }
}
Hargrave answered 26/3, 2013 at 15:39 Comment(0)
K
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.

Kong answered 12/10, 2013 at 20:48 Comment(0)
C
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.

Charinile answered 30/1, 2014 at 17:58 Comment(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.Yanina
P
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);
        }
    }
Protector answered 12/12, 2015 at 18:8 Comment(0)
L
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));
Lomond answered 1/11, 2016 at 14:52 Comment(0)
V
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++;
        }
}
Vascular answered 15/5, 2020 at 8:42 Comment(0)
V
-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));
Vevina answered 18/3, 2013 at 20:3 Comment(2)
Oh I get it...Gayot wanted to give the bounty to the most downvoted answer lolRhombus
Never seen awarding bounty in sarcasm :)Cazares

© 2022 - 2024 — McMap. All rights reserved.