Tengo un ObservableCollection<A> a_collection;
La colección contiene 'n' elementos. Cada elemento A se ve así:
public class A : INotifyPropertyChanged
{
public ObservableCollection<B> b_subcollection;
Thread m_worker;
}
Básicamente, todo está conectado a una vista de lista de WPF + un control de vista de detalles que muestra el b_subcollection
elemento seleccionado en una vista de lista separada (enlaces bidireccionales, actualizaciones de propiedad modificada, etc.).
El problema apareció para mí cuando comencé a implementar subprocesos. La idea era que todo el mundo a_collection
usara su hilo de trabajo para "hacer el trabajo" y luego actualizar sus respectivos b_subcollections
y hacer que la interfaz gráfica de usuario mostrara los resultados en tiempo real.
Cuando lo probé, obtuve una excepción que decía que solo el hilo Dispatcher puede modificar un ObservableCollection y el trabajo se detuvo.
¿Alguien puede explicar el problema y cómo solucionarlo?
Respuestas:
Técnicamente, el problema no es que esté actualizando ObservableCollection desde un hilo en segundo plano. El problema es que cuando lo haces, la colección genera su evento CollectionChanged en el mismo hilo que causó el cambio, lo que significa que los controles se actualizan desde un hilo en segundo plano.
Para completar una colección desde un hilo en segundo plano mientras los controles están vinculados a ella, probablemente tenga que crear su propio tipo de colección desde cero para solucionar este problema. Sin embargo, hay una opción más simple que puede funcionar para usted.
Publique las llamadas Add en el hilo de la interfaz de usuario.
public static void AddOnUI<T>(this ICollection<T> collection, T item) { Action<T> addMethod = collection.Add; Application.Current.Dispatcher.BeginInvoke( addMethod, item ); } ... b_subcollection.AddOnUI(new B());
Este método regresará inmediatamente (antes de que el elemento se agregue realmente a la colección), luego en el hilo de la interfaz de usuario, el elemento se agregará a la colección y todos deberían estar felices.
La realidad, sin embargo, es que esta solución probablemente se atascará bajo una carga pesada debido a toda la actividad de los hilos cruzados. Una solución más eficiente agruparía un montón de elementos y los publicaría en el hilo de la interfaz de usuario periódicamente para que no esté llamando a los hilos para cada elemento.
La clase BackgroundWorker implementa un patrón que le permite informar el progreso a través de su método ReportProgress durante una operación en segundo plano. El progreso se informa en el hilo de la interfaz de usuario a través del evento ProgressChanged. Esta puede ser otra opción para ti.
fuente
Nueva opción para .NET 4.5
A partir de .NET 4.5 hay un mecanismo integrado para sincronizar automáticamente el acceso a la colección y enviar
CollectionChanged
eventos al subproceso de la interfaz de usuario. Para habilitar esta función, debe llamar desde su hilo de interfaz de usuario .BindingOperations.EnableCollectionSynchronization
EnableCollectionSynchronization
hace dos cosas:CollectionChanged
eventos en ese subproceso.Muy importante, esto no se encarga de todo : para garantizar el acceso seguro para subprocesos a una colección intrínsecamente no segura para subprocesos, debe cooperar con el marco adquiriendo el mismo bloqueo de sus subprocesos en segundo plano cuando la colección está a punto de ser modificada.
Por tanto, los pasos necesarios para un correcto funcionamiento son:
1. Decida qué tipo de bloqueo utilizará
Esto determinará qué sobrecarga se
EnableCollectionSynchronization
debe utilizar. La mayoría de las veces, unalock
declaración simple será suficiente, por lo que esta sobrecarga es la opción estándar, pero si está utilizando algún mecanismo de sincronización elegante, también hay soporte para bloqueos personalizados .2. Cree la colección y habilite la sincronización
Según el mecanismo de bloqueo elegido, llame a la sobrecarga adecuada en el subproceso de la interfaz de usuario . Si utiliza una
lock
declaración estándar , debe proporcionar el objeto de bloqueo como argumento. Si utiliza la sincronización personalizada, debe proporcionar unCollectionSynchronizationCallback
delegado y un objeto de contexto (que puede sernull
). Cuando se invoca, este delegado debe adquirir su bloqueo personalizado, invocar el que se le haAction
pasado y liberar el bloqueo antes de regresar.3. Coopere bloqueando la colección antes de modificarla.
También debe bloquear la colección utilizando el mismo mecanismo cuando esté a punto de modificarla usted mismo; Haga esto con
lock()
el mismo objeto de bloqueo que se pasóEnableCollectionSynchronization
en el escenario simple, o con el mismo mecanismo de sincronización personalizado en el escenario personalizado.fuente
BeginInvoke
para ejecutar un método que realice todos los cambios apropiados en el hilo de la interfaz de usuario [como máximo unoBeginInvoke
estaría pendiente en un momento dado.Con .NET 4.0 puede utilizar estos conceptos básicos:
.Add
Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));
.Remove
Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));
fuente
Código de sincronización de colección para la posteridad. Utiliza un mecanismo de bloqueo simple para permitir la sincronización de la colección. Tenga en cuenta que deberá habilitar la sincronización de colecciones en el hilo de la interfaz de usuario.
public class MainVm { private ObservableCollection<MiniVm> _collectionOfObjects; private readonly object _collectionOfObjectsSync = new object(); public MainVm() { _collectionOfObjects = new ObservableCollection<MiniVm>(); // Collection Sync should be enabled from the UI thread. Rest of the collection access can be done on any thread Application.Current.Dispatcher.BeginInvoke(new Action(() => { BindingOperations.EnableCollectionSynchronization(_collectionOfObjects, _collectionOfObjectsSync); })); } /// <summary> /// A different thread can access the collection through this method /// </summary> /// <param name="newMiniVm">The new mini vm to add to observable collection</param> private void AddMiniVm(MiniVm newMiniVm) { lock (_collectionOfObjectsSync) { _collectionOfObjects.Insert(0, newMiniVm); } } }
fuente