¿Cómo actualizo una ObservableCollection a través de un hilo de trabajo?

Resuelto Maciek asked hace 14 años • 7 respuestas

Tengo una ObservableCollection<A> a_collection;La colección contiene 'n' elementos. Cada elemento A tiene este aspecto:

public class A : INotifyPropertyChanged
{

    public ObservableCollection<B> b_subcollection;
    Thread m_worker;
}

Básicamente, todo está conectado a una vista de lista WPF + un control de vista de detalles que muestra elb_subcollection elemento seleccionado en una vista de lista separada (enlaces bidireccionales, actualizaciones sobre cambios de propiedad, etc.).

El problema me apareció cuando comencé a implementar subprocesos. La idea era que todos usaran a_collectionsu hilo de trabajo para "hacer el trabajo" y luego actualizar sus respectivosb_subcollections y hacer que la interfaz gráfica de usuario mostrara los resultados en tiempo real.

Cuando lo probé, recibí una excepción que decía que solo el subproceso Dispatcher puede modificar una ObservableCollection y el trabajo se detuvo.

¿Alguien puede explicar el problema y cómo solucionarlo?

Maciek avatar Jan 19 '10 15:01 Maciek
Aceptado

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 CollectionChangedeventos al subproceso de la interfaz de usuario. Para habilitar esta función, debe llamar desde su hilo de interfaz de usuario .BindingOperations.EnableCollectionSynchronization

EnableCollectionSynchronizationhace dos cosas:

  1. Recuerda el subproceso desde el que se llama y hace que la canalización de enlace de datos reúna CollectionChangedeventos en ese subproceso.
  2. Adquiere un bloqueo en la colección hasta que se haya manejado el evento ordenado, de modo que los controladores de eventos que ejecutan el subproceso de la interfaz de usuario no intenten leer la colección mientras se modifica desde un subproceso en segundo plano.

Muy importante, esto no se encarga de todo : para garantizar el acceso seguro para subprocesos a una colección inherentemente 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 modificarse.

Por lo tanto los pasos necesarios para su correcto funcionamiento son:

1. Decide qué tipo de bloqueo utilizarás

Esto determinará qué sobrecarga se EnableCollectionSynchronizationdebe utilizar. La mayoría de las veces, una simple lockdeclaración será suficiente, por lo que esta sobrecarga es la opción estándar, pero si está utilizando algún mecanismo de sincronización sofisticado, también hay soporte para bloqueos personalizados .

2. Crea la colección y habilita la sincronización.

Dependiendo del mecanismo de bloqueo elegido, llame a la sobrecarga adecuada en el subproceso de la interfaz de usuario . Si utiliza una lockdeclaración estándar, debe proporcionar el objeto de bloqueo como argumento. Si utiliza sincronización personalizada, debe proporcionar un CollectionSynchronizationCallbackdelegado y un objeto de contexto (que puede ser null). Cuando se invoca, este delegado debe adquirir su bloqueo personalizado, invocar el que Actionse le pasó y liberar el bloqueo antes de regresar.

3. Coopere bloqueando la colección antes de modificarla.

También debes bloquear la colección usando el mismo mecanismo cuando estés a punto de modificarla tú mismo; haga esto con lock()el mismo objeto de bloqueo pasado EnableCollectionSynchronizationen el escenario simple, o con el mismo mecanismo de sincronización personalizado en el escenario personalizado.

Jon avatar Jan 30 '2013 10:01 Jon

Técnicamente, el problema no es que esté actualizando ObservableCollection desde un hilo en segundo plano. El problema es que cuando lo hace, la colección genera su evento CollectionChanged en el mismo hilo que causó el cambio, lo que significa que los controles se están actualizando desde un hilo en segundo plano.

Para completar una colección desde un hilo en segundo plano mientras los controles están vinculados a él, probablemente tendrás que crear tu propio tipo de colección desde cero para poder solucionar este problema. Sin embargo, existe una opción más sencilla que puede funcionar para usted.

Publique las llamadas Agregar 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 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 contentos.

La realidad, sin embargo, es que esta solución probablemente se estancará bajo una carga pesada debido a toda la actividad entre subprocesos. Una solución más eficiente sería agrupar un montón de elementos y publicarlos periódicamente en el hilo de la interfaz de usuario para que no se realicen llamadas entre 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.

Josh avatar Jan 19 '2010 08:01 Josh

Con .NET 4.0 puede utilizar estas frases ingeniosas:

.Add

Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));

.Remove

Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));
WhileTrueSleep avatar Jan 22 '2015 11:01 WhileTrueSleep

Código de sincronización de colección para la posteridad. Esto 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 la colección 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);
        }
    }
}
LadderLogic avatar Aug 17 '2018 19:08 LadderLogic