how to sort ObservableCollection

41,571

Solution 1

hummm first question I have for you is: is it really important that your ObservableCollection is sorted, or is what you really want is to have the display in GUI sorted?

I assume that the aim is to have a sorted display that will be updated "real time". Then I see 2 solutions

  1. get the ICollectionView of your ObservableCollection and sort it, as explained here http://marlongrech.wordpress.com/2008/11/22/icollectionview-explained/

  2. bind your ObservableCollection to a CollectionViewsource, add a sort on it, then use thatCollectionViewSource as the ItemSource of a ListView.

i.e:

add this namespace

xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"

then

<CollectionViewSource x:Key='src' Source="{Binding MyObservableCollection, ElementName=MainWindowName}">
    <CollectionViewSource.SortDescriptions>
        <scm:SortDescription PropertyName="MyField" />
    </CollectionViewSource.SortDescriptions>

</CollectionViewSource>

and bind like this

<ListView ItemsSource="{Binding Source={StaticResource src}}" >

Solution 2

I just created a class that extends the ObservableCollection because over time I've also wanted other functionality that I'm used to using from a List (Contains, IndexOf, AddRange, RemoveRange, etc)

I usually use it with something like

MyCollection.Sort(p => p.Name);

Here's my sort implementation

/// <summary>
/// Expanded ObservableCollection to include some List<T> Methods
/// </summary>
[Serializable]
public class ObservableCollectionEx<T> : ObservableCollection<T>
{

    /// <summary>
    /// Constructors
    /// </summary>
    public ObservableCollectionEx() : base() { }
    public ObservableCollectionEx(List<T> l) : base(l) { }
    public ObservableCollectionEx(IEnumerable<T> l) : base(l) { }

    #region Sorting

    /// <summary>
    /// Sorts the items of the collection in ascending order according to a key.
    /// </summary>
    /// <typeparam name="TKey">The type of the key returned by <paramref name="keySelector"/>.</typeparam>
    /// <param name="keySelector">A function to extract a key from an item.</param>
    public void Sort<TKey>(Func<T, TKey> keySelector)
    {
        InternalSort(Items.OrderBy(keySelector));
    }

    /// <summary>
    /// Sorts the items of the collection in descending order according to a key.
    /// </summary>
    /// <typeparam name="TKey">The type of the key returned by <paramref name="keySelector"/>.</typeparam>
    /// <param name="keySelector">A function to extract a key from an item.</param>
    public void SortDescending<TKey>(Func<T, TKey> keySelector)
    {
        InternalSort(Items.OrderByDescending(keySelector));
    }

    /// <summary>
    /// Sorts the items of the collection in ascending order according to a key.
    /// </summary>
    /// <typeparam name="TKey">The type of the key returned by <paramref name="keySelector"/>.</typeparam>
    /// <param name="keySelector">A function to extract a key from an item.</param>
    /// <param name="comparer">An <see cref="IComparer{T}"/> to compare keys.</param>
    public void Sort<TKey>(Func<T, TKey> keySelector, IComparer<TKey> comparer)
    {
        InternalSort(Items.OrderBy(keySelector, comparer));
    }

    /// <summary>
    /// Moves the items of the collection so that their orders are the same as those of the items provided.
    /// </summary>
    /// <param name="sortedItems">An <see cref="IEnumerable{T}"/> to provide item orders.</param>
    private void InternalSort(IEnumerable<T> sortedItems)
    {
        var sortedItemsList = sortedItems.ToList();

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

    #endregion // Sorting
}

Solution 3

The problem with sorting an ObservableCollection is that every time you change the collection, an event will get fired off. So for a sort that is removing items from one position and adding them to another, you will end up having a ton of events firing.

I think you're best bet is to just insert the stuff into the ObservableCollection in the proper order to begin with. Removing items from the collection won't effect ordering. I whipped up a quick extension method to illustrate

    public static void InsertSorted<T>(this ObservableCollection<T> collection, T item, Comparison<T> comparison)
    {
        if (collection.Count == 0)
            collection.Add(item);
        else
        {
            bool last = true;
            for (int i = 0; i < collection.Count; i++)
            {
                int result = comparison.Invoke(collection[i], item);
                if (result >= 1)
                {
                    collection.Insert(i, item);
                    last = false;
                    break;
                }
            }
            if (last)
                collection.Add(item);
        }
    }

So if you were to use strings (for instance), the code would look like this

        ObservableCollection<string> strs = new ObservableCollection<string>();
        Comparison<string> comparison = new Comparison<string>((s1, s2) => { return String.Compare(s1, s2); });
        strs.InsertSorted("Mark", comparison);
        strs.InsertSorted("Tim", comparison);
        strs.InsertSorted("Joe", comparison);
        strs.InsertSorted("Al", comparison);

Edit

You can keep the calls identical if you extend the ObservableCollection and supply your own insert/add methods. Something like this:

public class BarDataCollection : ObservableCollection<BarData>
{
    private Comparison<BarData> _comparison = new Comparison<BarData>((bd1, bd2) => { return DateTime.Compare(bd1.StartDate, bd2.StartDate); });

    public new void Insert(int index, BarData item)
    {
        InternalInsert(item);
    }

    protected override void InsertItem(int index, BarData item)
    {
        InternalInsert(item);
    }

    public new void Add(BarData item)
    {
        InternalInsert(item);
    }

    private void InternalInsert(BarData item)
    {
        if (Items.Count == 0)
            Items.Add(item);
        else
        {
            bool last = true;
            for (int i = 0; i < Items.Count; i++)
            {
                int result = _comparison.Invoke(Items[i], item);
                if (result >= 1)
                {
                    Items.Insert(i, item);
                    last = false;
                    break;
                }
            }
            if (last)
                Items.Add(item);
        }
    }
}

The insert index is ignored.

