ManualResetEvent WaitOne blocks the owner Thread of my CollectionView
Asked Answered
V

2

6

I've written a WPF WizardFramework which performs some actions in the background using some BackgroundWorker. While processing it can happen that I have to update an ObservableCollection which is bound to my UI.

For this case I've written a ThreadableObservableCollection, which provides threadsafe methods for Insert, Remove and RemoveAt. Though I'm using .NET 4.5 I was not able to get BindingOperations.EnableCollectionSynchronization working without many other invalid access exceptions. My Collection looks like:

  public class ThreadableObservableCollection<T> : ObservableCollection<T>
  {
    private readonly Dispatcher _dispatcher;
    public ThreadableObservableCollection()
    {
      _dispatcher = Dispatcher.CurrentDispatcher;
    }

    public void ThreadsafeInsert(int pos, T item, Action callback)
    {
      if (_dispatcher.CheckAccess())
      {
        Insert(pos, item);
        callback();
      }
      else
      {
        _dispatcher.Invoke(() =>
          {
            Insert(pos, item);
            callback();
          });
      }
    }

    [..]
  }

This is working as expected, while I am using the wizard in my application. Now I'm using NUnit to write some integrationtests for the application.

There's a listener which waits for the WizardViewModel to finish it's work and looking for some pages which are injected in the Steps-Collection. After the asyncrone work is done I can use Validate to check the viewmodel state.

Unfortunately I'm using a ManualResetEvent to wait for the wizard to close. This looks like following:

  public class WizardValidator : IValidator, IDisposable
  {
    private WizardViewModel _dialog;
    private readonly ManualResetEvent _dialogClosed = new ManualResetEvent(false);

    [..]

    public void ListenTo(WizardViewModel dialog)
    {
      _dialog = dialog;
      dialog.RequestClose += (sender, args) => _dialogClosed.Set();
      dialog.StepsDefaultView.CurrentChanged += StepsDefaultViewOnCurrentChanged;

      _dialogClosed.WaitOne();
    }

    [..]
 }

Now there's a problem: While the Application is running the UI Thread is not blocked, the Collection can be updated without any problems. But in my testcases the "main" Thread where I initialize the ViewModel (and because of that the Collections) is an AppDomainThread which is blocked by the testcode. Now my ThreadsafeInsert wants to update the collection but cannot use the AppDomain Thread.

But I have to wait for the wizard to finish, how can I solve this kind of deadlock? Or is there a more elegant solution for this one?

edit: I worked around this problem with a check if there's a user interface, and only then I invoke on the Application-Thread, otherwise I change the collection intentionally on another thread. This does not prevent the exception, but it is not recognized from the test... the items are inserted nevertheless, only the NotifyCollectionChanged-Handler is not called (which is only used in the UI anyway).

  if (Application.Current != null)
  {
    Application.Current.Dispatcher.Invoke(() =>
      {
        Steps.Insert(pos, step);
        stepsView.MoveCurrentTo(step);
      });
  }
  else
  {
    new Action(() => Steps.Insert(pos, step)).BeginInvoke(ar => stepsView.MoveCurrentToPosition(pos), null);  
  }

This is an ugly workaround and I am still interested in a clean solution.

Is there a way to use an alternate Dispatcher to create (e.g.) the whole ViewModel and use this to change my collection?

Vase answered 17/12, 2013 at 12:24 Comment(6)
This is a common question. The canonical answer is in this SO question.Regurgitate
okay so I should not wait for this with the ManualResetEvent but push a DispatcherFrame to wait for the result of the dialog? seems reasonable.Vase
Use application.current.dispatcher instead of dispatcher.currentdispatcher. Take a look at this link: #10449487Fran
Windows Created Modal Dialogs to solve this, why aren't you using Modal Dialog?Thoroughbred
@devhedgehog Application.Current is null while Unittesting.Vase
@AkashKava there's no dialog in unittesting.Vase
H
3

As I see the main problem that main thread is blocked and other operations are trying to be executed in main thread too? What about not to block main thread, like this:

// helper functions
public void DoEvents()
{
    DispatcherFrame frame = new DispatcherFrame();
    Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
        new DispatcherOperationCallback(ExitFrame), frame);
    Dispatcher.PushFrame(frame);
}

public object ExitFrame(object f)
{
    ((DispatcherFrame)f).Continue = false;

    return null;
}

// in your code:  
while(!_dialogClosed.WaitOne(200)) 
    DoEvents();

If it will not help then I guess need to try some SynchronisationContext workarounds.

Hygrometer answered 25/12, 2013 at 2:56 Comment(1)
the use of DispatcherFrame is something HansPassant wrote in the comments of the questions. This is the only (clean) way to work around the blocked dispatcher problem and the correct way to wait in unittests for an asyncrone dialogVase
S
1

I think the problems boil down to the fact that you create ObservableCollection that is tied to Dispatcher object.

Involving Dispatcher object directly is almost never good idea(as you just witnessed). Instead I would suggest you to see how others have implemented ThreadSafeObservableCollection. This is a little example I put together, it should illustrate the point:

public class ThreadSafeObservableCollection<T> : ObservableCollection<T>
{
    private readonly object _lock = new object();

    public ThreadSafeObservableCollection()
    {
        BindingOperations.CollectionRegistering += CollectionRegistering;
    }

    protected override void InsertItem(int index, T item)
    {
        lock (_lock)
        {
            base.InsertItem(index, item);
        }
    }

    private void CollectionRegistering(object sender, CollectionRegisteringEventArgs e)
    {
        if (e.Collection == this)
            BindingOperations.EnableCollectionSynchronization(this, _lock);
    }
}
Sweated answered 22/12, 2013 at 14:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.