ManualResetEvent WaitOne阻止我的CollectionView的所有者Thread

时间:2013-12-17 12:24:50

标签: c# wpf nunit dispatcher manualresetevent

我编写了一个WPF WizardFramework,它使用一些BackgroundWorker在后​​台执行一些操作。在处理时,我必须更新绑定到我的UI的ObservableCollection

对于这种情况,我写了一个ThreadableObservableCollection,它为InsertRemoveRemoveAt提供了线程安全的方法。虽然我使用的是.NET 4.5但是在没有许多其他无效访问异常的情况下我无法工作BindingOperations.EnableCollectionSynchronization。我的Collection看起来像是:

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

    [..]
  }

这正在按预期工作,而我在我的应用程序中使用向导。现在我正在使用NUnit为应用程序编写一些集成测试。

有一个监听器等待WizardViewModel完成它的工作并寻找在Steps-Collection中注入的一些页面。完成asyncrone工作后,我可以使用Validate来检查viewmodel状态。

不幸的是我正在使用ManualResetEvent等待向导关闭。这看起来如下:

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

    [..]
 }

现在有一个问题: 在应用程序运行时,UI线程未被阻止,可以毫无问题地更新集合。但是在我的测试用例中,我初始化ViewModel的“主”线程(以及因为它的集合)是一个被测试代码阻止的AppDomainThread。现在我的ThreadsafeInsert想要更新集合但不能使用AppDomain线程。

但我必须等待向导完成,我该如何解决这种僵局?或者是否有更优雅的解决方案?

修改 我解决了这个问题,检查是否有用户界面,然后我才在Application-Thread上调用,否则我会故意在另一个线程上更改集合。这不会阻止异常,但是从测试中无法识别...但是插入了项目,只调用了NotifyCollectionChanged - Handler(无论如何都只在UI中使用)。

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

这是一个丑陋的解决方法,我仍然对一个干净的解决方案感兴趣。

有没有办法使用备用Dispatcher来创建(例如)整个ViewModel并使用它来更改我的集合?

2 个答案:

答案 0 :(得分:3)

我看到主要线程被阻止的主要问题以及其他操作也试图在主线程中执行?如何不阻止主线程,如下所示:

// 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();

如果没有帮助,我想我需要尝试一些SynchronisationContext解决方法。

答案 1 :(得分:0)

我认为问题归结为你创建了与Dispatcher对象绑定的ObservableCollection。

直接涉及Dispatcher对象几乎从来都不是好主意(正如您刚才所见)。相反,我建议你看看其他人是如何实现ThreadSafeObservableCollection的。这是我放在一起的一个小例子,它应该说明一点:

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