6

我编写了一个 WPF WizardFramework,它使用一些BackgroundWorker. 在处理时可能会发生我必须更新ObservableCollection绑定到我的 UI 的内容。

对于这种情况,我编写了一个,它为和ThreadableObservableCollection提供线程安全方法。虽然我使用的是 .NET 4.5,但如果没有许多其他无效访问异常,我就无法开始工作。我的样子:InsertRemoveRemoveAtBindingOperations.EnableCollectionSynchronizationCollection

  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 检查视图模型状态。

不幸的是,我正在使用 aManualResetEvent等待向导关闭。如下所示:

  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 并使用它来更改我的集合?

4

2 回答 2

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.

于 2013-12-25T02:56:26.687 回答
1

我认为问题归结为您创建了与 Dispatcher 对象相关联的 ObservableCollection。

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);
    }
}
于 2013-12-22T14:19:44.157 回答