        BarData db1 = new BarData(DateTime.Now.AddDays(-1));
        BarData db2 = new BarData(DateTime.Now.AddDays(-2));
        BarData db3 = new BarData(DateTime.Now.AddDays(1));
        BarData db4 = new BarData(DateTime.Now);
        BarDataCollection bdc = new BarDataCollection();
        bdc.Add(db1);
        bdc.Insert(100, db2);
        bdc.Insert(1, db3);
        bdc.Add(db4);
Share:
41,571
Souvik Basu
Author by

Souvik Basu

Hobbyist programmer since Y2K bug (1999), coding professionally since 2003. Area of Interest: C#, WPF, Silverlight, Human Computer Interface and Usablity, Graphics, AI

Updated on March 16, 2021

Comments

  • Souvik Basu
    Souvik Basu over 3 years

    I have a an ObservableCollection and a WPF UserControl is Databound to it. The Control is a graph that shows a vertical bar for each item of type BarData in the ObservableCollection.

    ObservableCollection<BarData>
    
    class BarData
    {
       public DateTime StartDate {get; set;}
       public double MoneySpent {get; set;}
       public double TotalMoneySpentTillThisBar {get; set;}
    }
    

    Now I want to sort out the ObservableCollection based on StartDate so that the BarData's will be in increasing order of StartDate in the collection. Then I can calculate values of TotalMoneySpentTillThisBar in each BarData like this -

    var collection = new ObservableCollection<BarData>();
    //add few BarData objects to collection
    collection.Sort(bar => bar.StartData);    // this is ideally the kind of function I was looking for which does not exist 
    double total = 0.0;
    collection.ToList().ForEach(bar => {
                                         bar.TotalMoneySpentTillThisBar = total + bar.MoneySpent;
                                         total = bar.TotalMoneySpentTillThisBar; 
                                       }
                                );
    

    I know I can use ICollectionView to sort, filter data for veiwing but that does not change the actual collection. I need to sort the actual collection so that I can calculate TotalMoneySpentTillThisBar for each item. Its value depends on order of items in colection.

    Thanks.

  • mdm20
    mdm20 almost 13 years
    Whenever you call Sort(), you will get an CollectionChanged event for every item in the collection...
  • Souvik Basu
    Souvik Basu almost 13 years
    Yeah I had this as the first option to insert at right index, but thought that would introduce quite a bit of complexity while adding an item to collection. But your argument about tons of events being fired is true and probably I should rethink on this.
  • mdm20
    mdm20 almost 13 years
    You can extend ObserableCollection if you want the calls to remain the same. I added code.
  • Gregfr
    Gregfr almost 13 years
    Then to try to answer your question, i think i would create a new class that inherit from the ObservableCollection. Then i would override the constructor to recalculate the TotalMoneySpentTillThisBar for each item. Something like foreach item get the collection of item with a stardate sooner that the current one, do the sum, and update the current. Then override the Add() with a similar mechanism for each new instance added in the collection, and use a ICollectionViewSource to sort the display
  • Roman Starkov
    Roman Starkov over 12 years
    Is it possible to disable notifications during the sort?
  • Berryl
    Berryl about 12 years
    @romkyns. I'm not aware of ObservableCollection directly supporting the ability to disable notification the way BindingList does (ie, list.RaiseListChangedEvents = false). BUT you can deregister your handler before the sort and re-register afterwards.
  • Roman Starkov
    Roman Starkov about 12 years
    @Berryl I've since implemented an ObservableSortedList<T> which keeps items always sorted, even if the property on which the items are sorted is modified (requires that the object implements INotifyPropertyChanged).
  • Berryl
    Berryl about 12 years
    @romkyns. looks sweet, thx. I usually use ICollectionView but want a view model without a wpf dependency. Do you have tests showing it with filtering, etc?
  • Roman Starkov
    Roman Starkov about 12 years
    @Berryl afraid not; the only test is how it's used in one of my projects. It also doesn't support any filtering at all. You're welcome to contribute some tests, or just derive your own variant and use as you please (under the GPL).
  • Doug
    Doug over 10 years
    +1 for sorting outside ObservableCollection - this is how it should be done.
  • rajibdotnet
    rajibdotnet about 10 years
    Works like a charm. Elegant programming.
  • Scott Nimrod
    Scott Nimrod almost 10 years
    Is there a WinRT version of this implementation? CollectionViewSource.SortDescriptions does not seem to be supported.
  • dba
    dba over 7 years
    Applying Sol. 2 automtically Selects the first Listboxitem, although I styled the ItemcontainerStyle with Focusable=false. Any thoughts on this? Thanks, Daniel
  • dba
    dba over 7 years
    Changing the Collection (adding Items) doesn't resort automatically.. . BR, Daniel
  • Tarnschaf
    Tarnschaf over 4 years
    Why would you need ab ObservableCollection in this case? It appears the content cannot change afterwards...
  • StayOnTarget
    StayOnTarget about 4 years
    What is the , ElementName=MainWindowName for ?
  • nawfal
    nawfal almost 4 years
    The InternalSort implementation could be improved a few ways. One, you don't need the ToList() part, you could directly enumerate sortedItems. Two, you dont need another O(N) sortedItemsList.IndexOf(item) part, you could just keep an int counter to get the right index. But this implementation is really good, even if not the most efficient. At least it keeps the sort stable which is a more likely requirement for UI situations.
  • dba
    dba over 2 years
    it did 6 years ago, and yes, it's still there...