I have something here that is really catching me off guard.
I have an ObservableCollection of T that is filled with items. I also have an event handler attached to t
I've found a solution that allows the user to both capitalize on the efficiency of adding or removing many items at a time while only firing one event - and satisfy the needs of UIElements to get the Action.Reset event args while all other users would like a list of elements added and removed.
This solution involves overriding the CollectionChanged event. When we go to fire this event, we can actually look at the target of each registered handler and determine their type. Since only ICollectionView classes require NotifyCollectionChangedAction.Reset
args when more than one item changes, we can single them out, and give everyone else proper event args that contain the full list of items removed or added. Below is the implementation.
public class BaseObservableCollection<T> : ObservableCollection<T>
{
//Flag used to prevent OnCollectionChanged from firing during a bulk operation like Add(IEnumerable<T>) and Clear()
private bool _SuppressCollectionChanged = false;
/// Overridden so that we may manually call registered handlers and differentiate between those that do and don't require Action.Reset args.
public override event NotifyCollectionChangedEventHandler CollectionChanged;
public BaseObservableCollection() : base(){}
public BaseObservableCollection(IEnumerable<T> data) : base(data){}
#region Event Handlers
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if( !_SuppressCollectionChanged )
{
base.OnCollectionChanged(e);
if( CollectionChanged != null )
CollectionChanged.Invoke(this, e);
}
}
//CollectionViews raise an error when they are passed a NotifyCollectionChangedEventArgs that indicates more than
//one element has been added or removed. They prefer to receive a "Action=Reset" notification, but this is not suitable
//for applications in code, so we actually check the type we're notifying on and pass a customized event args.
protected virtual void OnCollectionChangedMultiItem(NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedEventHandler handlers = this.CollectionChanged;
if( handlers != null )
foreach( NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList() )
handler(this, !(handler.Target is ICollectionView) ? e : new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
#endregion
#region Extended Collection Methods
protected override void ClearItems()
{
if( this.Count == 0 ) return;
List<T> removed = new List<T>(this);
_SuppressCollectionChanged = true;
base.ClearItems();
_SuppressCollectionChanged = false;
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
}
public void Add(IEnumerable<T> toAdd)
{
if( this == toAdd )
throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");
_SuppressCollectionChanged = true;
foreach( T item in toAdd )
Add(item);
_SuppressCollectionChanged = false;
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(toAdd)));
}
public void Remove(IEnumerable<T> toRemove)
{
if( this == toRemove )
throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");
_SuppressCollectionChanged = true;
foreach( T item in toRemove )
Remove(item);
_SuppressCollectionChanged = false;
OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(toRemove)));
}
#endregion
}
You can override ClearItems method and raise event with Remove action and OldItems .
public class ObservableCollection<T> : System.Collections.ObjectModel.ObservableCollection<T>
{
protected override void ClearItems()
{
CheckReentrancy();
var items = Items.ToList();
base.ClearItems();
OnPropertyChanged(new PropertyChangedEventArgs("Count"));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, items, -1));
}
}
Part of System.Collections.ObjectModel.ObservableCollection<T>
realization:
public class ObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
protected override void ClearItems()
{
CheckReentrancy();
base.ClearItems();
OnPropertyChanged(CountString);
OnPropertyChanged(IndexerName);
OnCollectionReset();
}
private void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
private void OnCollectionReset()
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private const string CountString = "Count";
private const string IndexerName = "Item[]";
}
The ObservableCollection as well as the INotifyCollectionChanged interface are clearly written with a specific use in mind: UI building and its specific performance characteristics.
When you want notifications of collection changes then you are generally only interested in Add and Remove events.
I use the following interface:
using System;
using System.Collections.Generic;
/// <summary>
/// Notifies listeners of the following situations:
/// <list type="bullet">
/// <item>Elements have been added.</item>
/// <item>Elements are about to be removed.</item>
/// </list>
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
interface INotifyCollection<T>
{
/// <summary>
/// Occurs when elements have been added.
/// </summary>
event EventHandler<NotifyCollectionEventArgs<T>> Added;
/// <summary>
/// Occurs when elements are about to be removed.
/// </summary>
event EventHandler<NotifyCollectionEventArgs<T>> Removing;
}
/// <summary>
/// Provides data for the NotifyCollection event.
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
public class NotifyCollectionEventArgs<T> : EventArgs
{
/// <summary>
/// Gets or sets the elements.
/// </summary>
/// <value>The elements.</value>
public IEnumerable<T> Items
{
get;
set;
}
}
I've also written my own overload of Collection where:
Of course, AddRange can be added as well.
Okay, I know this is a very old question but I have come up with a good solution to the issue and thought I would share. This solution takes inspiration from a lot of the great answers here but has the following advantages:
Here is the code:
public static void Clear<T>(this ObservableCollection<T> collection, Action<ObservableCollection<T>> unhookAction)
{
unhookAction.Invoke(collection);
collection.Clear();
}
This extension method simply takes an Action
which will be invoked before the collection is cleared.
This is how ObservableCollection works, you can work around this by keeping your own list outside of the ObservableCollection (adding to the list when action is Add, remove when action is Remove etc.) then you can get all the removed items (or added items) when action is Reset by comparing your list with the ObservableCollection.
Another option is to create your own class that implements IList and INotifyCollectionChanged, then you can attach and detach events from within that class (or set OldItems on Clear if you like) - it's really not difficult, but it is a lot of typing.
Looking at the NotifyCollectionChangedEventArgs, it appears that OldItems only contains items changed as a result of Replace, Remove, or Move action. It doesn't indicate that it will contain anything on Clear. I suspect that Clear fires the event, but does not registered the removed items and does not invoke the Remove code at all.