更新线程中列表框中的ObservableCollection

时间:2014-03-19 17:20:53

标签: c# .net wpf multithreading thread-safety

HY,

我有一个Observable Collection,它与列表框绑定。我将日志添加到Observable Collection。我总是立即将消息添加到Observable Collecten。但是这个列表只在循环结束时才更新,但我想在for循环中添加一个项目时更新它。这就是我使用Thread但我遇到一些问题的原因。

我有一个线程安全的ObservableCollection:

class ThreadSafeObservableCollection<T> : ObservableCollection<T>
{
    public override event NotifyCollectionChangedEventHandler CollectionChanged;
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        NotifyCollectionChangedEventHandler collectionChanged = this.CollectionChanged;
        if (collectionChanged != null)
            foreach (NotifyCollectionChangedEventHandler handler in collectionChanged.GetInvocationList())
            {
                DispatcherObject dispatcherObject = handler.Target as DispatcherObject;
                if (dispatcherObject != null)
                {
                    Dispatcher dispatcher = dispatcherObject.Dispatcher;
                    if (dispatcher != null && !dispatcher.CheckAccess())
                    {
                        dispatcher.BeginInvoke(
                            (Action)(() => handler.Invoke(this,
                                new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))),
                            DispatcherPriority.DataBind);
                        continue;
                    }
                }
                handler.Invoke(this, e);
            }
    }
}

这是我的测试类:

public partial class MainWindow : Window
{
    ThreadSafeObservableCollection<Animal> list = new ThreadSafeObservableCollection<Animal>();
    public MainWindow()
    {
        InitializeComponent();
        list.Add(new Animal() { Name = "test1" });
        list.Add(new Animal() { Name = "test2" });
        this.DataContext = list;
    }

    private void dsofsdkfd(object sender, RoutedEventArgs e)
    {
        //Version 1
        Task.Factory.StartNew(() => test());

        //Version2
        /*
        var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
        var token = Task.Factory.CancellationToken;

        Task.Factory.StartNew(() => test(), token, TaskCreationOptions.None, uiScheduler);
        */
    }

    public void test()
    {
        for (int i = 0; i < 10000; i++)
        {
            list.Add(new Animal() { Name = "test" + i });
            System.Threading.Thread.Sleep(1);
        }
    }
}

查看评论 Version1 private void dsofsdkfd(object sender,RoutedEventArgs e)功能。 一开始它可以工作,所以每次添加项目时列表都会更新。在几个条目之后我得到一个例外:

  

