96 votes

Comment mettre à jour une ObservableCollection via un fil 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;
}

En fait, tout est relié à une vue de liste WPF et à un contrôle de la vue détaillée qui affiche l'information sur l'entreprise. b_subcollection de l'élément sélectionné dans une liste séparée (liaisons bidirectionnelles, mises à jour en cas de changement de propriété, etc.)

Le problème est apparu lorsque j'ai commencé à mettre en œuvre le threading. L'idée était de faire en sorte que tout le a_collection utilise son fil de travail pour "faire le travail" et met ensuite à jour ses fils de travail respectifs. b_subcollections et faire en sorte que le gui montre les résultats en temps réel.

Lorsque je l'ai essayé, j'ai obtenu une exception disant que seul le thread Dispatcher peut modifier une ObservableCollection, et le travail s'est arrêté.

Quelqu'un peut-il expliquer le problème, et comment le contourner ?

0 votes

Essayez le lien suivant qui fournit une solution à sécurité thread qui fonctionne à partir de n'importe quel thread et peut être liée à plusieurs threads d'interface utilisateur : codeproject.com/Articles/64936/

149voto

Jon Points 194296

Nouvelle option pour .NET 4.5

À partir de .NET 4.5, il existe un mécanisme intégré permettant de synchroniser automatiquement l'accès à la collection et à la répartition. CollectionChanged au fil de l'interface utilisateur. Pour activer cette fonctionnalité, vous devez appeler BindingOperations.EnableCollectionSynchronization à partir de votre fil d'interface utilisateur .

EnableCollectionSynchronization fait deux choses :

  1. Se souvient du fil d'exécution à partir duquel il est appelé et fait en sorte que le pipeline de liaison de données marshale CollectionChanged événements sur ce fil.
  2. Acquiert un verrou sur la collection jusqu'à ce que l'événement marshalled ait été traité, de sorte que les gestionnaires d'événements exécutant le thread UI ne tenteront pas de lire la collection alors qu'elle est en cours de modification depuis un thread d'arrière-plan.

Très important, cela ne prend pas tout en charge pour assurer un accès sécurisé à une collection qui ne l'est pas par nature. vous devez coopérer avec le framework en acquérant le même verrou de vos threads d'arrière-plan lorsque la collection est sur le point d'être modifiée.

Les étapes requises pour un fonctionnement correct sont donc les suivantes :

1. Décidez du type de fermeture que vous allez utiliser

Cela permettra de déterminer quelle surcharge de EnableCollectionSynchronization doit être utilisé. La plupart du temps, un simple lock suffira pour que cette surcharge est le choix standard, mais si vous utilisez un mécanisme de synchronisation sophistiqué, il y a aussi support pour les serrures personnalisées .

2. Créer la collection et activer la synchronisation

En fonction du mécanisme de verrouillage choisi, appelez la surcharge appropriée. sur le fil de l'interface utilisateur . Si vous utilisez un lock vous devez fournir l'objet de verrouillage comme argument. Si vous utilisez une synchronisation personnalisée, vous devez fournir un objet CollectionSynchronizationCallback et un objet de contexte (qui peut être null ). Lorsqu'il est invoqué, ce délégué doit acquérir votre verrou personnalisé, invoquer la fonction Action qui lui est passé et libère le verrou avant de revenir.

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

Vous devez également verrouiller la collection en utilisant le même mécanisme lorsque vous êtes sur le point de la modifier vous-même ; faites-le 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é.

2 votes

Cela entraîne-t-il le blocage des mises à jour de la collection jusqu'à ce que le thread de l'interface utilisateur puisse les traiter ? Dans les scénarios impliquant des collections d'objets immuables liées à des données à sens unique (un scénario relativement courant), il semblerait qu'il soit possible d'avoir une classe de collection qui conserverait une "dernière version affichée" de chaque objet ainsi qu'une file d'attente des modifications, et d'utiliser la fonction BeginInvoke pour exécuter une méthode qui effectuerait tous les changements appropriés dans le thread UI [au plus un BeginInvoke seraient en attente à un moment donné.

2 votes

Je ne savais même pas que cela existait ! Merci de l'avoir écrit !

0 votes

"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". J'ai parcouru le Web à la recherche d'informations sur cette fonctionnalité, et votre article semble être le seul qui mentionne le verrouillage de la collection avant de la modifier. Est-ce vraiment nécessaire ? Pouvez-vous donner un exemple de la façon de procéder ?

81voto

Josh Points 38617

Techniquement, le problème n'est pas que vous mettez à jour l'ObservableCollection à partir d'un thread d'arrière-plan. Le problème est que lorsque vous le faites, la collection lève son événement CollectionChanged sur le même thread que celui qui a provoqué la modification - ce qui signifie que les contrôles sont mis à jour depuis un thread d'arrière-plan.

Pour alimenter une collection à partir d'un thread d'arrière-plan alors que des 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 pourrait vous convenir.

Déposez les appels Add sur le thread de l'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 renvoie immédiatement (avant que l'élément ne soit réellement ajouté à la collection), puis sur le thread de l'interface utilisateur, l'élément sera ajouté à la collection et tout le monde devrait être content.

En réalité, cette solution risque de s'enliser sous une charge importante en raison de l'activité croisée des fils. Une solution plus efficace consisterait à regrouper un certain nombre d'éléments et à les envoyer périodiquement au thread de l'interface utilisateur afin de ne pas appeler plusieurs threads pour chaque élément.

El Travailleur d'arrière-plan met en œuvre un modèle qui vous permet de signaler la progression via sa classe ReportProgress pendant une opération en arrière-plan. La progression est signalée sur le thread de l'interface utilisateur via l'événement ProgressChanged. Cela peut être une autre option pour vous.

0 votes

Qu'en est-il de la fonction runWorkerAsyncCompleted du BackgroundWorker ? Est-elle également liée au thread de l'interface utilisateur ?

1 votes

La façon dont BackgroundWorker est conçu est d'utiliser SynchronizationContext.Current pour déclencher ses événements d'achèvement et de progression. L'événement DoWork sera exécuté sur le thread d'arrière-plan. Voici un bon article sur le threading dans WPF qui traite également du BackgroundWorker. msdn.microsoft.com/en-us/magazine/cc163328.aspx#S4

5 votes

Cette réponse est belle dans sa simplicité. Merci de la partager !

23voto

WhileTrueSleep Points 1287

Avec .NET 4.0, vous pouvez utiliser ces phrases d'une seule ligne :

.Add

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

.Remove

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

15voto

LadderLogic Points 900

Code de synchronisation des collections pour la postérité. Il 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 de l'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);
        }
    }
}

2voto

Jeffrey McNeal Points 117

J'ai utilisé un SynchronizationContext :

SynchronizationContext SyncContext { get; set; }

// dans le Constructeur :

SyncContext = SynchronizationContext.Current;

// dans le travailleur d'arrière-plan ou le gestionnaire d'événements :

SyncContext.Post(o =>
{
    ObservableCollection.AddRange(myData);
}, null);

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X