2010-01-26 3 views
64

Quando si aggiorna una collezione di oggetti di business su un thread in background ricevo questo messaggio di errore:Dove posso ottenere un CollectionView sicuro per i thread?

Questo tipo di CollectionView non supporta le modifiche alla sua SourceCollection da un thread diverso dal thread Dispatcher.

Ok, questo ha senso. Ma pone anche la domanda, quale versione di CollectionView supporta più thread e come faccio a far sì che i miei oggetti lo utilizzino?

+1

provare il collegamento seguente che fornisce una soluzione thread-safe che funziona da qualsiasi thread e può essere legato a via più thread UI: http: //www.codeproject .com/Articles/64936/Multithreaded-ObservableImmutableCollection – Anthony

risposta

64

Quello che segue è un miglioramento sull'attuazione trovato da Jonathan. Innanzitutto esegue ogni gestore di eventi sul dispatcher ad esso associato, piuttosto che supponendo che siano tutti sullo stesso dispatcher (UI). In secondo luogo utilizza BeginInvoke per consentire all'elaborazione di continuare mentre attendiamo che il dispatcher sia disponibile. Ciò rende la soluzione molto più veloce nelle situazioni in cui il thread in background sta eseguendo molti aggiornamenti con l'elaborazione tra ciascuno di essi. Forse, ancora più importante, risolve i problemi causati dal blocco durante l'attesa di Invoke (i deadlock possono verificarsi ad esempio quando si utilizza WCF con ConcurrencyMode.Single).

public class MTObservableCollection<T> : ObservableCollection<T> 
{ 
    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); 
      } 
    } 
} 

Poiché stiamo usando BeginInvoke, è possibile che il cambiamento dalla notifica viene annullata prima che il gestore è chiamato. Ciò in genere si traduce in un "Indice non compreso nell'intervallo."l'eccezione viene generata quando gli argomenti dell'evento vengono verificati rispetto al nuovo stato (alterato) della lista.Per evitare ciò, tutti gli eventi in ritardo vengono sostituiti con Ripristina eventi.Questo potrebbe causare un eccessivo ridisegno in alcuni casi.

+1

Un po 'in ritardo e un vecchio argomento ma questo bit di codice mi ha salvato un sacco di mal di testa, grazie! :) – KingTravisG

+0

Caliburn ha anche un'implementazione davvero bella nella sua BindableCollection . Date un'occhiata qui: http://caliburn.codeplex.com/SourceControl/changeset/view/e80dd6b444f4#src/Caliburn.PresentationFramework/BindableCollection.cs – Stephanvs

+0

Che cosa significa MT? – guilhermecgs

0

Nessuno di loro, basta usare Dispatcher.BeginInvoke

+0

Che vanifica lo scopo di avere thread in background e un livello di dati indipendente. –

+3

No, non funziona: tutto il lavoro è recuperare i dati/elaborarli; lo fai nel thread in background, quindi usa Dispatcher.BeginInvoke per spostarlo nella raccolta (che richiede pochissimo tempo). –

7

uno Found.

public class MTObservableCollection<T> : ObservableCollection<T> 
{ 
    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

+3

Si noti che ciò causerà un cambio di thread per ogni modifica della raccolta e che tutte le modifiche sono serializzate (che vanifica lo scopo di avere thread in background :-)). Per alcuni articoli non ha importanza, ma se hai intenzione di aggiungere molti oggetti, danneggerà molto le prestazioni. Di solito aggiungo gli oggetti a un'altra raccolta nel thread in background e li sposto alla raccolta gui su un timer. – adrianm

+1

Posso conviverci. Il costo che sto cercando di evitare è il recupero degli articoli, in quanto bloccherà l'interfaccia utente. Aggiungendoli alla collezione è economico rispetto al confronto. –

+0

@adrianm Sono interessato alla tua osservazione: cosa intendi per "serializzazione" in questo caso? E hai un esempio di "passare alla collezione gui su un timer"? – Gerard

17

This messaggio di Bea Stollnitz spiega che messaggio di errore e il motivo per cui è formulata così com'è.

