how to sort ObservableCollection
I have a an ObservableCollection and a WPF UserControl is Databound to 开发者_JAVA技巧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.
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}}" >
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
}
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);
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.
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
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);
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);
}
精彩评论