19

平台:WPF, .NET 4.0, C# 4.0.NET 4中是否有Threadsafe Observable集合?

问题:在Mainwindow.xaml我已绑定到客户收集这是目前一个ObservableCollection <顾客>列表框。

ObservableCollection<Customer> c = new ObservableCollection<Customer>();

这个系列可以通过多种来源进行更新,如文件系统,web服务等

,让客户的并行加载我创建了一个辅助类

public class CustomerManager(ref ObsevableCollection<Customer> cust)

那在内部为每个客户Source产生一个新的Task(来自并行扩展库),并向客户集合对象添加一个新的Customer实例(通过re f到它的ctor)。

的问题是,的ObservableCollection < T>(或与此有关的任何集合)不能从除UI线程之外调用中使用,并在遇到异常:

“NotSupportedException异常 - 这种类型的CollectionView不支持从与分派器线程不同的线程更改为其SourceCollection。“

我尝试使用

System.Collections.Concurrent.ConcurrentBag<Customer>

集合,但它亘古不变的实现INotifyCollectionChanged接口。因此我的WPF UI不会自动更新。

那么,是否有一个集合类实现了属性/集合更改通知,并且还允许来自其他非UI线程的调用?

通过我最初的Bing /谷歌搜索,没有提供任何开箱即用。

编辑:我创建了自己的集合,从ConcurrentBag <客户>继承,也实现了INotifyCollectionChanged接口。但令我惊讶的是,即使在单独的任务中调用它之后,WPF UI也会挂起,直到任务完成。 是不是应该并行执行任务,而不是阻止UI线程

感谢您的任何建议,提前。

+0

