Suppression for EntityObservableCollection

Mar 18, 2011 at 5:23 PM

I came across an issue with EntityObservableCollection whereby an observer which tries to use the entity object tree gets an inconsistent picture. For example:

- A Book is deleted from the EntityObservableCollection

- EntityObservableCollection.RemoveItem calls base.RemoveItem which emits the CollectionChangedEvent

- An event observer gets the current list of Books in the Library: Library.Books. However, Library.Books still contains the 'deleted' item because it hasn't yet been removed from the ObjectSet.

- EntityObservableCollection.RemoveItem calls ObjectSet.DeleteObject but it's too late since the observer has already been notified of the event.

Therefore, in the short duration between ObservableCollection.RemoveItem and ObjectSet.DeleteObject being called there is an inconsistency. Unfortunately, a lot can happen within this time since that's when all the event observers get notified.

I realised that both operations need to be completed before the events are published. Once both operations are complete, the suppressed events can be published. Here's my single-thread implementation. It works for my requirements but ideally you would have more of the state attached to the Suppressor object rather than the Collection itself.

    internal class EntityObservableCollection<T> : ObservableCollection<T> where T : class
    {
        private IObjectSet<T> objectSet;

        private bool suppressNotification = false;
        private Collection<NotifyCollectionChangedEventArgs> suppressedNotifications = new Collection<NotifyCollectionChangedEventArgs>();

        /// <summary>
        /// Allows a UnitOfWork to be completed without emitting change events.
        /// 
        /// The suppressed events will be published once the Suppressor is disposed.
        /// </summary>
        private class Suppressor : IDisposable
        {
            private readonly EntityObservableCollection<T> parent;
            public Suppressor(EntityObservableCollection<T> parent)
            {
                this.parent = parent;
                parent.suppressNotification = true;
            }

            public void Dispose()
            {
                parent.suppressNotification = false;

                foreach (var t in parent.suppressedNotifications)
                {
                    parent.OnCollectionChanged(t);
                }
                parent.suppressedNotifications.Clear();
            }
        }

        public EntityObservableCollection(IObjectSet<T> objectSet)
            : base(objectSet)
        {
            this.objectSet = objectSet;
        }

        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (!suppressNotification)
            {
                base.OnCollectionChanged(e);
            }
            else
            {
                suppressedNotifications.Add(e);
            }
        }

        protected override void InsertItem(int index, T item)
        {
            using (new Suppressor(this))
            {
                base.InsertItem(index, item);
                objectSet.AddObject(item);
            }
        }

        protected override void RemoveItem(int index)
        {
            using (new Suppressor(this))
            {
                T itemToDelete = this[index];
                base.RemoveItem(index);
                objectSet.DeleteObject(itemToDelete);
            }
        }

        protected override void ClearItems()
        {
            using (new Suppressor(this))
            {
                T[] itemsToDelete = this.ToArray<T>();
                base.ClearItems();

                foreach (T item in itemsToDelete)
                {
                    objectSet.DeleteObject(item);
                }
            }
        }

        protected override void SetItem(int index, T item)
        {
            using (new Suppressor(this))
            {
                T itemToReplace = this[index];
                base.SetItem(index, item);

                objectSet.DeleteObject(itemToReplace);
                objectSet.AddObject(item);
            }
        }
    }

Coordinator
Mar 18, 2011 at 6:11 PM

Hi Daniel,

Thanks for your feedback. I will think about it.

Greetings
  jbe

Coordinator
Mar 28, 2011 at 7:27 PM

I have changed the implementation of the EntityObservableCollection slightly. The ObjectSet operations are performed before the underlying ObservableCollection is updated.  This might solve your issue.

Best Regards
  jbe

Mar 29, 2011 at 3:56 PM

Our implementations have another side effect. In EntityFramework when an Entity is deleted, all of it's navigation properties are nulled.

1. So when Book is removed, Book.Library == null, Book.BookShelf == null etc...

2. A Librarian is observing the EntityService.Books collection for when Books are permanently removed from the Library because he/she needs to rearrange the BookShelf when this happens:

        private void OnBookCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Remove)
            {
               foreach (Book book in e.OldItems.OfType<Book>())
               {
                    TidyUpBookShelf(book.BookShelf);
               }
            }
        }

3. NullReferenceException is thrown because book.BookShelf is null.

So it is a question of which is the lesser evil because I'm not sure it is possible to have it both ways. Any thoughts?

Coordinator
Mar 31, 2011 at 8:12 PM

That's an interesting side effect. I see the dilemma. I believe that our recent implementations behave more correct because the name CollectionChanged suggests that the Entities have been removed and they might not be valid anymore.

For your scenario with the Book.BookShelf we would need the ObservableCollection also to implement an INotifyCollectionChanging interface which defines an event that occurs before the change has been made.

However, Microsoft has anounced the Entity Framework 4.1 release candidate which provides an alternative to the EntityObservableCollection. Let's see how the ObservableCollection returned by the IDbSet<T>.Local property behaves.

Best Regards
  jbe