I have written the following class which implements(or tries to!) a dictionary with notifications:
public partial class ObservableDictionary
As Ignatio and Matthew pointed out in this answer, only raising the Reset
collection change notification is incorrect, and not very useful if the caller needs to know what actually changed. Fortunately it is easily corrected. Note this version uses Send
instead of Post
as Nathan mentioned in the earlier answer, because WPF is sensitive to reporting back the correct index upon removal, and getting it wrong yields this confusing exception. (Caveat emptor: I'm still not completely convinced the reported index will be fully reliable if there are many overlapping changes, especially given that Dictionaries should be treated as un-ordered.)
//--------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// File: ObservableConcurrentDictionary.cs
//
//--------------------------------------------------------------------------
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Threading;
using System.Diagnostics;
namespace System.Collections.Concurrent
{
///
/// Provides a thread-safe dictionary for use with data binding.
///
/// Specifies the type of the keys in this collection.
/// Specifies the type of the values in this collection.
[DebuggerDisplay("Count={Count}")]
public class ObservableConcurrentDictionary :
ICollection>, IDictionary,
INotifyCollectionChanged, INotifyPropertyChanged
{
private readonly SynchronizationContext _context;
private readonly ConcurrentDictionary _dictionary;
///
/// Initializes an instance of the ObservableConcurrentDictionary class.
///
public ObservableConcurrentDictionary()
{
_context = AsyncOperationManager.SynchronizationContext;
_dictionary = new ConcurrentDictionary();
}
/// Event raised when the collection changes.
public event NotifyCollectionChangedEventHandler CollectionChanged;
/// Event raised when a property on the collection changes.
public event PropertyChangedEventHandler PropertyChanged;
///
/// Notifies observers of CollectionChanged or PropertyChanged of an update to the dictionary.
///
private void NotifyObserversOfChange()
{
var collectionHandler = CollectionChanged;
var propertyHandler = PropertyChanged;
if (collectionHandler != null || propertyHandler != null)
{
_context.Send(s =>
{
collectionHandler?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
propertyHandler?.Invoke(this, new PropertyChangedEventArgs("Count"));
propertyHandler?.Invoke(this, new PropertyChangedEventArgs("Keys"));
propertyHandler?.Invoke(this, new PropertyChangedEventArgs("Values"));
}, null);
}
}
///
/// Notifies observers of CollectionChanged or PropertyChanged of an update to the dictionary.
///
/// Add or Update action
/// The item involved with the change
private void NotifyObserversOfChange(NotifyCollectionChangedAction actionType, object changedItem)
{
var collectionHandler = CollectionChanged;
var propertyHandler = PropertyChanged;
if (collectionHandler != null || propertyHandler != null)
{
_context.Send(s =>
{
collectionHandler?.Invoke(this, new NotifyCollectionChangedEventArgs(actionType, changedItem));
propertyHandler?.Invoke(this, new PropertyChangedEventArgs("Count"));
propertyHandler?.Invoke(this, new PropertyChangedEventArgs("Keys"));
propertyHandler?.Invoke(this, new PropertyChangedEventArgs("Values"));
}, null);
}
}
///
/// Notifies observers of CollectionChanged or PropertyChanged of an update to the dictionary.
///
/// Remove action or optionally an Add action
/// The item in question
/// The position of the item in the collection
private void NotifyObserversOfChange(NotifyCollectionChangedAction actionType, object item, int index)
{
var collectionHandler = CollectionChanged;
var propertyHandler = PropertyChanged;
if (collectionHandler != null || propertyHandler != null)
{
_context.Send(s =>
{
collectionHandler?.Invoke(this, new NotifyCollectionChangedEventArgs(actionType, item, index));
propertyHandler?.Invoke(this, new PropertyChangedEventArgs("Count"));
propertyHandler?.Invoke(this, new PropertyChangedEventArgs("Keys"));
propertyHandler?.Invoke(this, new PropertyChangedEventArgs("Values"));
}, null);
}
}
/// Attempts to add an item to the dictionary, notifying observers of any changes.
/// The item to be added.
/// Whether the add was successful.
private bool TryAddWithNotification(KeyValuePair item)
=> TryAddWithNotification(item.Key, item.Value);
/// Attempts to add an item to the dictionary, notifying observers of any changes.
/// The key of the item to be added.
/// The value of the item to be added.
/// Whether the add was successful.
private bool TryAddWithNotification(TKey key, TValue value)
{
bool result = _dictionary.TryAdd(key, value);
int index = IndexOf(key);
if (result) NotifyObserversOfChange(NotifyCollectionChangedAction.Add, value, index);
return result;
}
/// Attempts to remove an item from the dictionary, notifying observers of any changes.
/// The key of the item to be removed.
/// The value of the item removed.
/// Whether the removal was successful.
private bool TryRemoveWithNotification(TKey key, out TValue value)
{
int index = IndexOf(key);
bool result = _dictionary.TryRemove(key, out value);
if (result) NotifyObserversOfChange(NotifyCollectionChangedAction.Remove, value, index);
return result;
}
/// Attempts to add or update an item in the dictionary, notifying observers of any changes.
/// The key of the item to be updated.
/// The new value to set for the item.
/// Whether the update was successful.
private void UpdateWithNotification(TKey key, TValue value)
{
_dictionary[key] = value;
NotifyObserversOfChange(NotifyCollectionChangedAction.Replace, value);
}
///
/// WPF requires that the reported index for Add/Remove events are correct/reliable. With a dictionary there
/// is no choice but to brute-force search through the key list. Ugly.
///
private int IndexOf(TKey key)
{
var keys = _dictionary.Keys;
int index = -1;
foreach(TKey k in keys)
{
index++;
if (k.Equals(key)) return index;
}
return -1;
}
// ICollection> Members
void ICollection>.Add(KeyValuePair item)
=> TryAddWithNotification(item);
void ICollection>.Clear()
{
_dictionary.Clear();
NotifyObserversOfChange();
}
bool ICollection>.Contains(KeyValuePair item)
=> ((ICollection>)_dictionary).Contains(item);
void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex)
=> ((ICollection>)_dictionary).CopyTo(array, arrayIndex);
int ICollection>.Count
{
get => _dictionary.Count;
}
bool ICollection>.IsReadOnly
{
get => ((ICollection>)_dictionary).IsReadOnly;
}
bool ICollection>.Remove(KeyValuePair item)
=> TryRemoveWithNotification(item.Key, out TValue temp);
// IEnumerable> Members
IEnumerator> IEnumerable>.GetEnumerator()
=> _dictionary.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> _dictionary.GetEnumerator();
// IDictionary Members
public void Add(TKey key, TValue value)
=> TryAddWithNotification(key, value);
public bool ContainsKey(TKey key)
=> _dictionary.ContainsKey(key);
public ICollection Keys
{
get { return _dictionary.Keys; }
}
public bool Remove(TKey key)
=> TryRemoveWithNotification(key, out TValue temp);
public bool TryGetValue(TKey key, out TValue value)
=> _dictionary.TryGetValue(key, out value);
public ICollection Values
{
get => _dictionary.Values;
}
public TValue this[TKey key]
{
get => _dictionary[key];
set => UpdateWithNotification(key, value);
}
}
}