您可以尝试在执行[这个博客帖子(http://www.deanchalk.me.uk/post/Thread-Safe-Dispatcher-Safe-Observable-Collection-for-WPF.aspx) 。 – 2010-10-10 10:38:26

+0

谢谢。看起来不错。尝试一下! – Vaibhav 2010-10-10 10:44:32

+0

嗨达林,我使用了博客文章中提到的集合类。正如我的问题所述,我能够通过创建自己的集合类来合并并发和通知更改,继承和实现所需的类/接口,但UI被阻止。 使用任务的主要原因是具有UI响应。 那还不行!非常感谢您的建议。 – Vaibhav 2010-10-10 10:56:13

回答

3

请大家看看​​3210从Caliburn.Micro库:

/// <summary> 
/// A base collection class that supports automatic UI thread marshalling. 
/// </summary> 
/// <typeparam name="T">The type of elements contained in the collection.</typeparam> 
#if !SILVERLIGHT && !WinRT 
[Serializable] 
#endif 
public class BindableCollection<T> : ObservableCollection<T>, IObservableCollection<T> { 

    /// <summary> 
    /// Initializes a new instance of the <see cref = "Caliburn.Micro.BindableCollection{T}" /> class. 
    /// </summary> 
    public BindableCollection() { 
     IsNotifying = true; 
    } 

    /// <summary> 
    /// Initializes a new instance of the <see cref = "Caliburn.Micro.BindableCollection{T}" /> class. 
    /// </summary> 
    /// <param name = "collection">The collection from which the elements are copied.</param> 
    /// <exception cref = "T:System.ArgumentNullException"> 
    /// The <paramref name = "collection" /> parameter cannot be null. 
    /// </exception> 
    public BindableCollection(IEnumerable<T> collection) : base(collection) { 
     IsNotifying = true; 
    } 

#if !SILVERLIGHT && !WinRT 
    [field: NonSerialized] 
#endif 
    bool isNotifying; //serializator try to serialize even autogenerated fields 

    /// <summary> 
    /// Enables/Disables property change notification. 
    /// </summary> 
#if !WinRT 
    [Browsable(false)] 
#endif 
    public bool IsNotifying { 
     get { return isNotifying; } 
     set { isNotifying = value; } 
    } 

    /// <summary> 
    /// Notifies subscribers of the property change. 
    /// </summary> 
    /// <param name = "propertyName">Name of the property.</param> 
#if WinRT || NET45 
    public virtual void NotifyOfPropertyChange([CallerMemberName]string propertyName = "") { 
#else 
    public virtual void NotifyOfPropertyChange(string propertyName) { 
#endif 
     if(IsNotifying) 
      Execute.OnUIThread(() => OnPropertyChanged(new PropertyChangedEventArgs(propertyName))); 
    } 

    /// <summary> 
    /// Raises a change notification indicating that all bindings should be refreshed. 
    /// </summary> 
    public void Refresh() { 
     Execute.OnUIThread(() => { 
      OnPropertyChanged(new PropertyChangedEventArgs("Count")); 
      OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); 
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 
     }); 
    } 

    /// <summary> 
    /// Inserts the item to the specified position. 
    /// </summary> 
    /// <param name = "index">The index to insert at.</param> 
    /// <param name = "item">The item to be inserted.</param> 
    protected override sealed void InsertItem(int index, T item) { 
     Execute.OnUIThread(() => InsertItemBase(index, item)); 
    } 

    /// <summary> 
    /// Exposes the base implementation of the <see cref = "InsertItem" /> function. 
    /// </summary> 
    /// <param name = "index">The index.</param> 
    /// <param name = "item">The item.</param> 
    /// <remarks> 
    /// Used to avoid compiler warning regarding unverifiable code. 
    /// </remarks> 
    protected virtual void InsertItemBase(int index, T item) { 
     base.InsertItem(index, item); 
    } 

#if NET || WP8 || WinRT 
/// <summary> 
/// Moves the item within the collection. 
/// </summary> 
/// <param name="oldIndex">The old position of the item.</param> 
/// <param name="newIndex">The new position of the item.</param> 
    protected sealed override void MoveItem(int oldIndex, int newIndex) { 
     Execute.OnUIThread(() => MoveItemBase(oldIndex, newIndex)); 
    } 

    /// <summary> 
    /// Exposes the base implementation fo the <see cref="MoveItem"/> function. 
    /// </summary> 
    /// <param name="oldIndex">The old index.</param> 
    /// <param name="newIndex">The new index.</param> 
    /// <remarks>Used to avoid compiler warning regarding unverificable code.</remarks> 
    protected virtual void MoveItemBase(int oldIndex, int newIndex) { 
     base.MoveItem(oldIndex, newIndex); 
    } 
#endif 

    /// <summary> 
    /// Sets the item at the specified position. 
    /// </summary> 
    /// <param name = "index">The index to set the item at.</param> 
    /// <param name = "item">The item to set.</param> 
    protected override sealed void SetItem(int index, T item) { 
     Execute.OnUIThread(() => SetItemBase(index, item)); 
    } 

    /// <summary> 
    /// Exposes the base implementation of the <see cref = "SetItem" /> function. 
    /// </summary> 
    /// <param name = "index">The index.</param> 
    /// <param name = "item">The item.</param> 
    /// <remarks> 
    /// Used to avoid compiler warning regarding unverifiable code. 
    /// </remarks> 
    protected virtual void SetItemBase(int index, T item) { 
     base.SetItem(index, item); 
    } 

    /// <summary> 
    /// Removes the item at the specified position. 
    /// </summary> 
    /// <param name = "index">The position used to identify the item to remove.</param> 
    protected override sealed void RemoveItem(int index) { 
     Execute.OnUIThread(() => RemoveItemBase(index)); 
    } 

    /// <summary> 
    /// Exposes the base implementation of the <see cref = "RemoveItem" /> function. 
    /// </summary> 
    /// <param name = "index">The index.</param> 
    /// <remarks> 
    /// Used to avoid compiler warning regarding unverifiable code. 
    /// </remarks> 
    protected virtual void RemoveItemBase(int index) { 
     base.RemoveItem(index); 
    } 

    /// <summary> 
    /// Clears the items contained by the collection. 
    /// </summary> 
    protected override sealed void ClearItems() { 
     Execute.OnUIThread(ClearItemsBase); 
    } 

    /// <summary> 
    /// Exposes the base implementation of the <see cref = "ClearItems" /> function. 
    /// </summary> 
    /// <remarks> 
    /// Used to avoid compiler warning regarding unverifiable code. 
    /// </remarks> 
    protected virtual void ClearItemsBase() { 
     base.ClearItems(); 
    } 

    /// <summary> 
    /// Raises the <see cref = "E:System.Collections.ObjectModel.ObservableCollection`1.CollectionChanged" /> event with the provided arguments. 
    /// </summary> 
    /// <param name = "e">Arguments of the event being raised.</param> 
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { 
     if (IsNotifying) { 
      base.OnCollectionChanged(e); 
     } 
    } 

    /// <summary> 
    /// Raises the PropertyChanged event with the provided arguments. 
    /// </summary> 
    /// <param name = "e">The event data to report in the event.</param> 
    protected override void OnPropertyChanged(PropertyChangedEventArgs e) { 
     if (IsNotifying) { 
      base.OnPropertyChanged(e); 
     } 
    } 

    /// <summary> 
    /// Adds the range. 
    /// </summary> 
    /// <param name = "items">The items.</param> 
    public virtual void AddRange(IEnumerable<T> items) { 
     Execute.OnUIThread(() => { 
      var previousNotificationSetting = IsNotifying; 
      IsNotifying = false; 
      var index = Count; 
      foreach(var item in items) { 
       InsertItemBase(index, item); 
       index++; 
      } 
      IsNotifying = previousNotificationSetting; 

      OnPropertyChanged(new PropertyChangedEventArgs("Count")); 
      OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); 
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 
     }); 
    } 

    /// <summary> 
    /// Removes the range. 
    /// </summary> 
    /// <param name = "items">The items.</param> 
    public virtual void RemoveRange(IEnumerable<T> items) { 
     Execute.OnUIThread(() => { 
      var previousNotificationSetting = IsNotifying; 
      IsNotifying = false; 
      foreach(var item in items) { 
       var index = IndexOf(item); 
       if (index >= 0) { 
        RemoveItemBase(index); 
       } 
      } 
      IsNotifying = previousNotificationSetting; 

      OnPropertyChanged(new PropertyChangedEventArgs("Count")); 
      OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); 
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 
     }); 
    } 

    /// <summary> 
    /// Called when the object is deserialized. 
    /// </summary> 
    /// <param name="c">The streaming context.</param> 
    [OnDeserialized] 
    public void OnDeserialized(StreamingContext c) { 
     IsNotifying = true; 
    } 

    /// <summary> 
    /// Used to indicate whether or not the IsNotifying property is serialized to Xml. 
    /// </summary> 
    /// <returns>Whether or not to serialize the IsNotifying property. The default is false.</returns> 
    public virtual bool ShouldSerializeIsNotifying() { 
     return false; 
    } 
} 

