Thread-Safe Observable List for WPF

I’m probably posting this too early; I haven’t had a chance to extensively test it yet but I basically just locked every function down, and made any method that actually modifies the list run on the main thread so that notifications can be sent. It ought to work 🙂

class ObservableList<T> : IList<T>, INotifyCollectionChanged where T : INotifyPropertyChanged
{
    private Dispatcher dispatcher;
    private List<T> list;
    private object sync;

    public ObservableList(Dispatcher dispatcher = null)
    {
        this.dispatcher = dispatcher ?? Dispatcher.CurrentDispatcher;
        this.list = new List<T>();
        this.sync = new object();
    }

    public event NotifyCollectionChangedEventHandler CollectionChanged;

    public virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        lock (sync)
        {
            if (CollectionChanged != null)
            {
                CollectionChanged(this, e);
            }
        }
    }

    public int IndexOf(T item)
    {
        lock (sync)
        {
            return list.IndexOf(item);
        }
    }

    public void Insert(int index, T item)
    {
        if (dispatcher.CheckAccess())
        {
            lock (sync)
            {
                list.Insert(index, item);
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
            }
        }
        else
        {
            dispatcher.Invoke(new Action<int, T>(Insert), DispatcherPriority.Send, index, item);
        }
    }

    public void RemoveAt(int index)
    {
        if (dispatcher.CheckAccess())
        {
            lock (sync)
            {
                var item = list[index];
                list.RemoveAt(index);
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
            }
        }
        else
        {
            dispatcher.Invoke(new Action<int>(RemoveAt), DispatcherPriority.Send, index);
        }
    }

    public T this[int index]
    {
        get
        {
            lock (sync) { return list[index]; }
        }
        set
        {
            lock (sync) { list[index] = value; }
        }
    }

    public void Add(T item)
    {
        if (dispatcher.CheckAccess())
        {
            lock (sync)
            {
                list.Add(item);
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
            }
        }
        else
        {
            dispatcher.Invoke(new Action<T>(Add), DispatcherPriority.Send, item);
        }
    }

    public void Clear()
    {
        if (dispatcher.CheckAccess())
        {
            lock (sync)
            {
                list.Clear();
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            }
        }
        else
        {
            dispatcher.Invoke(new Action(Clear), DispatcherPriority.Send);
        }
    }

    public bool Contains(T item)
    {
        lock (sync) { return list.Contains(item); }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        lock (sync) { list.CopyTo(array, arrayIndex); }
    }

    public int Count
    {
        get { lock (sync) { return list.Count; } }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }

    public bool Remove(T item)
    {
        if (dispatcher.CheckAccess())
        {
            lock (sync)
            {
                var index = list.IndexOf(item);
                var result = list.Remove(item);
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
                return result;
            }
        }
        else
        {
            return (bool)dispatcher.Invoke(new Func<T, bool>(Remove), DispatcherPriority.Send, item);
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        lock (sync)
        {
            return list.GetEnumerator();
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

You can simply delete the “where T : INotifyPropertyChanged” if you don’t like that restriction, but I put it there so that you don’t forget that your objects should notify your controls that their properties have changed, so that the GUI gets refreshed properly.

Posted in

2 thoughts on “Thread-Safe Observable List for WPF

  1. Didn’t know about DispatcherObject, but from what I’m reading about it, you could definitely derive from it but the logic would still be identical.

Leave a Reply

Your email address will not be published. Required fields are marked *