Question

Hy,

I have a Observable Collection which is bind with a list box. I add logs to the Observable Collection. I always add the message immediately to the Observable Collecten. But the list gets only updated when the loop is finished but I want to Update it when I add one item in the for loop. This is why I use a Thread but I have a few problems.

I have a thread safe 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);
            }
    }
}

This is my test class:

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);
        }
    }
}

See the private void dsofsdkfd(object sender, RoutedEventArgs e) function to the comment Version1. In the beginning it works so the list updates everytime I add a item. After a few entries I get an exception:

"Information for developers (use Text Visualizer to read this):\r\nThis exception was thrown because the generator for control 'System.Windows.Controls.ListBox Items.Count:1089' with name 'Logger' has received sequence of CollectionChanged events that do not agree with the current state of the Items collection. The following differences were detected:\r\n Accumulated count 994 is different from actual count 1089. [Accumulated count is (Count at last Reset +

Adds - #Removes since last Reset).]\r\n\r\nOne or more of the following sources may have raised the wrong events:\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(The starred sources are considered more likely to be the cause of the problem.)\r\n\r\nThe most common causes are (a) changing the collection or its Count without raising a corresponding event, and (b) raising an event with an incorrect index or item parameter.\r\n\r\nThe exception's stack trace describes how the inconsistencies were detected, not how they occurred. To get a more timely exception, set the attached property 'PresentationTraceSources.TraceLevel' on the generator to value 'High' and rerun the scenario. One way to do this is to run a command similar to the following:\n
System.Diagnostics.PresentationTraceSources.SetTraceLevel(myItemsControl.ItemContainerGenerator, System.Diagnostics.PresentationTraceLevel.High)\r\nfrom the Immediate window. This causes the detection logic to run after every CollectionChanged event, so it will slow down the application.\r\n"


See private void dsofsdkfd(object sender, RoutedEventArgs e) function to the comment Version2. I also tried it with the TaskScheduler using FromCurrentSynchronizationContext.

Then it throws no exception but I have the same problem like at the beginning, so the list box refreshes only if the for each loop is finished.

How I can accomplish that the list box updates when I add an element?

Best regards

Was it helpful?

Solution

I wouldn't roll my own ObservableCollection for this. I'd just perform the .Add call on the UI thread.

    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);
        }
    }

Note that since you're in a Window subclass, this.Dispatcher will correspond to the dispatcher for the UI thread. If you move this logic to, say, a model or view model class, you'll need to explicitly capture the value of Dispatcher.Current on the UI thread, and pass that dispatcher manually to the background thread.

EDIT: OP asked for more information on using the Dispatcher outside of a FrameworkElement class. Here's how you would do that. The dispatcher for the UI thread is acquired on the UI thread by calling Dispatcher.CurrentDispatcher. That dispatcher is then passed directly into the background thread procedure.

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);
        }
    }
}

OTHER TIPS

You need to maintain current dispatcher thread for the same. You must update collection in current dispatcher thread only. One way to do it is to use BiginInvoke() method of dispatcher class.

Save current dispatcher in a variable in constructor and then use it when needed.

 _currentDispatcher = Application.Current.Dispatcher;

For example: We have a scenario where we popup an error window if we have an error. We need to close an Error window if error count is zero. Now if we are handling events and message in another thread (not on UI thread) then we need to save the UI thread dispatcher object and need to use it to update collection or any other action. Here I am closing Error Window. (I don't have solution ready for updating collection.)

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

Here CloseErrorNotificationWindow is method with parameter _errWindow.

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

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

In CloseErrorNotificationWindow() method you can update your collections and it should not give any exception as you would be using main UI thread to do it. Hope this will helpful.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top