EDIT: dal blog di Bea

Purtroppo, questo codice si traduce in un'eccezione: “NotSupportedException - Questo tipo di CollectionView non supporta le modifiche alla sua SourceCollection da un thread diverso dal thread Dispatcher” Capisco che questo messaggio di errore induca le persone a pensare che, se il CollectionView che stanno utilizzando non supporta le modifiche cross-thread, quindi devono trovare quello che fa. Bene, questo messaggio di errore è un po 'fuorviante: nessuna delle CollectionViews che forniamo supporta i cambiamenti di raccolta cross-thread. E no, sfortunatamente non possiamo correggere il messaggio di errore a questo punto, siamo molto bloccati.

+0

Mi piace l'implementazione del marchio, ma devo dare il merito di aver trovato la spiegazione migliore. –

87

Usa:

System.Windows.Application.Current.Dispatcher.Invoke(
    System.Windows.Threading.DispatcherPriority.Normal, 
    (Action)delegate() 
    { 
     // Your Action Code 
    }); 
+0

Semplice, elegante, dritto al punto, lo adoro ... L'ho imparato per sempre. Grazie. –

+8

utilizzando 'Invoke' provoca il congelamento dell'interfaccia utente. Usa invece 'BeginInvoke'. – Xaqron

+1

@MonsterMMORPG Questa soluzione con .BeginInvoke invece. Invoke è una buona risposta. – amaranth

1

Se si desidera aggiornare periodicamente WPF UI di controllo e allo stesso tempo utilizzare interfaccia utente è possibile utilizzare DispatcherTimer.

XAML

<Grid> 
     <DataGrid AutoGenerateColumns="True" Height="200" HorizontalAlignment="Left" Name="dgDownloads" VerticalAlignment="Top" Width="548" /> 
     <Label Content="" Height="28" HorizontalAlignment="Left" Margin="0,221,0,0" Name="lblFileCouner" VerticalAlignment="Top" Width="173" /> 
</Grid> 

C#

public partial class DownloadStats : Window 
    { 
     private MainWindow _parent; 

     DispatcherTimer timer = new DispatcherTimer(); 

     ObservableCollection<FileView> fileViewList = new ObservableCollection<FileView>(); 

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

    } 
+0

Questa è un'ottima soluzione, ma c'è un errore Clark, quando si crea l'istanza del timer, affinché funzioni, è necessario passare Application Dispatcher ad esso! Puoi fare nel costruttore passando, oltre alla priorità, l'oggetto System.Windows.Application.Current.Dispatcher! – Andry

0

Prova Questo:

this.Dispatcher.Invoke(DispatcherPriority.Background, new Action(
() => 
{ 

//Code 

})); 
0

Ecco una versione VB che ho fatto dopo un po 'googling e lievi mods funziona per me

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 
2

Siamo spiacenti, non è possibile aggiungere un commento, ma tutto questo è sbagliato.

ObservableCollection non è thread-safe. Non solo a causa di questi problemi di dispatcher, ma non è affatto sicuro (da msdn):

Qualsiasi membro statico pubblico (Shared in Visual Basic) di questo tipo è thread-safe. Non è garantito che tutti i membri di istanza siano thread-safe.

Guardate qui http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx

C'è anche un problema quando si chiama BeginInvoke con un "Reset" azione. "Ripristina" è l'unica azione in cui il gestore deve esaminare la raccolta stessa. Se BeginInvoke un "Reset" e quindi BeginInvoke immediatamente un paio di azioni "Aggiungi" rispetto al gestore accetterà un "Reset" con raccolta già aggiornato e il prossimo "Aggiungi" creerà un disastro.

Ecco la mia implementazione che funziona. In realtà sto pensando di rimuovere BeginInvoke affatto:

Fast performing and thread safe observable collection

0

piccolo errore nella versione VB. Basta sostituire:

Dim obj As DispatcherObject = invocation.Target 

Con

Dim obj As DispatcherObject = TryCast(invocation.Target, DispatcherObject)