سؤال

I have the following ObservableCollection that's bound to a DataGrid:

public ObservableCollection<Message> Messages = new ObservableCollection<Message>;

XAML:

<DataGrid ItemsSource="{Binding Path=Messages}">

I sort it on startup, using default view:

ICollectionView view = CollectionViewSource.GetDefaultView(Messages);
view.SortDescriptions.Add(new SortDescription("TimeSent", ListSortDirection.Descending));

It all works fine, but the problem is that whenever I add a new message to Messages collection, it simply gets appended to the bottom of the list, and not sorted automatically.

Messages.Add(message);

Am I doing something wrong? I'm sure I could work around the problem by refreshing the view each time I add an item, but that just seems like the wrong way of doing it (not to mention performance-wise).

هل كانت مفيدة؟

المحلول

So I did a bit more investigating, and it turns out my problem is due to limitation of WPF datagrid. It will not automatically re-sort the collection when underlying data changes. In other words, when you first add your item, it will be sorted and placed in the correct spot, but if you change a property of the item, it will not get re-sorted. INotifyPropertyChanged has no bearing on sorting updates. It only deals with updating displayed data, but doesn't trigger sorting it. It's the CollectionChanged event that forces re-sorting, but modifying an item that's already in the collection won't trigger this particular event, and hence no sorting will be performed.

Here's another similar issue: C# WPF Datagrid doesn't dynamically sort on data update

That user's solution was to manually call OnCollectionChanged().

In the end, I combined the answers from these two threads:

  1. ObservableCollection not noticing when Item in it changes (even with INotifyPropertyChanged)
  2. ObservableCollection and Item PropertyChanged

I also added 'smart' sorting, that only Calls OnCollectionChanged() if the property changed is the value that's being currently used in SortDescription.

    public class MessageCollection : ObservableCollection<Message>
    {
        ICollectionView _view;

        public MessageCollection()
        {
            _view = CollectionViewSource.GetDefaultView(this);                        
        }

        public void Sort(string propertyName, ListSortDirection sortDirection)
        {
            _view.SortDescriptions.Clear();
            _view.SortDescriptions.Add(new SortDescription(propertyName, sortDirection));
        }

        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {            
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    this.AddPropertyChanged(e.NewItems);
                    break;

                case NotifyCollectionChangedAction.Remove:
                    this.RemovePropertyChanged(e.OldItems);
                    break;

                case NotifyCollectionChangedAction.Replace:
                case NotifyCollectionChangedAction.Reset:
                    this.RemovePropertyChanged(e.OldItems);
                    this.AddPropertyChanged(e.NewItems);
                    break;
            }

            base.OnCollectionChanged(e);
        }

        private void AddPropertyChanged(IEnumerable items)
        {
            if (items != null)
            {
                foreach (var obj in items.OfType<INotifyPropertyChanged>())
                {
                    obj.PropertyChanged += OnItemPropertyChanged;
                }
            }
        }

        private void RemovePropertyChanged(IEnumerable items)
        {
            if (items != null)
            {
                foreach (var obj in items.OfType<INotifyPropertyChanged>())
                {
                    obj.PropertyChanged -= OnItemPropertyChanged;
                }
            }
        }

        private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            bool sortedPropertyChanged = false;
            foreach (SortDescription sortDescription in _view.SortDescriptions)
            {
                if (sortDescription.PropertyName == e.PropertyName)
                    sortedPropertyChanged = true;
            }

            if (sortedPropertyChanged)
            {                
                NotifyCollectionChangedEventArgs arg = new NotifyCollectionChangedEventArgs(
                    NotifyCollectionChangedAction.Replace, sender, sender, this.Items.IndexOf((Message)sender));

                OnCollectionChanged(arg);          
            }
        }

نصائح أخرى

My whole answer below is gibberish. As pointed out in the comments, if you bind to the collection itself, then you are implicitly binding to the default collection view. (However, as a comment at the link notes, Silverlight is an exception -- there no default collection view is created implicitly, unless the collection implements ICollectionViewFactory.)

The CollectionViewSource doesn't modify the underlying collection. To get the sorting, you'll need to bind to the view itself, eg:

<DataGrid ItemsSource="{Binding Path=CollectionViewSource.View}">

Note that, while the original collection (Messages) is untouched, your sorted view will get updated via the notification event:

If the source collection implements the INotifyCollectionChanged interface, the changes raised by the CollectionChanged event are propagated to the views.

I just found the problem, after trying to sort on another property and noticing that it happens to work. Turns out when my messages were being added to the collection the TimeSent property was being initialized to MinDate, and only then updated to actual date. So it was properly being placed at the bottom of the list. The issue is that the position wasn't getting updated when the TimeSent property was modified. Looks like I have an issue with propagation of INotifyPropertyChanged events (TimeSent resides in another object inside Message object).

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top