Travis Liew
Travis Liew

Reputation: 797

C# inline sorting ObservableCollection does not update Data Binding

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.

Upvotes: 3

Views: 1763

Answers (1)

Travis Liew
Travis Liew

Reputation: 797

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.

Upvotes: 1

Related Questions