tags:

views:

1377

answers:

3

Consider this code (type names genericised for the purposes of example):

// Bound to ListBox.ItemsSource
_items = new ObservableCollection<Item>();

// ...Items are added here ...

// Specify custom IComparer for this collection view
_itemsView = CollectionViewSource.GetDefaultView(_items)
((ListCollectionView)_itemsView).CustomSort = new ItemComparer();

When I set CustomSort, the collection is sorted as I expect.

However I require the data to re-sort itself at runtime in response to the changing of the properties on Item. The Item class derives from INotifyPropertyChanged and I know that the property fires correctly as my data template updates the values on screen, only the sorting logic is not being called.

I have also tried raising INotifyPropertyChanged.PropertyChanged passing an empty string, to see if a generic notification would cause the sorting to be initiated. No bananas.

EDIT In response to Kent's suggestion I thought I'd point out that sorting the items using this has the same result, namely that the collection sorts once but does not re-sort as the data changes:

_itemsView.SortDescriptions.Add(
    new SortDescription("PropertyName", ListSortDirection.Ascending));
+1  A: 

Given that you're using custom sorting, there's no way for the ListCollectionView to know what criteria should trigger a refresh. Therefore, you will need to call Refresh() on the collection view yourself.

HTH, Kent

Kent Boogaart
@Kent, thanks I'll try this. I'm not clear on the logic here as I also tried sorting using "SortDescriptions.Add(new SortDescription". Even when the the ListCollectionView does know what criteria to sort on, it does not do so.
Drew Noakes
@Kent - I'm revisiting this again. I still don't see why the framework couldn't watch the property specified in the `SortDescription` for changes. My class notifies when this property changes.
Drew Noakes
+4  A: 

I found this article by Dr. WPF which starts out with an answer to my question, then moves on to discuss the performance impact of calling Refresh. Some key excerpts:

Unfortunately, the Refresh() method results in a complete regeneration of the view. When a Refresh() occurs within the view, it raises a CollectionChanged notification and supplies the Action as "Reset". The ItemContainerGenerator for the ListBox receives this notification and responds by discarding all the existing visuals for the items. It then completely regenerates new item containers and visuals.

He then describes a hacky workaround that improves performance. Rather than calling Refresh, remove, change then re-add the item.

I would have thought it possible that the list view could track an item that changes and know to re-position that item alone within the view.

A new approach was introduced in .NET 3.5 SP1 involving the interface IEditableObject that provides transactional editing via data bindings to the template with methods BeginEdit(), CancelEdit(), and EndEdit(). Read the article for more information.

EDIT As user346528 points out, IEditableObject was not in fact new in 3.5SP1. It actually looks like it's been in the framework since 1.0.

Drew Noakes
+1  A: 

Bumping an old post, but just make a new collection class which inherits from ListViewCollection and Overrides OnPropertyChanged (for an IBindingList, ListChanged events will contain the property change in the ListChangedEventArgs parameter). And make sure the items within the collection implements and uses INotifyPropertyChange whenever a property changes (raised by you), or the collection won't bind to property changes.

Then in this OnPropertyChanged method, sender will be the item. Remove the item if--and only if--a property which would cause a resort is changed, then re-add it (insert it in sorted position if adding it doesn't do this already). Moving an item is preferable if it is available instead of removing/adding it. Similarly, this should also be done with filtering (checking the filter predicate).

IEditableObject is not needed! If you desire to edit several properties, then finish editing (like editing 3 properties and then selecting on a different row in WinForm DataGridView), then having it sort, this would be the proper method of getting this to work. But a lot of times you will probably want the collection to resort after changing each property without having to manually call BeginEdit/EndEdit. IEditableObject, btw, is present in the .NET 2.0 framework and is not new to .NET 3.5 (if you read the Dr's Article).

Note: Issues can occur using BeginEdit() and EndEdit() with multiple edits to the same item--unless you increment (for true)/decrement (for false) an integer instead of setting a Boolean! Remember to increment/decrement an integer to truly know when editing is finished.

Keeping a perpetually sorted list is time consuming and error prone (and it can break the insert contract if you force sorted inserts), and should only be used in certain places such as ComboBoxes. On any grid, this is a very bad idea, as changing a row will cause it to resort out from under the users current position.

Public Class ListView
  Inherits ListCollectionView

  Protected Overrides Sub OnPropertyChanged(sender As Object, e As PropertyChangedEventArgs)

    ' Add resorting/filtering logic here.
  End Sub
End Class

The best example on a collection that does similar to what you need is Jesse Johnsons ObjectListView, although this is .NET 2.0 specific (IBindingList instead of INotifyCollectionChanged/ObservableCollection/ListCollectionView) and uses a very restrictive license. His blog may still be very valuable in how he accomplished this.

Edit:

Forgot to add that Sender will be the the item you need to resort, and e.PropertyName is what you will need to use to determine if it is within the SortDescriptions. If it isn't, a change to that property won't result in a resort being needed. If e.PropertyName is Nothing, then it is just like a refresh, where many properties may have changed and resorting should be done.

To determine if it needs filtered, just run it through the FilterPredicate, and remove it if needed. Filtering is a lot less expensive than sorting.

Hopefully Helpful,

TamusJRoyce

TamusJRoyce
Great answer. I'll give this a go next time I come up against this. For one of your first SO answers, this is a great one :) Thanks.
Drew Noakes
Thanks. I currently have this implemented for a project I am working on. I so wish it wasn't closed source, as it was fun to implement and I want to share it so badly :). I also saw a couple of errors in Dr. WPF's article, so I thought I'd mention an alternative which doesn't use BeingEdit/EndEdit.
TamusJRoyce