Где я могу получить потокобезопасный CollectionView?
При обновлении коллекции бизнес-объектов в фоновом потоке я получаю следующее сообщение об ошибке:
Этот тип CollectionView не поддерживает изменения в его SourceCollection из потока, отличного от потока Dispatcher.
Хорошо, это имеет смысл. Но также возникает вопрос: какая версия CollectionView поддерживает несколько потоков и как мне сделать так, чтобы мои объекты ее использовали?
11 ответов
Следующее является улучшением реализации, найденной Джонатаном. Во-первых, он запускает каждый обработчик событий в связанном с ним диспетчере, а не предполагает, что все они находятся в одном диспетчере (UI). Во-вторых, он использует BeginInvoke, чтобы продолжить обработку, пока мы ожидаем, что диспетчер станет доступным. Это значительно ускоряет решение в ситуациях, когда фоновый поток выполняет много обновлений с обработкой между ними. Возможно, более важно, что он преодолевает проблемы, вызванные блокировкой во время ожидания Invoke (взаимные блокировки могут возникать, например, при использовании WCF с ConcurrencyMode.Single).
public class MTObservableCollection<T> : ObservableCollection<T>
{
public override event NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
NotifyCollectionChangedEventHandler CollectionChanged = this.CollectionChanged;
if (CollectionChanged != null)
foreach (NotifyCollectionChangedEventHandler nh in CollectionChanged.GetInvocationList())
{
DispatcherObject dispObj = nh.Target as DispatcherObject;
if (dispObj != null)
{
Dispatcher dispatcher = dispObj.Dispatcher;
if (dispatcher != null && !dispatcher.CheckAccess())
{
dispatcher.BeginInvoke(
(Action)(() => nh.Invoke(this,
new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))),
DispatcherPriority.DataBind);
continue;
}
}
nh.Invoke(this, e);
}
}
}
Поскольку мы используем BeginInvoke, возможно, что уведомление об изменении отменяется до вызова обработчика. Это обычно приводит к тому, что "индекс выходит за пределы диапазона". генерируется исключение, когда аргументы события проверяются относительно нового (измененного) состояния списка. Чтобы избежать этого, все отложенные события заменяются событиями сброса. Это может вызвать чрезмерное перерисовывание в некоторых случаях.
Использование:
System.Windows.Application.Current.Dispatcher.Invoke(
System.Windows.Threading.DispatcherPriority.Normal,
(Action)delegate()
{
// Your Action Code
});
Это сообщение от Bea Stollnitz объясняет это сообщение об ошибке и почему оно так и есть.
РЕДАКТИРОВАТЬ: Из блога Беа
К сожалению, этот код приводит к исключению: "NotSupportedException - этот тип CollectionView не поддерживает изменения в его SourceCollection из потока, отличного от потока Dispatcher". Я понимаю, что это сообщение об ошибке заставляет людей думать, что если CollectionView они Использование не поддерживает изменения между потоками, тогда они должны найти тот, который делает. Что ж, это сообщение об ошибке немного вводит в заблуждение: ни один из представлений CollectionView, которые мы предоставляем из коробки, не поддерживает изменения коллекции между потоками. И нет, к сожалению, мы не можем исправить сообщение об ошибке на данный момент, мы очень сильно заблокированы.
Нашел один.
public class MTObservableCollection<T> : ObservableCollection<T>
{
public override event NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
var eh = CollectionChanged;
if (eh != null)
{
Dispatcher dispatcher = (from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()
let dpo = nh.Target as DispatcherObject
where dpo != null
select dpo.Dispatcher).FirstOrDefault();
if (dispatcher != null && dispatcher.CheckAccess() == false)
{
dispatcher.Invoke(DispatcherPriority.DataBind, (Action)(() => OnCollectionChanged(e)));
}
else
{
foreach (NotifyCollectionChangedEventHandler nh in eh.GetInvocationList())
nh.Invoke(this, e);
}
}
}
}
http://www.julmar.com/blog/mark/2009/04/01/AddingToAnObservableCollectionFromABackgroundThread.aspx
Вы также можете посмотреть на: BindingOperations.EnableCollectionSynchronization
,
См. Обновление до.NET 4.5: ItemsControl несовместим с источником своих элементов.
Извините, не могу добавить комментарий, но все это неправильно.
ObservableCollection не является потокобезопасным. Не только из-за проблем с диспетчером, но и вовсе не является потокобезопасным (из msdn):
Любые открытые статические (Shared в Visual Basic) члены этого типа являются потокобезопасными. Любые члены экземпляра не гарантированно являются потокобезопасными.
Посмотрите здесь http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx
Существует также проблема при вызове BeginInvoke с действием "Сброс". "Сброс" - единственное действие, когда обработчик должен смотреть на саму коллекцию. Если вы начинаете вызывать "Сброс", а затем сразу начинаете инициировать пару действий "Добавить", то обработчик примет "Сброс" с уже обновленной коллекцией, а последующие "Добавить" создадут беспорядок.
Вот моя реализация, которая работает. На самом деле я думаю об удалении BeginInvoke:
Вы можете настроить wpf для управления межпотоковыми изменениями в коллекции, включив синхронизацию коллекции следующим образом:
BindingOperations.EnableCollectionSynchronization(collection, syncLock);
listBox.ItemsSource = collection;
Это сообщает WPF, что коллекция может быть изменена вне потока пользовательского интерфейса, поэтому он знает, что он должен маршалировать любые изменения пользовательского интерфейса обратно в соответствующий поток.
Также существует перегрузка для обеспечения обратного вызова синхронизации, если у вас нет объекта блокировки.
Попробуй это:
this.Dispatcher.Invoke(DispatcherPriority.Background, new Action(
() =>
{
//Code
}));
Если вы хотите периодически обновлять WPF UI Control и одновременно использовать UI, вы можете использовать DispatcherTimer.
XAML
<Grid>
<DataGrid AutoGenerateColumns="True" Height="200" HorizontalAlignment="Left" Name="dgDownloads" VerticalAlignment="Top" Width="548" />
<Label Content="" Height="28" HorizontalAlignment="Left" Margin="0,221,0,0" Name="lblFileCouner" VerticalAlignment="Top" Width="173" />
</Grid>
C#
public partial class DownloadStats : Window
{
private MainWindow _parent;
DispatcherTimer timer = new DispatcherTimer();
ObservableCollection<FileView> fileViewList = new ObservableCollection<FileView>();
public DownloadStats(MainWindow parent)
{
InitializeComponent();
_parent = parent;
Owner = parent;
timer.Interval = new TimeSpan(0, 0, 1);
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
}
void timer_Tick(object sender, EventArgs e)
{
dgDownloads.ItemsSource = null;
fileViewList.Clear();
if (_parent.contentManagerWorkArea.Count > 0)
{
foreach (var item in _parent.contentManagerWorkArea)
{
FileView nf = item.Value.FileView;
fileViewList.Add(nf);
}
}
if (fileViewList.Count > 0)
{
lblFileCouner.Content = fileViewList.Count;
dgDownloads.ItemsSource = fileViewList;
}
}
}
Вот версия для VB, которую я сделал после некоторых поисков в Google и небольших модов. Работает для меня.
Imports System.Collections.ObjectModel
Imports System.Collections.Specialized
Imports System.ComponentModel
Imports System.Reflection
Imports System.Windows.Threading
'from: http://stackru.com/questions/2137769/where-do-i-get-a-thread-safe-collectionview
Public Class ThreadSafeObservableCollection(Of T)
Inherits ObservableCollection(Of T)
'from: http://geekswithblogs.net/NewThingsILearned/archive/2008/01/16/listcollectionviewcollectionview-doesnt-support-notifycollectionchanged-with-multiple-items.aspx
Protected Overrides Sub OnCollectionChanged(ByVal e As System.Collections.Specialized.NotifyCollectionChangedEventArgs)
Dim doit As Boolean = False
doit = (e.NewItems IsNot Nothing) AndAlso (e.NewItems.Count > 0)
doit = doit OrElse ((e.OldItems IsNot Nothing) AndAlso (e.OldItems.Count > 0))
If (doit) Then
Dim handler As NotifyCollectionChangedEventHandler = GetType(ObservableCollection(Of T)).GetField("CollectionChanged", BindingFlags.Instance Or BindingFlags.NonPublic).GetValue(Me)
If (handler Is Nothing) Then
Return
End If
For Each invocation As NotifyCollectionChangedEventHandler In handler.GetInvocationList
Dim obj As DispatcherObject = invocation.Target
If (obj IsNot Nothing) Then
Dim disp As Dispatcher = obj.Dispatcher
If (disp IsNot Nothing AndAlso Not (disp.CheckAccess())) Then
disp.BeginInvoke(
Sub()
invocation.Invoke(Me, New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))
End Sub, DispatcherPriority.DataBind)
Continue For
End If
End If
invocation.Invoke(Me, e)
Next
End If
End Sub
End Class
Маленькая ошибка в версии VB. Просто замените:
Dim obj As DispatcherObject = invocation.Target
От
Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)