Source

PS。请记住,该类使用Caliburn.Micro中的其他类,以便您可以通过自己的方式复制/处理所有依赖关系 - 如果不使用任何其他应用程序框架,只需引用库二进制文件并给它一个机会。

+0

_despite_使用其他框架,只需从另一个包中使用任何你想要的类,没错? – heltonbiker 2017-01-05 15:50:36

+1

'BindableCollection'不是线程安全的。它覆盖了'InsertItem'和'RemoveItem'方法,但是在调用这些虚拟方法之前,公共方法在调用线程上使用其他方法(如“IndexOf”或“Count”)。 – 2017-07-25 07:01:16

+0

好点,@YusufTarıkGünaydın,谢谢! – Sevenate 2017-07-25 22:21:03

6

有两种可能的方法。第一种方法是从并发集合继承并添加INotifyCollectionChanged功能,第二种方法是从实现INotifyCollectionChanged的集合继承并添加并发支持。我认为将INotifyCollectionChanged支持添加到并发集合中变得更加简单和安全。我的建议如下。

它看起来长,但大部分的方法,如主叫方直接使用它只需调用内部并发收集。少量添加或从集合中删除的方法将一个调用注入到一个私有方法中,该方法在构造时提供的调度程序中引发通知事件,从而允许该类是线程安全的,但确保通知在同一线程中引发时间。

