Como faço para atualizar um ObservableCollection por meio de um segmento de trabalho?

Eu tenho um ObservableCollection a_collection; A coleção contém itens ‘n’. Cada item A se parece com isto:

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

Basicamente, tudo é conectado a um listview do WPF + um controle de visualização de detalhes que mostra a subcoleção b_s do item selecionado em um listview separado (ligações bidirecionais, atualizações em propertychanged etc.). O problema apareceu para mim quando comecei a implementar o threading. A ideia era que todo o a_collection usasse seu thread de trabalho para “fazer o trabalho” e, em seguida, atualizasse seus respectivos b_subcollections e fizesse o gui mostrar os resultados em tempo real.

Quando tentei, recebi uma exceção dizendo que apenas o thread do Dispatcher pode modificar um ObservableCollection e o trabalho foi interrompido.

Alguém pode explicar o problema e como contornar isso?

Felicidades

Tecnicamente, o problema não é que você esteja atualizando o ObservableCollection de um thread de segundo plano. O problema é que, quando você faz isso, a coleção gera seu evento CollectionChanged no mesmo thread que causou a alteração – o que significa que os controles estão sendo atualizados de um thread em segundo plano.

Para preencher uma coleção de um thread em segundo plano enquanto os controles estão vinculados a ela, é provável que você tenha que criar seu próprio tipo de coleção do zero para resolver isso. Existe uma opção mais simples que pode funcionar para você embora.

Poste as chamadas de adição no thread da interface do usuário.

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

Esse método retornará imediatamente (antes que o item seja realmente adicionado à coleção) e, em seguida, no thread da interface do usuário, o item será adicionado à coleção e todos deverão ficar felizes.

A realidade, no entanto, é que essa solução provavelmente ficará sobrecarregada sob carga pesada por causa de toda a atividade cross-thread. Uma solução mais eficiente agruparia vários itens e os publicaria periodicamente no thread da interface do usuário para que você não estivesse chamando os threads de cada item.

A class BackgroundWorker implementa um padrão que permite relatar o progresso por meio do método ReportProgress durante uma operação em segundo plano. O progresso é relatado no thread da interface do usuário por meio do evento ProgressChanged. Esta pode ser outra opção para você.

Nova opção para o .NET 4.5

A partir do .NET 4.5, há um mecanismo interno para sincronizar automaticamente o access à coleção e despachar events CollectionChanged para o segmento de interface do usuário. Para ativar esse recurso, você precisa chamar BindingOperations.EnableCollectionSynchronization de dentro do seu thread da interface do usuário .

EnableCollectionSynchronization faz duas coisas:

  1. Lembra o encadeamento do qual ele é chamado e faz com que o encadeamento de associação de dados marque os events CollectionChanged nesse encadeamento.
  2. Adquire um bloqueio na coleção até que o evento empacotado tenha sido manipulado, para que os manipuladores de events que executam o thread de interface do usuário não tentem ler a coleção enquanto ela está sendo modificada de um thread de plano de fundo.

Muito importante, isso não cuida de tudo : para garantir access seguro a thread a uma coleção inerentemente não segura para thread, você tem que cooperar com o framework adquirindo o mesmo lock de seus threads de segundo plano quando a coleção está prestes a ser modificada.

Portanto, as etapas necessárias para a operação correta são:

1. Decida que tipo de bloqueio você estará usando

Isso determinará qual sobrecarga de EnableCollectionSynchronization deve ser usada. Na maioria das vezes, uma instrução de lock simples será suficiente, portanto, essa sobrecarga é a opção padrão, mas se você estiver usando algum mecanismo sofisticado de synchronization, também haverá suporte para bloqueios personalizados .

2. Crie a coleção e ative a synchronization

Dependendo do mecanismo de bloqueio escolhido, chame a sobrecarga apropriada no thread de interface do usuário . Se estiver usando uma instrução de lock padrão, você precisará fornecer o object de bloqueio como um argumento. Se estiver usando synchronization personalizada, você precisará fornecer um delegado CollectionSynchronizationCallback e um object de contexto (que pode ser null ). Quando chamado, esse delegado deve adquirir seu bloqueio personalizado, invocar a Action passada para ele e liberar o bloqueio antes de retornar.

3. Cooperar bloqueando a coleção antes de modificá-la

Você também deve bloquear a coleção usando o mesmo mecanismo quando estiver prestes a modificá-la por conta própria; faça isso com lock() no mesmo object de bloqueio passado para EnableCollectionSynchronization no cenário simples ou com o mesmo mecanismo de synchronization customizado no cenário customizado.

Com o .NET 4.0, você pode usar esses one-liners:

.Add

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

.Remove

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

Código de synchronization de coleção para a posteridade. Isso usa um mecanismo de bloqueio simples para ativar a synchronization de coleta. Observe que você precisará ativar a synchronization de coleta no thread da interface do usuário.

 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); } } }