¿Dónde obtengo un CollectionView seguro para subprocesos?

Al actualizar una colección de objetos comerciales en un hilo de fondo aparece este mensaje de error:

Este tipo de CollectionView no admite cambios en su SourceCollection de un hilo diferente del hilo Dispatcher.

Ok, eso tiene sentido. Pero también plantea la pregunta, ¿qué versión de CollectionView admite múltiples hilos y cómo hago que mis objetos la usen?

Lo siguiente es una mejora en la implementación encontrada por Jonathan. En primer lugar, ejecuta cada controlador de eventos en el despachador asociado, en lugar de asumir que todos están en el mismo despachador (UI). En segundo lugar, usa BeginInvoke para permitir que el procesamiento continúe mientras esperamos que el despachador esté disponible. Esto hace que la solución sea mucho más rápida en situaciones donde el hilo de fondo está haciendo muchas actualizaciones con procesamiento entre cada una. Quizás lo más importante es que supera los problemas causados ​​por el locking mientras espera la Invocación (los lockings pueden ocurrir, por ejemplo, al usar WCF con ConcurrencyMode.Single).

public class MTObservableCollection : ObservableCollection { 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); } } } 

Debido a que estamos utilizando BeginInvoke, es posible que el cambio notificado se deshaga antes de que se llame al controlador. Esto normalmente daría como resultado un “Índice estaba fuera de rango”. se lanza una excepción cuando los argumentos del evento se comparan con el nuevo estado (alterado) de la lista. Para evitar esto, todos los eventos retrasados ​​se reemplazan con eventos de reinicio. Esto podría causar un redibujado excesivo en algunos casos.

Utilizar:

 System.Windows.Application.Current.Dispatcher.Invoke( System.Windows.Threading.DispatcherPriority.Normal, (Action)delegate() { // Your Action Code }); 

Esta publicación de Bea Stollnitz explica ese mensaje de error y por qué está redactado tal como es.

EDITAR: Del blog de Bea

Desafortunadamente, este código da como resultado una excepción: “NotSupportedException: este tipo de CollectionView no admite cambios en su SourceCollection desde un hilo diferente al de Dispatcher”. Entiendo que este mensaje de error lleva a las personas a pensar que, si CollectionView están el uso no admite cambios cruzados, entonces tienen que encontrar el que sí lo haga. Bueno, este mensaje de error es un poco engañoso: ninguno de los CollectionViews que brindamos de fábrica admite cambios en la recolección de hilos cruzados. Y no, desafortunadamente no podemos arreglar el mensaje de error en este momento, estamos muy bloqueados.

Encontré uno.

 public class MTObservableCollection : ObservableCollection { 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

También puede consultar: BindingOperations.EnableCollectionSynchronization .

Consulte Actualización a .NET 4.5: Un control de elementos no es coherente con su fuente de elementos

Lo siento, no puedo agregar un comentario pero todo esto está mal.

ObservableCollection no es seguro para subprocesos. No solo por los problemas de este despachador, sino porque no es seguro para subprocesos (desde msdn):

Cualquier miembro público estático (compartido en Visual Basic) de este tipo es seguro para subprocesos. No se garantiza que ningún miembro de instancia sea seguro para subprocesos.

Mira aquí http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx

También hay un problema al llamar a BeginInvoke con una acción de “Restablecimiento”. “Restablecer” es la única acción donde el manejador debe mirar la colección en sí. Si comienzaInvoque un “Restablecer” y luego Inmediato Invoque un par de acciones “Agregar” que el controlador aceptará un “Restablecer” con la colección ya actualizada y el siguiente “Agregar” creará un desastre.

Aquí está mi implementación que funciona. En realidad, estoy pensando en eliminar BeginInvoke en absoluto:

Colección observable de ejecución rápida y segura para hilos

Si desea actualizar el control de interfaz de usuario de WPF periódicamente y, al mismo tiempo, usar la interfaz de usuario, puede usar DispatcherTimer .

XAML

     

DO#

  public partial class DownloadStats : Window { private MainWindow _parent; DispatcherTimer timer = new DispatcherTimer(); ObservableCollection fileViewList = new ObservableCollection(); 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; } } } 

Ninguno de ellos, solo use Dispatcher.BeginInvoke

Prueba esto:

 this.Dispatcher.Invoke(DispatcherPriority.Background, new Action( () => { //Code })); 

Aquí hay una versión de VB que hice después de googlear y ligeras modificaciones. Funciona para mi.

  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 

Pequeño error en la versión VB. Simplemente reemplace:

 Dim obj As DispatcherObject = invocation.Target 

Por

 Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)