3

I have a ViewModel which contains an ObservableCollection<CustomKeyGroup<CustomItem>> property bound to a control in a View and the problem is that I want to sort this collection by a property in CustomKeyGroup<T>, without setting the ObservableCollection<...> object property (i.e. sort the collection inline):

public class MainViewModel : ViewModelBase {
    ... // data service etc code

    private ObservableCollection<CustomKeyGroup<CustomItem>> _items = new ObservableCollection<CustomKeyGroup<CustomItem>>();
    public ObservableCollection<CustomKeyGroup<CustomItem>> Items
    {
        get
        {
            return _items;
        }
        set
        {
            _items = value;
            RaisePropertyChanged("Items");
        }
    }

    public void Sort(string _orderBy = null, bool _descending = true) {
        if (string.IsNullOrEmpty(_orderBy) || this.Items.Count == 0) {
            return;
        }

        var test = this.Items.ToList();

        // bubble sort
        try {
            for (int i = test.Count - 1; i >= 0; i--) {
                for (int j = 1; j <= i; j++) {
                    CustomKeyGroup<CustomItem> o1 = test[j - 1];
                    CustomKeyGroup<CustomItem> o2 = test[j];
                    bool move = false;

                    var order = typeof(CustomKeyGroup<CustomItem>).GetProperty(orderBy);
                    var t = order.GetValue(o1);
                    var t2 = order.GetValue(o2);

                    // sort comparisons depending on property
                    if (_descending) { // ascending
                        if (t.GetType() == typeof(int)) { // descending and int property
                            if ((int)t < (int)t2) {
                                move = true;
                            }
                        } else { // descending and string property
                            if (t.ToString().CompareTo(t2.ToString()) > 0) {
                                move = true;
                            }
                        }
                    } else { // ascending
                        if (t.GetType() == typeof(int)) { // ascending and int property
                            if ((int)t > (int)t2) {
                                move = true;
                            }
                        } else { // ascending and string property
                            if (t.ToString().CompareTo(t2.ToString()) < 0) {
                                move = true;
                            }
                        }
                    }

                    // swap elements
                    if (move) {
                        //this.Items.Move(j - 1, j); // "inline"

                        test[j] = o1;
                        test[j - 1] = o2;
                    }
                }
            }
            // set property to raise property changed event
            this.Items = new ObservableCollection<CustomKeyGroup<CustomItem>>(test);
        } catch (Exception) {
            Debug.WriteLine("Sorting error");
        }

        //RaisePropertyChanged("Items"); // "inline sort" raise property changed to update Data binding

        Debug.WriteLine("Sorted complete");
    }

    ... // get data from service, etc.

From the code above, the attempted inline sorts are commented out (as they do not update the control that databinds to it), and the manual setting of Items are left in (works, but if you scroll down the control and sort, it will take you back to the top - undesirable!).

Anyone have any idea how I can update the view/control using an inline sort option? I've also tried manually raising the RaisePropertyChanged event (specified in ObservableObject using the MVVMLight Toolkit) to no avail.

Note: Setting a breakpoint at the end of the try-catch reveals that the ObservableCollection<...> is indeed sorted, but the changes just do not reflect in the View! Even weirder is that the control (LongListSelector) has a JumpList bound to another property of CustomKeyGroup<T> and it successfully updates instantly!! If I tap on any of these items in the JumpList, the View correctly updates itself, revealing the sorted items... I then thought of setting the DataContext of the View after sorting, but that also does not solve the issue.

Thanks.

Travis Liew
  • 787
  • 1
  • 11
  • 34
  • Sorting a collection in WPF can be problematic. See here: http://stackoverflow.com/questions/1945461/how-do-i-sort-an-observable-collection?rq=1 – piofusco Dec 08 '14 at 17:47
  • Thanks for your reply. I think my problem relates to WP/Silverlight in general as I am able to sort the `ObservableCollection` perfectly, the problem comes in with the XAML binding. It (the control, LLS in this case) doesn't update after the collection is sorted (inspecting the collection with a breakpoint after sorting reveals it has been sorted). – Travis Liew Dec 08 '14 at 23:16
  • You hit the nail on the head. Unless you set your ObservableCollection to a newly sorted ObservableCollection, the changes won't be flagged and thus your view won't get updated. – piofusco Dec 08 '14 at 23:43
  • I see. Quite an odd problem isn't it? I would've thought that _changing_ the collection (by removing some element) would at least update the view (by raising `NotifyCollectionChanged` on the collecton, but apparently not. It simply removes the element from the view, without updating the order! I suppose this makes sense in the implementation of `ObervableCollection` as `Move` raises `CollectionChanged` anyway. – Travis Liew Dec 09 '14 at 00:39
  • Indeed. It's very counter-intuitive you cannot change any part of an ObservableCollection and the change is not raised automatically. However, for every hole in WPF there is a third party library out their to fill it. – piofusco Dec 09 '14 at 05:07

1 Answers1

1

Adding my own answer here.

So following the comments from the original post, @piofusco points out that a View does not update when an ObservableCollection has only been sorted. Even manually changing the collection (hence, raising NotifyPropertyChanged or NotifyCollectionChanged) does not update it.

Searching around a little more, I decided I could make use of CollectionViewSource, which would do my sorting for me - without changing the collection itself (hence allowing the control to retain its current scroll position). To get it working, basically, add a new property to the ViewModel of type CollectionViewSource, add a SortDescription, set its Source and bind directly to that property (instead of the original ObservableCollection:

In ViewModel:

private CollectionViewSource _sortedCollection = new CollectionViewSource();
public CollectionViewSource SortedCollection {
    get {
        _sortedCollection.Source = this.Items; // Set source to our original ObservableCollection
        return _sortedCollection;
    }
    set {
        if (value != _sortedCollection) {
            _sortedCollection = value;
            RaiseNotifyPropertyChanged("SortedCollection"); // MVVMLight ObservableObject
        }
    }
}

View XAML (note the binding to Property.View):

<ListBox ItemsSource="{Binding SortedCollection.View}" ... />

And in your View code-behind, if you have a Sort button:

ViewModel _vm = this.DataContext as ViewModel;
viewModel.SortedCollection.SortDescriptions.Clear(); // Clear all 
viewModel.SortedCollection.SortDescriptions.Add(new SortDescription("PropertyName", ListSortDirection.Descending)); // Sort descending by "PropertyName"

And boom! Your sorted collection should update instantly in the View! Even better is that it retains our ObservableCollection functionality in that any updates to objects in the ObservableCollection will raise the NotifyPropertyChanged or NotifyCollectionChanged handlers, thereby updating the View (allowing for both sorting and updating of objects while retaining current scroll positions)!

Note: For those out there using a LongListSelector control, I wasn't able to get it to work, and with a little more internet-digging with I came across this post, which, discusses why LLS cannot bind to a CollectionViewSource.View without some modifications. So I ended up using a ListBox control instead. You can read about some of the differences here. For my task though, the ListBox will suffice.

Community
  • 1
  • 1
Travis Liew
  • 787
  • 1
  • 11
  • 34