Lors de la mise à jour d’une collection d’objects métier sur un thread d’arrière-plan, j’obtiens ce message d’erreur:
Ce type de CollectionView ne prend pas en charge les modifications apscopes à sa sourceCollection à partir d’un thread différent du thread Dispatcher.
Ok, ça a du sens. Mais cela pose également la question suivante: quelle version de CollectionView prend en charge plusieurs threads et comment puis-je utiliser mes objects?
Ce qui suit est une amélioration de l’implémentation trouvée par Jonathan. Tout d’abord, il exécute chaque gestionnaire d’événement sur le dissortingbuteur qui lui est associé plutôt que de supposer qu’ils sont tous sur le même répartiteur (interface utilisateur). Deuxièmement, il utilise BeginInvoke pour permettre au traitement de continuer pendant que le répartiteur est disponible. Cela rend la solution beaucoup plus rapide dans les situations où le thread d’arrière-plan effectue de nombreuses mises à jour avec le traitement entre chacun. Peut-être plus important encore, il surmonte les problèmes causés par le blocage en attendant l’invocation (des blocages peuvent se produire, par exemple, lors de l’utilisation de WCF avec ConcurrencyMode.Single).
public class MTObservableCollection : ObservableCollection { public override event NotifyCollectionChangedEventHandler CollectionChanged; protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { NotifyCollectionChangedEventHandler CollectionChanged = this.CollectionChanged; if (CollectionChanged != null) foreach (NotifyCollectionChangedEventHandler nh in CollectionChanged.GetInvocationList()) { DispatcherObject dispObj = nh.Target as DispatcherObject; if (dispObj != null) { Dispatcher dispatcher = dispObj.Dispatcher; if (dispatcher != null && !dispatcher.CheckAccess()) { dispatcher.BeginInvoke( (Action)(() => nh.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))), DispatcherPriority.DataBind); continue; } } nh.Invoke(this, e); } } }
Comme nous utilisons BeginInvoke, il est possible que la modification notifiée soit annulée avant l’appel du gestionnaire. Cela se traduirait généralement par un “index hors de scope”. exception levée lorsque les arguments de l’événement sont vérifiés par rapport au nouvel état (modifié) de la liste. Pour éviter cela, tous les événements différés sont remplacés par des événements de réinitialisation. Cela pourrait entraîner une redessinisation excessive dans certains cas.
Utilisation:
System.Windows.Application.Current.Dispatcher.Invoke( System.Windows.Threading.DispatcherPriority.Normal, (Action)delegate() { // Your Action Code });
Ce message de Bea Stollnitz explique ce message d’erreur et pourquoi il est libellé tel quel.
EDIT: blog de Bea
Malheureusement, ce code entraîne une exception: «NotSupportedException – Ce type de CollectionView ne prend pas en charge les modifications apscopes à sa sourceCollection à partir d’un thread différent du thread Dispatcher.» Ce message d’erreur amène les utilisateurs à penser que using ne prend pas en charge les modifications inter-threads, alors ils doivent trouver celui qui le fait. Eh bien, ce message d’erreur est un peu trompeur: aucune des CollectionViews que nous fournissons ne prend en charge les modifications de collection inter-thread. Et non, malheureusement, nous ne pouvons pas corriger le message d’erreur à ce stade, nous sums très bloqués.
Trouvé un.
public class MTObservableCollection : ObservableCollection { public override event NotifyCollectionChangedEventHandler CollectionChanged; protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { var eh = CollectionChanged; if (eh != null) { Dispatcher dispatcher = (from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList() let dpo = nh.Target as DispatcherObject where dpo != null select dpo.Dispatcher).FirstOrDefault(); if (dispatcher != null && dispatcher.CheckAccess() == false) { dispatcher.Invoke(DispatcherPriority.DataBind, (Action)(() => OnCollectionChanged(e))); } else { foreach (NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()) nh.Invoke(this, e); } } } }
http://www.julmar.com/blog/mark/2009/04/01/AddingToAnObservableCollectionFromABackgroundThread.aspx
Vous pouvez également consulter: BindingOperations.EnableCollectionSynchronization
.
Voir Mise à niveau vers .NET 4.5: Un ItemsControl est incompatible avec sa source d’éléments
Désolé, impossible d’append un commentaire, mais tout cela est faux.
ObservableCollection n’est pas thread-safe. Non seulement à cause de ces problèmes de répartiteur, mais pas du tout (à partir de msdn):
Tous les membres publics static (Shared en Visual Basic) de ce type sont thread-safe. Tous les membres d’instance ne sont pas garantis pour être thread-safe.
Regardez ici http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx
Il y a aussi un problème lors de l’appel de BeginInvoke avec une action “Reset”. “Reset” est la seule action où le gestionnaire doit regarder la collection elle-même. Si vous commencez à invoquer un “Reset” et ensuite immédiatement BeginInvoke quelques actions “Add”, le gestionnaire acceptera un “Reset” avec une collection déjà mise à jour et le prochain “Add” créera un désordre.
Voici ma mise en œuvre qui fonctionne. En fait, je pense à supprimer BeginInvoke du tout:
Collecte rapide et collection observable sans fil
Si vous souhaitez mettre à jour régulièrement le contrôle de l’interface utilisateur de WPF et utiliser simultanément l’interface utilisateur, vous pouvez utiliser DispatcherTimer .
XAML
C #
public partial class DownloadStats : Window { private MainWindow _parent; DispatcherTimer timer = new DispatcherTimer(); ObservableCollection fileViewList = new ObservableCollection (); public DownloadStats(MainWindow parent) { InitializeComponent(); _parent = parent; Owner = parent; timer.Interval = new TimeSpan(0, 0, 1); timer.Tick += new EventHandler(timer_Tick); timer.Start(); } void timer_Tick(object sender, EventArgs e) { dgDownloads.ItemsSource = null; fileViewList.Clear(); if (_parent.contentManagerWorkArea.Count > 0) { foreach (var item in _parent.contentManagerWorkArea) { FileView nf = item.Value.FileView; fileViewList.Add(nf); } } if (fileViewList.Count > 0) { lblFileCouner.Content = fileViewList.Count; dgDownloads.ItemsSource = fileViewList; } } }
Aucun d’entre eux, utilisez simplement Dispatcher.BeginInvoke
Essaye ça:
this.Dispatcher.Invoke(DispatcherPriority.Background, new Action( () => { //Code }));
Voici une version VB que j’ai faite après quelques googlings et de légers mods. Travaille pour moi.
Imports System.Collections.ObjectModel Imports System.Collections.Specialized Imports System.ComponentModel Imports System.Reflection Imports System.Windows.Threading 'from: http://stackoverflow.com/questions/2137769/where-do-i-get-a-thread-safe-collectionview Public Class ThreadSafeObservableCollection(Of T) Inherits ObservableCollection(Of T) 'from: http://geekswithblogs.net/NewThingsILearned/archive/2008/01/16/listcollectionviewcollectionview-doesnt-support-notifycollectionchanged-with-multiple-items.aspx Protected Overrides Sub OnCollectionChanged(ByVal e As System.Collections.Specialized.NotifyCollectionChangedEventArgs) Dim doit As Boolean = False doit = (e.NewItems IsNot Nothing) AndAlso (e.NewItems.Count > 0) doit = doit OrElse ((e.OldItems IsNot Nothing) AndAlso (e.OldItems.Count > 0)) If (doit) Then Dim handler As NotifyCollectionChangedEventHandler = GetType(ObservableCollection(Of T)).GetField("CollectionChanged", BindingFlags.Instance Or BindingFlags.NonPublic).GetValue(Me) If (handler Is Nothing) Then Return End If For Each invocation As NotifyCollectionChangedEventHandler In handler.GetInvocationList Dim obj As DispatcherObject = invocation.Target If (obj IsNot Nothing) Then Dim disp As Dispatcher = obj.Dispatcher If (disp IsNot Nothing AndAlso Not (disp.CheckAccess())) Then disp.BeginInvoke( Sub() invocation.Invoke(Me, New NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)) End Sub, DispatcherPriority.DataBind) Continue For End If End If invocation.Invoke(Me, e) Next End If End Sub End Class
Petite erreur dans la version VB Il suffit de remplacer:
Dim obj As DispatcherObject = invocation.Target
Par
Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)