【问题标题】:ManualResetEvent WaitOne blocks the owner Thread of my CollectionViewManualResetEvent WaitOne 阻塞了我的 CollectionView 的所有者线程
【发布时间】:2014-01-05 05:01:39
【问题描述】:

我编写了一个 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 中的一些页面。异步工作完成后,我可以使用 Validate 检查视图模型状态。

不幸的是,我使用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 线程。

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

编辑: 我通过检查是否有用户界面来解决这个问题,然后我才在应用程序线程上调用,否则我故意在另一个线程上更改集合。这并不能防止异常,但无法从测试中识别出来……尽管如此,仍插入了项目,只有 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);  
  }

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

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

【问题讨论】:

  • 这是一个常见问题。规范答案在this SO question
  • 好的,所以我不应该用 ManualResetEvent 等待这个,而是推送一个 DispatcherFrame 来等待对话框的结果?看起来很合理。
  • 使用 application.current.dispatcher 而不是 dispatcher.currentdispatcher。看看这个链接:stackoverflow.com/questions/10448987/…
  • Windows 创建了模态对话框来解决这个问题,你为什么不使用模态对话框?
  • @devhedgehog Application.Current 在单元测试时为空。

标签: c# wpf nunit dispatcher manualresetevent


【解决方案1】:

我认为问题归结为您创建了绑定到 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);
    }
}

【讨论】:

    【解决方案2】:

    正如我看到的主要问题是主线程被阻塞并且其他操作也试图在主线程中执行?不阻塞主线程怎么样,像这样:

    // 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 解决方法。

    【讨论】:

    • DispatcherFrame 的使用是 HansPassant 在问题的 cmets 中写的。这是解决阻塞调度程序问题的唯一(干净)方法,也是在单元测试中等待异步对话框的正确方法
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-03-10
    • 1970-01-01
    • 2011-12-17
    • 1970-01-01
    • 2016-01-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多