web-dev-qa-db-fra.com

Comment mettre à jour une ObservableCollection via un thread de travail?

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é

68
Maciek

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.

65
Josh

Nouvelle option pour .NET 4.5

À 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:

  1. Se souvient du thread à partir duquel il est appelé et entraîne le pipeline de liaison de données à rassembler les événements CollectionChanged sur ce thread.
  2. Acquiert un verrou sur la collection jusqu'à ce que l'événement marshallé ait été géré, de sorte que les gestionnaires d'événements exécutant le thread d'interface utilisateur ne tentent pas de lire la collection pendant sa modification à partir d'un thread d'arrière-plan.

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:

1. Décidez du type de verrouillage que vous utiliserez

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 .

2. Créez la collection et activez la synchronisation

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.

3. Coopérez en verrouillant la collection avant de la modifier

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é.

111
Jon

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)));
15
WhileTrueSleep

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