J'ai un ObservableCollection<A> a_collection;
La collection contient 'n' éléments. Chaque élément A ressemble à ceci:
public class A : INotifyPropertyChanged
{
public ObservableCollection<B> b_subcollection;
Thread m_worker;
}
Fondamentalement, tout est connecté à une liste WPF + un contrôle de vue de détails qui montre la b_subcollection de l'élément sélectionné dans une liste séparée (liaisons bidirectionnelles, mises à jour sur propertychanged, etc.). Le problème est apparu pour moi lorsque j'ai commencé à implémenter le filetage. L'idée était de faire en sorte que l'ensemble a_collection utilise son thread de travail pour "faire le travail", puis de mettre à jour leurs b_subcollections respectives et que l'interface graphique affiche les résultats en temps réel.
Lorsque je l'ai essayé, j'ai eu une exception disant que seul le thread Dispatcher peut modifier une ObservableCollection et que le travail s'est arrêté.
Quelqu'un peut-il expliquer le problème et comment le contourner?
À votre santé
Techniquement, le problème n'est pas que vous mettez à jour ObservableCollection à partir d'un thread d'arrière-plan. Le problème est que lorsque vous procédez ainsi, la collection déclenche son événement CollectionChanged sur le même thread qui a provoqué la modification, ce qui signifie que les contrôles sont mis à jour à partir d'un thread d'arrière-plan.
Afin de remplir une collection à partir d'un thread d'arrière-plan alors que les contrôles y sont liés, vous devrez probablement créer votre propre type de collection à partir de zéro afin de résoudre ce problème. Il existe cependant une option plus simple qui peut vous convenir.
Publiez les appels d'ajout sur le thread d'interface utilisateur.
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());
Cette méthode reviendra immédiatement (avant que l'élément ne soit réellement ajouté à la collection) puis sur le thread d'interface utilisateur, l'élément sera ajouté à la collection et tout le monde devrait être content.
La réalité, cependant, est que cette solution va probablement s'enliser sous une forte charge en raison de toute l'activité inter-thread. Une solution plus efficace consisterait à regrouper un tas d'éléments et à les publier périodiquement sur le thread d'interface utilisateur afin de ne pas appeler entre les threads pour chaque élément.
La classe BackgroundWorker implémente un modèle qui vous permet de signaler la progression via sa méthode ReportProgress lors d'une opération en arrière-plan. La progression est signalée sur le thread d'interface utilisateur via l'événement ProgressChanged. Cela peut être une autre option pour vous.
À partir de .NET 4.5, il existe un mécanisme intégré pour synchroniser automatiquement l'accès à la collection et envoyer les événements CollectionChanged
au thread d'interface utilisateur. Pour activer cette fonctionnalité, vous devez appeler BindingOperations.EnableCollectionSynchronization
depuis votre thread d'interface utilisateur.
EnableCollectionSynchronization
fait deux choses:
CollectionChanged
sur ce thread.Très important, cela ne s'occupe pas de tout : pour assurer un accès thread-safe à une collection intrinsèquement non thread-safe vous devez coopérer avec le framework en acquérant le même verrou depuis vos threads d'arrière-plan lorsque la collection est sur le point d'être modifiée.
Par conséquent, les étapes requises pour un fonctionnement correct sont:
Cela déterminera quelle surcharge de EnableCollectionSynchronization
doit être utilisée. La plupart du temps, une simple instruction lock
suffit donc cette surcharge est le choix standard, mais si vous utilisez un mécanisme de synchronisation sophistiqué, il y a aussi prise en charge des verrous personnalisés .
Selon le mécanisme de verrouillage choisi, appelez la surcharge appropriée sur le thread d'interface utilisateur . Si vous utilisez une instruction lock
standard, vous devez fournir l'objet verrou comme argument. Si vous utilisez la synchronisation personnalisée, vous devez fournir un délégué CollectionSynchronizationCallback
et un objet contextuel (qui peut être null
). Lorsqu'il est invoqué, ce délégué doit acquérir votre verrou personnalisé, appeler le Action
qui lui a été transmis et libérer le verrou avant de revenir.
Vous devez également verrouiller la collection à l'aide du même mécanisme lorsque vous êtes sur le point de la modifier vous-même; faites cela avec lock()
sur le même objet verrou passé à EnableCollectionSynchronization
dans le scénario simple, ou avec le même mécanisme de synchronisation personnalisé dans le scénario personnalisé.
Avec .NET 4.0, vous pouvez utiliser ces lignes simples:
.Add
Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));
.Remove
Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));
Code de synchronisation de la collection pour la postérité. Cela utilise un mécanisme de verrouillage simple pour activer la synchronisation de la collection. Notez que vous devrez activer la synchronisation de la collection sur le thread d'interface utilisateur.
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);
}
}
}