using System; 
using System.Collections; 
using System.Collections.Concurrent; 
using System.Collections.Generic; 
using System.Collections.Specialized; 
using System.Runtime.InteropServices; 
using System.Security.Permissions; 
using System.Windows.Threading; 

namespace Collections 
{ 
    /// <summary> 
    /// Concurrent collection that emits change notifications on a dispatcher thread 
    /// </summary> 
    /// <typeparam name="T">The type of objects in the collection</typeparam> 
    [Serializable] 
    [ComVisible(false)] 
    [HostProtection(SecurityAction.LinkDemand, Synchronization = true, ExternalThreading = true)] 
    public class ObservableConcurrentBag<T> : IProducerConsumerCollection<T>, 
     IEnumerable<T>, ICollection, IEnumerable 
    { 
     /// <summary> 
     /// The dispatcher on which event notifications will be raised 
     /// </summary> 
     private readonly Dispatcher dispatcher; 

     /// <summary> 
     /// The internal concurrent bag used for the 'heavy lifting' of the collection implementation 
     /// </summary> 
     private readonly ConcurrentBag<T> internalBag; 

     /// <summary> 
     /// Initializes a new instance of the ConcurrentBag<T> class that will raise <see cref="INotifyCollectionChanged"/> events 
     /// on the specified dispatcher 
     /// </summary> 
     public ObservableConcurrentBag(Dispatcher dispatcher) 
     { 
      this.dispatcher = dispatcher; 
      this.internalBag = new ConcurrentBag<T>(); 
     } 

     /// <summary> 
     /// Initializes a new instance of the ConcurrentBag<T> class that contains elements copied from the specified collection 
     /// that will raise <see cref="INotifyCollectionChanged"/> events on the specified dispatcher 
     /// </summary> 
     public ObservableConcurrentBag(Dispatcher dispatcher, IEnumerable<T> collection) 
     { 
      this.dispatcher = dispatcher; 
      this.internalBag = new ConcurrentBag<T>(collection); 
     } 

     /// <summary> 
     /// Occurs when the collection changes 
     /// </summary> 
     public event NotifyCollectionChangedEventHandler CollectionChanged; 

     /// <summary> 
     /// Raises the <see cref="CollectionChanged"/> event on the <see cref="dispatcher"/> 
     /// </summary> 
     private void RaiseCollectionChangedEventOnDispatcher(NotifyCollectionChangedEventArgs e) 
     { 
      this.dispatcher.BeginInvoke(new Action<NotifyCollectionChangedEventArgs>(this.RaiseCollectionChangedEvent), e); 
     } 

     /// <summary> 
     /// Raises the <see cref="CollectionChanged"/> event 
     /// </summary> 
     /// <remarks> 
     /// This method must only be raised on the dispatcher - use <see cref="RaiseCollectionChangedEventOnDispatcher" /> 
     /// to do this. 
     /// </remarks> 
     private void RaiseCollectionChangedEvent(NotifyCollectionChangedEventArgs e) 
     { 
      this.CollectionChanged(this, e); 
     } 

     #region Members that pass through to the internal concurrent bag but also raise change notifications 

     bool IProducerConsumerCollection<T>.TryAdd(T item) 
     { 
      bool result = ((IProducerConsumerCollection<T>)this.internalBag).TryAdd(item); 
      if (result) 
      { 
       this.RaiseCollectionChangedEventOnDispatcher(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); 
      } 
      return result; 
     } 

     public void Add(T item) 
     { 
      this.internalBag.Add(item); 
      this.RaiseCollectionChangedEventOnDispatcher(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); 
     } 

     public bool TryTake(out T item) 
     { 
      bool result = this.TryTake(out item); 
      if (result) 
      { 
       this.RaiseCollectionChangedEventOnDispatcher(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item)); 
      } 
      return result; 
     } 

