Search code examples
wpfsortingdatagridobservablecollectionicollectionview

ObservableCollection doesn't sort newly added items


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).


Solution

  • 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);          
                }
            }