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