how to sort ObservableCollection
Asked Answered
P

7

21

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.

Praseodymium answered 2/9, 2011 at 14:18 Comment(6)
Is this a one time task, i.e. something that can be done before the collection is bound to the control?Winona
the collection keeps changing even when it is bound (that is the reason I am using ObservableCollection so that the UI updates if the collection changes). One option to solve this problem is I handle it while adding an item to collection to make sure is it inserted in proper index as per sort order or the second option is I sort the collection whenever an item is added or removed. I am trying to evaluate the second option here.Praseodymium
In my opinion, it is a design flaw that the object itself knows how much money has been spent up till now and that this information depends on the ordering. This should be a feature in the user control (ShowTotal = true).Winona
I am not strong with LINQ but I have used it withOUT the ForEach and the sorted output is a reference back to the objects in the original collection. sortFieldDefs = fieldDefs.Where(fd => fd.Sort && fd.ID > 0).OrderBy(fd => fd.DispName).ToList();Groundsill
This is an MVVM app and this collection is in the ViewModel of the UserControl so I cannot store data in UserControl. The actual ViewModel class contains much more details but I have given a simplified class here for the problem in hand.Praseodymium
Does this answer your question? How do I sort an observable collection?Espagnole
H
52

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}}" >
Hamal answered 2/9, 2011 at 17:25 Comment(7)
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 displayHamal
+1 for sorting outside ObservableCollection - this is how it should be done.Declaim
Works like a charm. Elegant programming.Jubilee
Is there a WinRT version of this implementation? CollectionViewSource.SortDescriptions does not seem to be supported.Suanne
Applying Sol. 2 automtically Selects the first Listboxitem, although I styled the ItemcontainerStyle with Focusable=false. Any thoughts on this? Thanks, DanielInactivate
Changing the Collection (adding Items) doesn't resort automatically.. . BR, DanielInactivate
What is the , ElementName=MainWindowName for ?Espagnole
S
18

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
}
Spacing answered 2/9, 2011 at 15:14 Comment(7)
Whenever you call Sort(), you will get an CollectionChanged event for every item in the collection...Iceskate
Is it possible to disable notifications during the sort?Lacilacie
@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.Bundle
@Bundle 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).Lacilacie
@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?Bundle
@Bundle 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).Lacilacie
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.Placeman
I
15

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);
Iceskate answered 2/9, 2011 at 15:0 Comment(2)
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.Praseodymium
You can extend ObserableCollection if you want the calls to remain the same. I added code.Iceskate
P
0

What about sorting the data using LINQ on the different collection:

var collection = new List<BarData>();
//add few BarData objects to collection

// sort the data using LINQ
var sorted = from item in collection orderby item.StartData select item;

// create observable collection
var oc = new ObservableCollection<BarData>(sorted);

This worked for me.

Purington answered 24/11, 2015 at 12:31 Comment(1)
Why would you need ab ObservableCollection in this case? It appears the content cannot change afterwards...Crispas
I
0

Also using LINQ/Extensionmethod one can aviod firing the NotifyPropertyChanged Event by not setting the source col to the sorted one, but clear the original and add the items of the sorted one. (this will continue fire the Collectionchanged Event, if implemented).

<Extension>
Public Sub SortByProp(Of T)(ByRef c As ICollection(Of T), PropertyName As String)
    Dim l = c.ToList
    Dim sorted = l.OrderBy(Function(x) x.GetType.GetProperty(PropertyName).GetValue(x))

    c.Clear()
    For Each i In sorted
        c.Add(i)
    Next

End Sub
Inactivate answered 5/10, 2016 at 10:9 Comment(1)
it did 6 years ago, and yes, it's still there...Inactivate
W
0

I know this is old post, but I was unhappy with most of the solutions, because it broke bindings. So if anyone comes across, this is what I did. You can make more overloads for more property sortings.

This doesnt break binding.

    public static void AddRangeSorted<T, TSort>(this ObservableCollection<T> collection, IEnumerable<T> toAdd, Func<T, TSort> sortSelector, OrderByDirection direction)
    {
        var sortArr = Enumerable.Concat(collection, toAdd).OrderBy(sortSelector, direction).ToList();
        foreach (T obj in toAdd.OrderBy(o => sortArr.IndexOf(o)).ToList())
        {
            collection.Insert(sortArr.IndexOf(obj), obj);
        }
    }

    public static void AddRangeSorted<T, TSort, TSort2>(this ObservableCollection<T> collection, IEnumerable<T> toAdd, Func<T, TSort> sortSelector, OrderByDirection direction, Func<T, TSort2> sortSelector2, OrderByDirection direction2)
    {
        var sortArr = Enumerable.Concat(collection, toAdd).OrderBy(sortSelector, direction).ThenBy(sortSelector2, direction2).ToList();
        foreach (T obj in toAdd.OrderBy(o=> sortArr.IndexOf(o)).ToList())
        {
            collection.Insert(sortArr.IndexOf(obj), obj);
        }
    }

And usage:

OrderLines.AddRangeSorted(toAdd,ol=>ol.ID, OrderByDirection.Ascending);
Womankind answered 15/3, 2021 at 19:5 Comment(0)
U
0

I'm using VMMV model. You can just add this code to an event or btn or something. and it sorts your collection by parameter (int,string...)-sortVar.

            OrgCollection = new ObservableCollection<ClassModel>();
        var sortedCollection = new ObservableCollection<ClassModel>(OrgCollection.OrderBy(x => x.sortVar));
        OrgCollection.Clear();
        foreach (var x in sortedCollection)
        {
            ClassModel modelToSort = new ClassModel()
            {// 'var y in ClassModel' = x.'var y in ClassModel'
             // e.g. VarName = x.VarName, ...
            };
            OrgCollection.Add(modelToSort);
        }
Uncompromising answered 17/1, 2023 at 9:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.