     #endregion 

     #region Members that pass through directly to the internal concurrent bag 

     public int Count 
     { 
      get 
      { 
       return this.internalBag.Count; 
      } 
     } 

     public bool IsEmpty 
     { 
      get 
      { 
       return this.internalBag.IsEmpty; 
      } 
     } 

     bool ICollection.IsSynchronized 
     { 
      get 
      { 
       return ((ICollection)this.internalBag).IsSynchronized; 
      } 
     } 

     object ICollection.SyncRoot 
     { 
      get 
      { 
       return ((ICollection)this.internalBag).SyncRoot; 
      } 
     } 

     IEnumerator<T> IEnumerable<T>.GetEnumerator() 
     { 
      return ((IEnumerable<T>)this.internalBag).GetEnumerator(); 
     } 

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

     public T[] ToArray() 
     { 
      return this.internalBag.ToArray(); 
     } 

     void IProducerConsumerCollection<T>.CopyTo(T[] array, int index) 
     { 
      ((IProducerConsumerCollection<T>)this.internalBag).CopyTo(array, index); 
     } 

     void ICollection.CopyTo(Array array, int index) 
     { 
      ((ICollection)this.internalBag).CopyTo(array, index); 
     } 

     #endregion 
    } 
} 
+0

在引发事件时不能使用SynchronizationContext,以便它在同一线程上执行。这将允许集合创建者不仅可以访问该事件。 – galford13x 2013-09-28 02:58:11

0

有详细的说明和实施here。它主要是为.NET 3.5 SP1编写的,但它仍将在4.0中工作。

这个实现的主要目标是当“真正的”列表中存在比它的绑定视图更长(例如,如果在用户能够打开和关闭一个窗口边界)。如果生命周期是另一种方式(例如,你只是在窗口打开时运行的后台工作人员更新列表),那么可以使用一些更简单的设计。

3

我花了年龄在看所有的解决方案,并没有真正适合我需要什么,直到我终于实现了这个问题:我要的不是线程安全的名单 - 我只是想,可以在任意的修改的非线程列表线程,但通知UI线程的变化。

(不需要线程安全集合的原因通常是这样的 - 通常您需要执行多个操作,比如“如果它不在列表中,然后添加它”哪个线程安全列表实际上没有帮助,那么你想自己控制锁定)。

的解决方案竟然是在概念上非常简单,一直运作良好适合我。只需创建一个新的列表类,实现IList<T>INotifyCollectionChanged。将所需的所有调用委托给底层实现(例如,List<T>),然后在需要的UI线程上调用通知。

public class AlbumList : IList<Album>, INotifyCollectionChanged 
{ 
    private readonly IList<Album> _listImplementation = new List<Album>(); 

    public event NotifyCollectionChangedEventHandler CollectionChanged; 

    private void OnChanged(NotifyCollectionChangedEventArgs e) 
    { 
     Application.Current?.Dispatcher.Invoke(DispatcherPriority.Render, 
        new Action(() => CollectionChanged?.Invoke(this, e))); 
    } 

    public void Add(Album item) 
    { 
     _listImplementation.Add(item); 
     OnChanged(new NotifyCollectionChangedEventArgs(
         NotifyCollectionChangedAction.Add, item)); 
    } 

    public bool Remove(Album item) 
    { 
     int index = _listImplementation.IndexOf(item); 
     var removed = index >= 0; 
     if (removed) 
     { 
      _listImplementation.RemoveAt(index); 
      OnChanged(new NotifyCollectionChangedEventArgs(
          NotifyCollectionChangedAction.Remove, item, index)); 
     } 
     return removed; 
    } 
    // ...snip... 
}