how to sort ObservableCollection
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
get the
ICollectionView
of yourObservableCollection
and sort it, as explained here http://marlongrech.wordpress.com/2008/11/22/icollectionview-explained/bind your
ObservableCollection
to aCollectionViewsource
, add a sort on it, then use thatCollectionViewSource
as theItemSource
of aListView
.
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);
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, 2021Comments
-
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 almost 13 yearsWhenever you call Sort(), you will get an CollectionChanged event for every item in the collection...
-
Souvik Basu almost 13 yearsYeah 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 almost 13 yearsYou can extend ObserableCollection if you want the calls to remain the same. I added code.
-
Gregfr almost 13 yearsThen 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 over 12 yearsIs it possible to disable notifications during the sort?
-
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 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 implementsINotifyPropertyChanged
). -
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 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 over 10 years+1 for sorting outside ObservableCollection - this is how it should be done.
-
rajibdotnet about 10 yearsWorks like a charm. Elegant programming.
-
Scott Nimrod almost 10 yearsIs there a WinRT version of this implementation? CollectionViewSource.SortDescriptions does not seem to be supported.
-
dba over 7 yearsApplying Sol. 2 automtically Selects the first Listboxitem, although I styled the ItemcontainerStyle with Focusable=false. Any thoughts on this? Thanks, Daniel
-
dba over 7 yearsChanging the Collection (adding Items) doesn't resort automatically.. . BR, Daniel
-
Tarnschaf over 4 yearsWhy would you need ab ObservableCollection in this case? It appears the content cannot change afterwards...
-
StayOnTarget about 4 yearsWhat is the
, ElementName=MainWindowName
for ? -
nawfal almost 4 yearsThe
InternalSort
implementation could be improved a few ways. One, you don't need theToList()
part, you could directly enumeratesortedItems
. 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 over 2 yearsit did 6 years ago, and yes, it's still there...