“开发人员的信息”(使用Text Visualizer阅读   这个):\ r \ n这个异常被抛出,因为生成器用于控制   'System.Windows.Controls.ListBox Items.Count:1089',名称为'Logger'   收到了不同意的CollectionChanged事件序列   与Items集合的当前状态。下列   检测到差异:\ r \ n累计计数994不同   从实际计数1089. [累计计数是(最后重置计数+

     

添加 - 自上次重置后#Removes。)\ r \ n \ r \ n以下一个或多个来源可能引发了错误的事件:\ r \ n

     

System.Windows.Controls.ItemContainerGenerator \ r \ n
  System.Windows.Controls.ItemCollection \ r \ n
  System.Windows.Data.ListCollectionView \ r \ n *
  WpfApplication1.ThreadSafeObservableCollection`1 [WpfApplication1.Animal,   WpfApplication1,Version = 1.0.0.0,Culture = neutral,   PublicKeyToken = null]] \ r \ n(已加星标的来源被认为更多   可能是导致问题的原因。)\ r \ n \ r \ n最常见的原因   是(a)更改集合或其计数而不提高   相应的事件,以及(b)提出索引不正确的事件   或项目参数。\ r \ n \ r \ n例外的堆栈跟踪描述了如何   检测到不一致,而不是它们是如何发生的。得到一个   更及时的例外,设置附加属性   生成器上的'PresentationTraceSources.TraceLevel'值为'High'   并重新运行该方案。一种方法是运行命令   类似如下:\ n
  System.Diagnostics.PresentationTraceSources.SetTraceLevel(myItemsControl.ItemContainerGenerator,   System.Diagnostics.PresentationTraceLevel.High)\ r \ n来自Immediate   窗口。这导致检测逻辑在每次运行之后运行   CollectionChanged事件,因此它会降低应用程序的速度。\ r \ n“


请参阅版本2 private void dsofsdkfd(对象发件人,RoutedEventArgs e)功能。 我还使用FromCurrentSynchronizationContext与TaskScheduler一起尝试了它。

然后它不会抛出任何异常,但是我在开始时遇到了同样的问题,所以只有当for循环结束时,列表框才会刷新。

如何在添加元素时更新列表框?

祝你好运

2 个答案:

答案 0 :(得分:1)

我不会为此推出自己的ObservableCollection。我只是在UI线程上执行.Add调用。

    public void test()
    {
        for (var i = 0; i < 10000; i++)
        {
            // create object
            var animal = new Animal {Name = "test" + i};

            // invoke list.Add on the UI thread
            this.Dispatcher.Invoke(new Action(() => list.Add(animal)));

            // sleep
            System.Threading.Thread.Sleep(1);
        }
    }

请注意,由于您位于Window子类中,this.Dispatcher将对应于UI线程的调度程序。如果将此逻辑移动到模型或视图模型类,则需要在UI线程上显式捕获Dispatcher.Current的值,并将该调度程序手动传递给后台线程。

编辑:OP要求提供有关在FrameworkElement类之外使用Dispatcher的更多信息。这是你如何做到这一点。通过调用Dispatcher.CurrentDispatcher在UI线程上获取UI线程的调度程序。然后,该调度程序直接传递给后台线程程序。

public class MainWindowViewModel
{
    // this should be called on the UI thread
    public void Start()
    {
        // get the dispatcher for the UI thread
        var uiDispatcher = Dispatcher.CurrentDispatcher;

        // start the background thread and pass it the UI thread dispatcher
        Task.Factory.StartNew(() => BackgroundThreadProc(uiDispatcher));
    }

    // this is called on the background thread
    public void BackgroundThreadProc(Dispatcher uiDispatcher)
    {
        for (var i = 0; i < 10000; i++)
        {
            // create object
            var animal = new Animal { Name = "test" + i };

            // invoke list.Add on the UI thread
            uiDispatcher.Invoke(new Action(() => list.Add(animal)));

            // sleep
            System.Threading.Thread.Sleep(1);
        }
    }
}

答案 1 :(得分:1)

您需要维护当前的调度程序线程。您必须仅在当前调度程序线程中更新集合。一种方法是使用调度程序类的BiginInvoke()方法。

将当前调度程序保存在构造函数中的变量中,然后在需要时使用它。

 _currentDispatcher = Application.Current.Dispatcher;

例如:我们有一个场景,如果出现错误,我们会弹出一个错误窗口。如果错误计数为零,我们需要关闭错误窗口。现在,如果我们在另一个线程(而不是UI线程)中处理事件和消息,那么我们需要保存UI线程调度程序对象,并且需要使用它来更新集合或任何其他操作。我在这里关闭错误窗口。 (我没有准备好更新收集的解决方案。)

 if (ErrorNotifications.Count == 0)
    _currentDispatcher.BeginInvoke(DispatcherPriority.Normal, new Action<ErrorNotificationWindow>(CloseErrorNotificationWindow), _errWindow);

这里的CloseErrorNotificationWindow是带参数_errWindow的方法。

private void CloseErrorNotificationWindow(ErrorNotificationWindow _errWindow)
{
  if (_errWindow == null)
    return;

  if (_errWindow.IsActive)
    _errWindow.Close();
}

在CloseErrorNotificationWindow()方法中,您可以更新集合,它不应该提供任何异常,因为您将使用主UI线程来执行此操作。 希望这会有所帮助。