ManualResetEvent WaitOne блокирует владельца Поток моего CollectionView

Я написал WPF WizardFramework, который выполняет некоторые действия в фоновом режиме, используя некоторые BackgroundWorker, Во время обработки может случиться так, что я должен обновить ObservableCollection который связан с моим пользовательским интерфейсом.

Для этого случая я написал ThreadableObservableCollection, который предоставляет потоковые методы для Insert, Remove а также RemoveAt, Хотя я использую.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 завершит свою работу, и ищет несколько страниц, которые добавляются в коллекцию шагов. После выполнения асинхронной работы я могу использовать 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();
    }

    [..]
 }

Теперь возникает проблема: во время работы приложения поток пользовательского интерфейса не блокируется, коллекция может быть обновлена ​​без каких-либо проблем. Но в моих тестовых случаях "основным" потоком, где я инициализирую ViewModel (и из-за этого Коллекции), является AppDomainThread, который блокируется тестовым кодом. Теперь мой ThreadsafeInsert хочет обновить коллекцию, но не может использовать поток AppDomain.

Но я должен дождаться завершения работы мастера, как я могу решить этот тупик? Или есть более элегантное решение для этого?

редактировать: я обошел эту проблему с проверкой, если есть пользовательский интерфейс, и только тогда я вызываю в Application-Thread, в противном случае я намеренно изменяю коллекцию в другом потоке. Это не исключает исключения, но оно не распознается из теста... элементы вставляются, тем не менее, только NotifyCollectionChanged-Handler не вызывается (который используется только в пользовательском интерфейсе в любом случае).

  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 ответа

Решение

Как я вижу, основная проблема в том, что основной поток заблокирован, а другие операции тоже пытаются выполняться в основном потоке? Как насчет того, чтобы не блокировать основной поток, например так:

// 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.

Я думаю, что проблемы сводятся к тому, что вы создаете ObservableCollection, который привязан к объекту Dispatcher.

Непосредственное вовлечение объекта 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);
    }
}
Другие вопросы по тегам