locked
CollectionViewSource sort not reflecting automatically after ObservableCollection item property change? RRS feed

  • Question

  • I use a CollectionViewSource to feed the Items of a ListView.  My CollectionViewSource is bound to an ObservableCollection that contains items that implement INotifyPropertyChanged.  Item property changes are being appropriately reflected in the ListView, but I'm surprised to find that the item does not automatically get resorted when its property that the sort is keying off of changes.  It would seem that all the information should be there for the ListView to recognize the need to resort the changed item.  Is there a way to make this happen or another 'lighter' alternative to calling Refresh() on the view?

     

    Thanks!

     

    mike

    Tuesday, January 8, 2008 12:21 AM

Answers

  • Marco,

     

    Thanks for getting back to me.  That's what I suspected, but it's unfortunate.  If my collection is very large, having to refresh my entire view just because the value of the sort or group key property changes for a single item is hugely inefficient.  Hopefully, future versions of WPF will address this issue.  I don't want to pollute my collection or its items with knowledge of their view, in order to refresh it when the group or sort key property changes.  My solution is to create a CollectionViewSource derivative that handles this for me, which I thought I'd share with others:

     

    (See updated verison in the post below)

    Thursday, January 10, 2008 5:12 PM
  • As far as I know, calling Refresh() on the view is the best bet for the current version of WPF, this also holds true if the property used for grouping is changed.
    Thursday, January 10, 2008 2:39 AM

