Beim Aktualisieren einer Sammlung von Geschäftsobjekten in einem Hintergrundthread wird folgende Fehlermeldung angezeigt:
Dieser CollectionView-Typ unterstützt keine Änderungen an seiner SourceCollection von einem anderen Thread als dem Dispatcher-Thread.
Ok, das macht Sinn. Es stellt sich jedoch auch die Frage, welche Version von CollectionView mehrere Threads unterstützt und wie kann ich meine Objekte dazu bringen, sie zu verwenden?
Das Folgende ist eine Verbesserung der von Jonathan gefundenen Implementierung. Erstens führt es jeden Event-Handler auf dem zugeordneten Dispatcher aus, anstatt davon auszugehen, dass sich alle auf demselben Dispatcher (UI) befinden. Zweitens verwendet es BeginInvoke, um die Verarbeitung fortzusetzen, während wir warten, bis der Dispatcher verfügbar wird. Dies macht die Lösung in Situationen, in denen der Hintergrund-Thread viele Aktualisierungen durchführt, viel schneller. Vielleicht noch wichtiger ist es, Probleme zu beheben, die durch das Blockieren beim Warten auf das Invoke verursacht werden (Deadlocks können beispielsweise bei der Verwendung von WCF mit ConcurrencyMode.Single auftreten).
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);
}
}
}
Da wir BeginInvoke verwenden, ist es möglich, dass die benachrichtigte Änderung rückgängig gemacht wird, bevor der Handler aufgerufen wird. Dies würde normalerweise dazu führen, dass "der Index außerhalb des Bereichs lag". Eine Ausnahme wird ausgelöst, wenn die Ereignisargumente gegen den neuen (geänderten) Status der Liste geprüft werden. Um dies zu vermeiden, werden alle verzögerten Ereignisse durch Reset-Ereignisse ersetzt. Dies kann in einigen Fällen zu übermäßigem Neuzeichnen führen.
Benutzen:
System.Windows.Application.Current.Dispatcher.Invoke(
System.Windows.Threading.DispatcherPriority.Normal,
(Action)delegate()
{
// Your Action Code
});
Dieser Post von Bea Stollnitz erklärt die Fehlermeldung that und warum es so formuliert ist.
EDIT: Aus Bea's Blog
Leider führt dieser Code zu einer Ausnahme: "NotSupportedException - Dieser Typ von CollectionView unterstützt keine Änderungen an seiner SourceCollection von einem anderen Thread als dem Dispatcher-Thread." Ich verstehe, dass diese Fehlermeldung den Leuten denkt, wenn das CollectionView ist Wenn keine Cross-Thread-Änderungen verwendet werden, müssen sie diejenige finden, die dies tut. Nun, diese Fehlermeldung ist etwas irreführend: Keine der von uns mitgelieferten CollectionViews unterstützt Änderungen der Cross-Thread-Erfassung. Und nein, leider können wir die Fehlermeldung an dieser Stelle nicht beheben, wir sind sehr gesperrt.
Fand einen.
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
Sie können auch Folgendes anzeigen: BindingOperations.EnableCollectionSynchronization
.
Siehe Upgrade auf .NET 4.5: Ein ItemsControl ist inkonsistent mit seiner Elementquelle
Sie können mit wpf Thread-übergreifende Änderungen an einer Sammlung verwalten, indem Sie die Sammlungssynchronisierung wie folgt aktivieren:
BindingOperations.EnableCollectionSynchronization(collection, syncLock);
listBox.ItemsSource = collection;
Dies teilt WPF mit, dass die Auflistung außerhalb des UI-Threads geändert werden kann, sodass bekannt ist, dass Änderungen an der Benutzeroberfläche wieder in dem entsprechenden Thread gespeichert werden müssen.
Es gibt auch eine Überladung, um einen Synchronisationsrückruf bereitzustellen, wenn Sie kein Sperrobjekt haben.
Sorry, ich kann keinen Kommentar hinzufügen, aber das ist alles falsch.
ObservableCollection ist nicht threadsicher. Nicht nur wegen dieser Dispatcher-Probleme, aber es ist überhaupt nicht threadsicher (von msdn):
Alle öffentlichen statischen Mitglieder (Shared in Visual Basic) dieses Typs sind threadsicher. Es ist nicht garantiert, dass alle Instanzmitglieder threadsicher sind.
Schauen Sie hier http://msdn.Microsoft.com/de-de/library/ms668604(v=vs.110).aspx
Es gibt auch ein Problem, wenn BeginInvoke mit der Aktion "Zurücksetzen" aufgerufen wird. "Zurücksetzen" ist die einzige Aktion, bei der der Handler die Sammlung selbst betrachten soll. Wenn Sie mit BeginInvoke ein "Reset" durchführen und sofort BeginInvoke ein paar "Add" - Aktionen ausführen, akzeptiert der Handler ein "Reset" mit bereits aktualisierter Sammlung und das nächste "Add" erstellt ein Durcheinander.
Hier ist meine Implementierung, die funktioniert. Eigentlich denke ich daran, BeginInvoke überhaupt zu entfernen:
Wenn Sie die WPF-UI-Steuerung regelmäßig aktualisieren und gleichzeitig die Benutzeroberfläche verwenden möchten, können Sie DispatcherTimer verwenden.
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;
}
}
}
Hier ist eine VB -Version, die ich nach ein paar guten und leichten Mods gemacht habe. Funktioniert bei mir.
Imports System.Collections.ObjectModel
Imports System.Collections.Specialized
Imports System.ComponentModel
Imports System.Reflection
Imports System.Windows.Threading
'from: http://stackoverflow.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
Kleiner Fehler in der VB Version. Ersetzen Sie einfach:
Dim obj As DispatcherObject = invocation.Target
Durch
Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)
Keiner von ihnen, verwenden Sie einfach Dispatcher.BeginInvoke
Versuche dies:
this.Dispatcher.Invoke(DispatcherPriority.Background, new Action(
() =>
{
//Code
}));