ObservableCollection… better
ObservableCollection class is probably one of the worst classes I have ever seen in the .NET fx:
- Some current collection methods are missing: (AddRange for example)
- Bad for performance when we Refresh the collection (1 Clear + n Add) => n+1 events sent to UI to refresh
- The Clear event “forgets” to give us the removed items even if we have an OldItems property in the event arg.
This class is a spot in our great .NET framework.
I wanted to redefine it but to do it quickly, I only inherited it.
First, the Reset issue.
With WPF or SL, when you use a collection which implements INotifyCollectionChanged , UI automatically adds hander to CollectionChanged event.
So, we will implement explicitly the interface to manage the event ourselves.
public new event NotifyCollectionChangedEventHandler CollectionChanged;
event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
{
add { CollectionChanged += value; }
remove { CollectionChanged -= value; }
}
Then, we will add two methods: BeginEdit and EndEdit
public bool IsEditing { get; private set; }
public void BeginEdit()
{
if (! IsEditing)
IsEditing = true;
}
public void EndEdit()
{
if (IsEditing)
{
IsEditing = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
if (CollectionChanged != null && ! IsEditing)
CollectionChanged(this, e);
}
Now, when we want to refresh the collection, we can call BeginEdit, Clear then the AddRange we will make now and the EndEdit.
public void AddRange(IEnumerable<T> items)
{
bool isEditing = IsEditing;
IsEditing = true;
foreach (T item in items)
Add(item);
IsEditing = isEditing;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, items.ToList()));
}
Finally, we will stop the Clear Reset event in order to send an event with deleted items. However, we want to keep a Reset action (better for UI than a Remove). The problem here is the fact that NotifyCollectionChangedEventArgs doesn’t allow us to set OldItems with Reset Action.
So I will code my own MyNotifyCollectionChangedEventArgs class which inherits NotifyCollectionChangedEventArgs. The issue here is the fact that we can’t change OldItems (no protected set or virtual get). Just a note, its type is IList which is a shame. IEnumerable would be better.
To resolve OldItems issue, we have three possibilities: you accept the risk that NotifyCollectionChangedEventArgs can change and you use reflection to change the private field value (bad):
public class MyNotifyCollectionChangedEventArgs : NotifyCollectionChangedEventArgs
{
public MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action)
: base(action)
{
}
public MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList items)
: base(action, items)
{
}
public new IList OldItems
{
get { return base.OldItems; }
set
{
typeof(NotifyCollectionChangedEventArgs).GetField("_oldItems", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(this, value);
}
}
}
You ask developers to cast the event arg if they want to have the OldItems.
Or you do both but you can guarantee only with the Cast:
public class MyNotifyCollectionChangedEventArgs : NotifyCollectionChangedEventArgs
{
public MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action)
: base(action)
{
}
public MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction action, IList items)
: base(action, items)
{
_oldItems = base.OldItems;
}
private IList _oldItems;
public new IList OldItems
{
get { return _oldItems; }
set
{
_oldItems = value;
try
{
typeof(NotifyCollectionChangedEventArgs).GetField("_oldItems", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(this, value);
}
catch
{
}
}
}
}
Now, we can override ClearItems method:
protected override void ClearItems()
{
var removedItems = this.ToList();
bool isEditing = IsEditing;
IsEditing = true;
base.ClearItems();
IsEditing = isEditing;
OnCollectionChanged(new MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset) { OldItems = removedItems });
}
It’s all folks! Our MyObservableCollection<T> class will be faster and more practicable:
public class MyObservableCollection<T> : ObservableCollection<T>, INotifyCollectionChanged
{
public MyObservableCollection()
{
}
public MyObservableCollection(IEnumerable<T> items)
:base(items)
{
}
public void AddRange(IEnumerable<T> items)
{
bool isEditing = IsEditing;
IsEditing = true;
foreach (T item in items)
Add(item);
IsEditing = isEditing;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, items.ToList()));
}
protected override void ClearItems()
{
var removedItems = this.ToList();
bool isEditing = IsEditing;
IsEditing = true;
base.ClearItems();
IsEditing = isEditing;
OnCollectionChanged(new MyNotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset) { OldItems = removedItems });
}
public bool IsEditing { get; private set; }
public void BeginEdit()
{
if (! IsEditing)
IsEditing = true;
}
public void EndEdit()
{
if (IsEditing)
{
IsEditing = false;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
if (CollectionChanged != null && ! IsEditing)
CollectionChanged(this, e);
}
public new event NotifyCollectionChangedEventHandler CollectionChanged;
event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
{
add { CollectionChanged += value; }
remove { CollectionChanged -= value; }
}
}