All replies

  • Anyone have any ideas on this one?

     

    Thanks,

     

    mike

     

    Wednesday, January 9, 2008 11:31 PM
  • As far as I know, calling Refresh() on the view is the best bet for the current version of WPF, this also holds true if the property used for grouping is changed.
    Thursday, January 10, 2008 2:39 AM
  • Marco,

     

    Thanks for getting back to me.  That's what I suspected, but it's unfortunate.  If my collection is very large, having to refresh my entire view just because the value of the sort or group key property changes for a single item is hugely inefficient.  Hopefully, future versions of WPF will address this issue.  I don't want to pollute my collection or its items with knowledge of their view, in order to refresh it when the group or sort key property changes.  My solution is to create a CollectionViewSource derivative that handles this for me, which I thought I'd share with others:

     

    (See updated verison in the post below)

    Thursday, January 10, 2008 5:12 PM
  • Thanks, demler.  A very concise explanation of the problem, and a great solution to the problem.  Your class is exactly what I needed today.  Thanks!

     

    David

    Thursday, February 14, 2008 9:35 PM
  • David,

     

    I'm glad to hear that I was able to help someone out and save them the trouble that I went through.  There was a slight issue with the previous version that I posted, and I'd be meaning to post an update.  If you later add new items to your collection, property changes to those items aren't automatically refreshing the view.  Here's an updated version:

     

    public class AutoRefreshCollectionViewSource : CollectionViewSource

    {

    protected override void OnSourceChanged(object oldSource, object newSource)

    {

    if (oldSource != null)

    {

    SubscribeSourceEvents(oldSource, true);

    }

     

    if (newSource != null)

    {

    SubscribeSourceEvents(newSource, false);

    }

     

    base.OnSourceChanged(oldSource, newSource);

    }

     

    private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)

    {

    bool refresh = false;

     

    foreach (SortDescription sort in SortDescriptions)

    {

    if (sort.PropertyName == e.PropertyName)

    {

    refresh = true;

    break;

    }

    }

     

    if (!refresh)

    {

    foreach (GroupDescription group in GroupDescriptions)

    {

    PropertyGroupDescription propertyGroup = group as PropertyGroupDescription;

     

    if (propertyGroup != null && propertyGroup.PropertyName == e.PropertyName)

    {

    refresh = true;

    break;

    }

    }

    }

     

    if (refresh)

    {

    View.Refresh();

    }

    }

     

    private void Source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)

    {

    if (e.Action == NotifyCollectionChangedAction.Add)

    {

    SubscribeItemsEvents(e.NewItems, false);

    }

    else if (e.Action == NotifyCollectionChangedAction.Remove)

    {

    SubscribeItemsEvents(e.OldItems, true);

    }

    else

    {

    // TODO: Support this

    Debug.Assert(false);

    }

    }

     

    private void SubscribeItemEvents(object item, bool remove)

    {

    INotifyPropertyChanged notify = item as INotifyPropertyChanged;

     

    if (notify != null)

    {

    if (remove)

    {

    notify.PropertyChanged -= Item_PropertyChanged;

    }

    else

    {

    notify.PropertyChanged += Item_PropertyChanged;

    }

    }

    }

     

    private void SubscribeItemsEvents(IEnumerable items, bool remove)

    {

    foreach (object item in items)

    {

    SubscribeItemEvents(item, remove);

    }

    }

     

    private void SubscribeSourceEvents(object source, bool remove)

    {

    INotifyCollectionChanged notify = source as INotifyCollectionChanged;

     

    if (notify != null)

    {

    if (remove)

    {

    notify.CollectionChanged -= Source_CollectionChanged;

    }

    else

    {

    notify.CollectionChanged += Source_CollectionChanged;

    }

    }

     

    SubscribeItemsEvents((IEnumerable)source, remove);

    }

    }

    • Proposed as answer by Mike Graham Thursday, January 21, 2010 3:25 PM
    Thursday, February 14, 2008 9:48 PM
  • Too funny. I saw that that would be a problem, and was verifying it the second that I got a notification about your new posting.

     

    This also doesn't work for DependencyObjects that have DependencyProperties changing, since change notifications aren't handled in that case via INotifyPropertyChanged.  I'm gonig to mull that over for a bit and see what I come up with.  If I come up with something useful I'll post it here as well.

     

    Thanks again,

     

    David

     

    Thursday, February 14, 2008 9:53 PM
  • I stumbled upon your post while searching for the same problem. I wanted to add the following information for anybody else who come across that problem.

    If your application can support it, using a DataView as the data context will automatically refresh the sort when a sort key value changes. And it is not slow, as compared to refreshing the whole view.

    So, an idea would be to reflector the DataView and check what it does and mimic that into your own collection.
    • Edited by Pierre-Alain Vigeant Tuesday, July 29, 2008 2:06 PM Changed DataTable for DataView
    • Proposed as answer by Jecho Jekov Friday, August 13, 2010 1:03 PM
    Tuesday, July 29, 2008 1:56 PM
  • I added your class to my project, changed the declaration and new in my existing code (from CollectionViewSource) to use the new class, and voila, the lists update with newly added items.  very cool :)

    Data Nomad: Row Level Security for SQL Server http://www.technicalmedia.com http://www.DataNomad.com
    Thursday, January 21, 2010 3:27 PM
  • Demler

     

    Thanks for this class. It has solved a rather large headache for me.

    Friday, July 23, 2010 1:23 PM
  • True. Using BindingListCollectionView with DataView completely solves sorting and filtering issues.
    • Proposed as answer by Jecho Jekov Friday, August 13, 2010 1:09 PM
    Friday, August 13, 2010 1:09 PM
  • i tried to use your class to reflect the changes in the data source , where i am doing some insert ,delete operations on my table

    but it didn't work for me , so please if any one have an idea on how to make my updates on the data base be reflected directly to the collection view , i'll be thankful

    Friday, August 13, 2010 7:17 PM
  • One solution for work-around refreshing the whole view, which is inefficient as you noted, is to remove and re-add the item to the ObservableCollection. This causes the item to appear again in the correct group.
    Thursday, October 14, 2010 7:33 PM
  • If you are targeting .NET 3.5SP1+, you can take advantage from the IEditableCollectionView capabilities, if supported.

    As stated in this post

    http://drwpf.com/blog/2008/10/20/itemscontrol-e-is-for-editable-collection/

    it is possible to force a partial refresh (withou invalidating the complete view) view without the remove/re-add hack (check the 'Is IEditableObject Absolutely Required?' paragraph).

    Following the modified version of the AutoRefreshCollectionViewSource:

    #define USE_WEAK_EVENTS
    
    namespace WPFCollectionViewSource
    {
      #region Namespaces
      using System;
      using System.Collections;
      using System.Collections.Specialized;
      using System.ComponentModel;
      using System.Linq;
      using System.Windows;
      using System.Windows.Data;
    
      #endregion
    
      /// <summary>
      ///  Class used to implement a collection view source which is able to automatically request a smart refresh an the associated view when an item changes.
      /// </summary>
      public class AutoRefreshCollectionViewSource : CollectionViewSource
      {
        /// <summary>
        ///  Called when the source has changed.
        /// </summary>
        /// <param name = "oldSource">The old source.</param>
        /// <param name = "newSource">The new source.</param>
        protected override void OnSourceChanged(object oldSource, object newSource)
        {
          if (oldSource != null)
            UnsubscribeSourceEvents(oldSource);
    
          if (newSource != null)
            SubscribeSourceEvents(newSource);
    
          base.OnSourceChanged(oldSource, newSource);
        }
    
        /// <summary>
        ///  Unsubscribes from the source events.
        /// </summary>
        /// <param name = "source">The source.</param>
        private void UnsubscribeSourceEvents(object source)
        {
          var notify = source as INotifyCollectionChanged;
    
          if (notify != null)
    #if USE_WEAK_EVENTS
            NotifyCollectionChangedWeakEventManager.RemoveListener(notify, this);
    #else
            notify.CollectionChanged -= OnSourceCollectionChanged;
    #endif
    
          if (source is IEnumerable)
            UnsubscribeItemsEvents((IEnumerable)source);
        }
    
        /// <summary>
        ///  Subscribes to the source events.
        /// </summary>
        /// <param name = "source">The source.</param>
        private void SubscribeSourceEvents(object source)
        {
          var notify = source as INotifyCollectionChanged;
    
          if (notify != null)
    #if USE_WEAK_EVENTS
            NotifyCollectionChangedWeakEventManager.AddListener(notify, this);
    #else
            notify.CollectionChanged += OnSourceCollectionChanged;
    #endif
    
          if (source is IEnumerable)
            SubscribeItemsEvents((IEnumerable)source);
        }
    
        /// <summary>
        ///  Unsubscribes from the item events.
        /// </summary>
        /// <param name = "item">The item.</param>
        private void UnsubscribeItemEvents(object item)
        {
          var notify = item as INotifyPropertyChanged;
    
          if (notify != null)
    #if USE_WEAK_EVENTS
            NotifyPropertyChangedWeakEventManager.RemoveListener(notify, this);
    #else
            notify.PropertyChanged -= OnItemPropertyChanged;
    #endif
        }
    
        /// <summary>
        ///  Subscribes to the item events.
        /// </summary>
        /// <param name = "item">The item.</param>
        private void SubscribeItemEvents(object item)
        {
          var notify = item as INotifyPropertyChanged;
    
          if (notify != null)
    #if USE_WEAK_EVENTS
            NotifyPropertyChangedWeakEventManager.AddListener(notify, this);
    #else
            notify.PropertyChanged += OnItemPropertyChanged;
    #endif
        }
    
        /// <summary>
        ///  Unsubscribes from the items events.
        /// </summary>
        /// <param name = "items">The items.</param>
        private void UnsubscribeItemsEvents(IEnumerable items)
        {
          foreach (object item in items)
            UnsubscribeItemEvents(item);
        }
    
        /// <summary>
        ///  Subscribes to the items events.
        /// </summary>
        /// <param name = "items">The items.</param>
        private void SubscribeItemsEvents(IEnumerable items)
        {
          foreach (object item in items)
            SubscribeItemEvents(item);
        }
    
    #if USE_WEAK_EVENTS
        /// <summary>
        ///  Handles events from the centralized event table.
        /// </summary>
        /// <param name = "managerType">The type of the <see cref = "T:System.Windows.WeakEventManager" /> calling this method. This only recognizes manager objects of type <see cref = "T:System.Windows.Data.DataChangedEventManager" />.</param>
        /// <param name = "sender">Object that originated the event.</param>
        /// <param name = "e">Event data.</param>
        /// <returns>
        ///  true if the listener handled the event; otherwise, false.
        /// </returns>
        protected override bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
        {
          if (managerType == typeof(NotifyPropertyChangedWeakEventManager))
          {
            OnItemPropertyChanged(sender, (PropertyChangedEventArgs)e);
            return true;
          }
          if (managerType == typeof(NotifyCollectionChangedWeakEventManager))
          {
            OnSourceCollectionChanged(sender, (NotifyCollectionChangedEventArgs)e);
            return true;
          }
          return base.ReceiveWeakEvent(managerType, sender, e);
        }
    #endif
    
        /// <summary>
        ///  Called when a source collection has changed.
        /// </summary>
        /// <param name = "sender">The sender.</param>
        /// <param name = "e">The <see cref = "System.Collections.Specialized.NotifyCollectionChangedEventArgs" /> instance containing the event data.</param>
        private void OnSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
    #if !USE_WEAK_EVENTS
          //If the collection view is cleared, events should be detached.
          //Failing to do so would case the collection view itself to linger in memory until all the removed items are garbage collected.
          //If weak events are used, there is not such risk, so it safe to ignore the problem.
          //Note anyway that if both the collection view and the removed objects are used after a Reset operation, it is possible
          //for the collection view to process unneeded events. This should just a bit of unneeded overhead.
          if (e.Action == NotifyCollectionChangedAction.Reset)
            throw new InvalidOperationException(string.Format("The action {0} is not supported by {1}", e.Action, GetType()));
    #endif
          if (e.NewItems != null)
            SubscribeItemsEvents(e.NewItems);
          if (e.OldItems != null)
            UnsubscribeItemsEvents(e.OldItems);
        }
    
        /// <summary>
        ///  Called when an item property has changed.
        /// </summary>
        /// <param name = "sender">The sender.</param>
        /// <param name = "e">The <see cref = "System.ComponentModel.PropertyChangedEventArgs" /> instance containing the event data.</param>
        private void OnItemPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
          if (IsViewRefreshNeeded(e.PropertyName))
          {
            var view = View;
            if (view != null)
            {
              var current = view.CurrentItem;
              var editableCollectionView = view as IEditableCollectionView;
    
              if (editableCollectionView != null)
              {
                editableCollectionView.EditItem(sender);
                editableCollectionView.CommitEdit();
              }
              else
                view.Refresh();
              view.MoveCurrentTo(current);
              //Ensure that the previously current item is maintained after the refresh operation
            }
          }
        }
    
        /// <summary>
        ///  Determines whether a view refresh is needed.
        /// </summary>
        /// <param name = "propertyName">The name of the changed property.</param>
        /// <returns>
        ///  <c>True</c> if a view refresh needed; otherwise, <c>false</c>.
        /// </returns>
        private bool IsViewRefreshNeeded(string propertyName)
        {
          return SortDescriptions.Any(sort => string.Equals(sort.PropertyName, propertyName)) || GroupDescriptions.OfType<PropertyGroupDescription>().Where(g => string.Equals(g.PropertyName, propertyName)).Any();
        }
    
    #if USE_WEAK_EVENTS
    
        #region Nested type: NotifyCollectionChangedWeakEventManager
        /// <summary>
        ///  Class used to create a weak event manager able to handle <see cref = "INotifyCollectionChanged.CollectionChanged" /> events.
        /// </summary>
        protected class NotifyCollectionChangedWeakEventManager : WeakEventManager
        {
          #region Static Properties
          /// <summary>
          ///  Gets the current manager.
          /// </summary>
          /// <value>The current manager.</value>
          public static NotifyCollectionChangedWeakEventManager CurrentManager
          {
            get
            {
              var manager = (NotifyCollectionChangedWeakEventManager)GetCurrentManager(typeof(NotifyCollectionChangedWeakEventManager));
    
              if (manager == null)
                SetCurrentManager(typeof(NotifyCollectionChangedWeakEventManager), (manager = new NotifyCollectionChangedWeakEventManager()));
    
              return manager;
            }
          }
          #endregion
    
          #region Static Members
          /// <summary>
          ///  Adds the provided listener to the provided source for the event being managed.
          /// </summary>
          /// <param name = "source">The source.</param>
          /// <param name = "listener">The listener.</param>
          public static void AddListener(INotifyCollectionChanged source, IWeakEventListener listener)
          {
            CurrentManager.ProtectedAddListener(source, listener);
          }
    
          /// <summary>
          ///  Removes a previously added listener from the provided source.
          /// </summary>
          /// <param name = "source">The source.</param>
          /// <param name = "listener">The listener.</param>
          public static void RemoveListener(INotifyCollectionChanged source, IWeakEventListener listener)
          {
            CurrentManager.ProtectedRemoveListener(source, listener);
          }
          #endregion
    
          /// <summary>
          ///  When overridden in a derived class, starts listening for the event being managed. After <see cref = "M:System.Windows.WeakEventManager.StartListening(System.Object)" /> is first called, the manager should be in the state of calling <see cref = "M:System.Windows.WeakEventManager.DeliverEvent(System.Object,System.EventArgs)" /> or <see cref = "M:System.Windows.WeakEventManager.DeliverEventToList(System.Object,System.EventArgs,System.Windows.WeakEventManager.ListenerList)" /> whenever the relevant event from the provided source is handled.
          /// </summary>
          /// <param name = "source">The source to begin listening on.</param>
          protected override void StartListening(object source)
          {
            var sender = source as INotifyCollectionChanged;
            if (sender != null)
              sender.CollectionChanged += OnCollectionChanged;
          }
    
          /// <summary>
          ///  Called when the collection has changed.
          /// </summary>
          /// <param name = "sender">The sender.</param>
          /// <param name = "e">The <see cref = "System.Collections.Specialized.NotifyCollectionChangedEventArgs" /> instance containing the event data.</param>
          private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
          {
            DeliverEvent(sender, e);
          }
    
          /// <summary>
          ///  When overridden in a derived class, stops listening on the provided source for the event being managed.
          /// </summary>
          /// <param name = "source">The source to stop listening on.</param>
          protected override void StopListening(object source)
          {
            var sender = source as INotifyCollectionChanged;
            if (sender != null)
              sender.CollectionChanged -= OnCollectionChanged;
          }
        }
        #endregion
    
        #region Nested type: NotifyPropertyChangedWeakEventManager
        /// <summary>
        ///  Class used to create a weak event manager able to handle <see cref = "INotifyPropertyChanged.PropertyChanged" /> events.
        /// </summary>
        protected class NotifyPropertyChangedWeakEventManager : WeakEventManager
        {
          #region Static Properties
          /// <summary>
          ///  Gets the current manager.
          /// </summary>
          /// <value>The current manager.</value>
          public static NotifyPropertyChangedWeakEventManager CurrentManager
          {
            get
            {
              var manager = (NotifyPropertyChangedWeakEventManager)GetCurrentManager(typeof(NotifyPropertyChangedWeakEventManager));
    
              if (manager == null)
                SetCurrentManager(typeof(NotifyPropertyChangedWeakEventManager), (manager = new NotifyPropertyChangedWeakEventManager()));
    
              return manager;
            }
          }
          #endregion
    
          #region Static Members
          /// <summary>
          ///  Adds the provided listener to the provided source for the event being managed.
          /// </summary>
          /// <param name = "source">The source.</param>
          /// <param name = "listener">The listener.</param>
          public static void AddListener(INotifyPropertyChanged source, IWeakEventListener listener)
          {
            CurrentManager.ProtectedAddListener(source, listener);
          }
    
          /// <summary>
          ///  Removes a previously added listener from the provided source.
          /// </summary>
          /// <param name = "source">The source.</param>
          /// <param name = "listener">The listener.</param>
          public static void RemoveListener(INotifyPropertyChanged source, IWeakEventListener listener)
          {
            CurrentManager.ProtectedRemoveListener(source, listener);
          }
          #endregion
    
          /// <summary>
          ///  When overridden in a derived class, starts listening for the event being managed. After <see cref = "M:System.Windows.WeakEventManager.StartListening(System.Object)" /> is first called, the manager should be in the state of calling <see cref = "M:System.Windows.WeakEventManager.DeliverEvent(System.Object,System.EventArgs)" /> or <see cref = "M:System.Windows.WeakEventManager.DeliverEventToList(System.Object,System.EventArgs,System.Windows.WeakEventManager.ListenerList)" /> whenever the relevant event from the provided source is handled.
          /// </summary>
          /// <param name = "source">The source to begin listening on.</param>
          protected override void StartListening(object source)
          {
            var sender = source as INotifyPropertyChanged;
            if (sender != null)
              sender.PropertyChanged += OnPropertyChanged;
          }
    
          /// <summary>
          ///  Called when the property has changed.
          /// </summary>
          /// <param name = "sender">The sender.</param>
          /// <param name = "e">The <see cref = "System.ComponentModel.PropertyChangedEventArgs" /> instance containing the event data.</param>
          private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
          {
            DeliverEvent(sender, e);
          }
    
          /// <summary>
          ///  When overridden in a derived class, stops listening on the provided source for the event being managed.
          /// </summary>
          /// <param name = "source">The source to stop listening on.</param>
          protected override void StopListening(object source)
          {
            var sender = source as INotifyPropertyChanged;
            if (sender != null)
              sender.PropertyChanged -= OnPropertyChanged;
          }
        }
        #endregion
    
    #endif
      }
    }

    Note that the modified implementation can be configured with a compile switch to use weak or strong events. Weak events are preferable, since they avoid coupling collection source and its items with collection view source.

     

    • Proposed as answer by Daniele Mancini Tuesday, January 25, 2011 2:03 PM
    • Edited by Daniele Mancini Wednesday, January 26, 2011 10:19 AM Added a check to ensure that a View is attached
    Tuesday, January 25, 2011 1:53 PM
  • Sorry to join the party later, but there is a minor bug in this code snippet. If you use a SetItem option to replace an item at an index, then this code does not refresh properly.

    In OnSourceCollectionChanged

    if (e.NewItems != null)
            SubscribeItemsEvents(e.NewItems);
    if (e.OldItems != null)
            UnsubscribeItemsEvents(e.OldItems);

    needs to be

    if (e.OldItems != null)
            UnsubscribeItemsEvents(e.OldItems);

    if (e.NewItems != null)
            SubscribeItemsEvents(e.NewItems);

    If not, then the unsubscribe removes notification of any changes. This only happens on a SetItem where Old and New come at the same time and they are the same item.

    Wednesday, April 23, 2014 2:19 PM