Come aggiorno ObservableCollection tramite un thread di lavoro?

Ho una ObservableCollection a_collection; La collezione contiene elementi “n”. Ogni articolo A assomiglia a questo:

 public class A : INotifyPropertyChanged { public ObservableCollection b_subcollection; Thread m_worker; } 

Fondamentalmente, è tutto cablato fino a una listview WPF + un controllo della vista dettagli che mostra la raccolta b_sub dell’elemento selezionato in una listview separata (binding a 2 vie, aggiornamenti su propertychanged, ecc.). Il problema mi è apparso quando ho iniziato a implementare il threading. L’idea era di fare in modo che tutta la raccolta di a_cartection utilizzasse il thread di lavoro per “fare il lavoro” e quindi aggiornare le rispettive b_subcollections e fare in modo che i GUI mostrassero i risultati in tempo reale.

Quando l’ho provato, ho ricevuto un’eccezione che diceva che solo il thread di Dispatcher può modificare ObservableCollection e che il lavoro si è interrotto.

Qualcuno può spiegare il problema e come aggirarlo?

Saluti

Tecnicamente il problema non sta nel fatto che stai aggiornando ObservableCollection da un thread in background. Il problema è che quando lo fai, la raccolta aumenta il suo evento CollectionChanged sullo stesso thread che ha causato la modifica – il che significa che i controlli vengono aggiornati da un thread in background.

Per popolare una raccolta da un thread in background mentre i controlli sono associati ad essa, probabilmente dovresti creare il tuo tipo di raccolta da zero per poterlo risolvere. C’è un’opzione più semplice che potrebbe funzionare per te però.

Pubblica le chiamate Aggiungi sul thread dell’interfaccia utente.

 public static void AddOnUI(this ICollection collection, T item) { Action addMethod = collection.Add; Application.Current.Dispatcher.BeginInvoke( addMethod, item ); } ... b_subcollection.AddOnUI(new B()); 

Questo metodo verrà restituito immediatamente (prima che l’elemento venga effettivamente aggiunto alla raccolta) quindi sul thread dell’interfaccia utente, l’elemento verrà aggiunto alla raccolta e tutti dovrebbero essere felici.

La realtà, tuttavia, è che questa soluzione probabilmente si impantanerà sotto carico pesante a causa di tutta l’attività cross-thread. Una soluzione più efficiente consente di raggruppare un gruppo di elementi e pubblicarli periodicamente sul thread dell’interfaccia utente, in modo che non si stia chiamando attraverso i thread per ciascun elemento.

La class BackgroundWorker implementa un modello che consente di segnalare lo stato di avanzamento tramite il metodo ReportProgress durante un’operazione in background. L’avanzamento è segnalato sul thread dell’interfaccia utente tramite l’evento ProgressChanged. Questa potrebbe essere un’altra opzione per te.

Nuova opzione per .NET 4.5

A partire da .NET 4.5 è disponibile un meccanismo integrato per sincronizzare automaticamente l’accesso alla raccolta e inviare eventi CollectionChanged al thread dell’interfaccia utente. Per abilitare questa funzione è necessario chiamare BindingOperations.EnableCollectionSynchronization dal thread dell’interfaccia utente .

EnableCollectionSynchronization fa due cose:

  1. Memorizza il thread da cui viene chiamato e fa in modo che la pipeline di associazione dei dati esegua il marshal degli eventi CollectionChanged su quel thread.
  2. Acquisisce un blocco sulla raccolta fino a quando l’evento marshalling è stato gestito, in modo che i gestori di eventi che eseguono il thread dell’interfaccia utente non tenteranno di leggere la raccolta mentre viene modificata da un thread in background.

Molto importante, questo non si occupa di tutto : per garantire un accesso sicuro ai thread a una raccolta intrinsecamente non thread-safe, è necessario collaborare con il framework acquisendo lo stesso lock dai thread in background quando la raccolta sta per essere modificata.

Pertanto i passaggi necessari per il corretto funzionamento sono:

1. Decidi quale tipo di blocco utilizzerai

Questo determinerà quale sovraccarico di EnableCollectionSynchronization deve essere usato. La maggior parte delle volte è sufficiente una semplice istruzione di lock , quindi questo overload è la scelta standard, ma se si utilizza un meccanismo di sincronizzazione di fantasia è disponibile anche il supporto per i blocchi personalizzati .

2. Creare la raccolta e abilitare la sincronizzazione

A seconda del meccanismo di blocco scelto, chiamare il sovraccarico appropriato sul thread dell’interfaccia utente . Se si utilizza un’istruzione di lock standard, è necessario fornire l’object lock come argomento. Se si utilizza la sincronizzazione personalizzata, è necessario fornire un delegato CollectionSynchronizationCallback e un object contesto (che può essere null ). Quando viene invocato, questo delegato deve acquisire il blocco personalizzato, richiamare l’ Action passata e rilasciare il blocco prima di tornare.

3. Cooperare bloccando la raccolta prima di modificarla

Devi anche bloccare la raccolta usando lo stesso meccanismo quando stai per modificarlo da solo; fallo con lock() sullo stesso object di blocco passato a EnableCollectionSynchronization nello scenario semplice o con lo stesso meccanismo di sincronizzazione personalizzato nello scenario personalizzato.

Con .NET 4.0 puoi usare questi one-liner:

.Add

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

.Remove

 Application.Current.Dispatcher.BeginInvoke(new Func(() => this.MyObservableCollection.Remove(myItem))); 

Codice di sincronizzazione raccolta per i posteri. Questo utilizza un semplice meccanismo di blocco per abilitare la sincronizzazione della raccolta. Si noti che è necessario abilitare la sincronizzazione della raccolta sul thread dell’interfaccia utente.

 public class MainVm { private ObservableCollection _collectionOfObjects; private readonly object _collectionOfObjectsSync = new object(); public MainVm() { _collectionOfObjects = new ObservableCollection(); // 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); })); } ///  /// A different thread can access the collection through this method ///  /// The new mini vm to add to observable collection private void AddMiniVm(MiniVm newMiniVm) { lock (_collectionOfObjectsSync) { _collectionOfObjects.Insert(0, newMiniVm); } } }