2009-12-10 5 views
6

Ho unaINotifyPropertyChanged con fili

BindingList<T> 

che è destinato a un datagridview. Una proprietà della mia classe richiede molto tempo per calcolare, quindi ho eseguito l'azione. Dopo il calcolo, sollevo l'evento OnPropertyChanged() per notificare alla griglia che il valore è pronto.

Almeno, questa è la teoria. Ma poiché il metodo OnPropertyChanged viene chiamato da un thread diverso, ottengo alcune eccezioni weired nel metodo OnRowPrePaint della griglia.

Qualcuno può dirmi come anticipare l'evento OnPropertyChanged da trarre nel thread principale? Non riesco a utilizzare Form.Invoke, poiché la classe MyClass non è a conoscenza dell'esecuzione in un'applicazione Winforms.

public class MyClass : INotifyPropertyChanged 
{ 
    public int FastMember {get;set;} 

    private int? slowMember; 
    public SlowMember 
    { 
     get 
     { 
      if (slowMember.HasValue) 
       return slowMember.Value; 
      else 
      { 
       Thread t = new Thread(getSlowMember); 
       t.Start(); 
       return -1; 
      } 

     } 
    } 

    private void getSlowMember() 
    { 
     Thread.Sleep(1000); 
     slowMember = 5; 
     OnPropertyChanged("SlowMember"); 
    } 

    public event PropertyChangedEventHandler PropertyChanged; 
    private void OnPropertyChanged(string propertyName) 
    { 
     PropertyChangingEventHandler eh = PropertyChanging; 
     if (eh != null) 
     { 
      eh(this, e); 
     } 
    } 

} 

risposta

8

In base alla progettazione, un controllo può essere aggiornato solo dal thread in cui è stato creato. Questo è il motivo per cui si ottengono eccezioni.

Considerare l'utilizzo di BackgroundWorker e aggiornare il membro solo dopo che l'operazione di lunga durata è stata completata iscrivendo un gestore di eventi a RunWorkerCompleted.

+0

Funziona come un fascino. Fino ad ora non sapevo di BackgroundWorker. Questo rende questa attività così facile, grazie mille. –

1

Considerazione 1:
Date un'occhiata a classe UIThreadMarshal e il suo utilizzo in questo articolo:
UI Thread Marshaling in the Model Layer
È possibile cambiare la classe da statico a istanza e iniettarlo nel vostro oggetto. Quindi il tuo oggetto non conoscerà la classe Form. Saprà solo della classe UIThreadMarshal.

Considerazione 2:
Non penso che restituire -1 dalla vostra proprietà sia una buona idea. Mi sembra un cattivo design.

Considerazione 3:
Forse la tua classe non dovrebbe usare il thread antoher. Forse sono le classi consumer che dovrebbero decidere come chiamare la tua proprietà: direttamente o in una thread separata. In questo caso, forse è necessario fornire proprietà aggiuntive, come IsSlowMemberInitialized.

+0

Per 1: Grazie per il link. The BackgroundWorker ha risolto il mio problema in questo caso, ma scommetto che i miei pantaloncini mi serviranno nel prossimo futuro. A 2: Hai ragione, specialmente perché SlowMember può essere -1. Era solo per il test Per 3: non possibile, perché il DataGridView interroga il valore (e ottiene -1 per la prima volta, quindi aggiorno il valore e uso l'interfaccia INotifyPropertyChanged per informare la vista datagrid della proprietà modificata, che deve (Ok potrei usare un Timer e controllare IsSlowMemberInitialized = true ma questo è brutto In ogni caso, molto –

+0

Se usi DataGridView, allora forse hai bisogno di usare BindingSource.Nel link ti ho dato, esiste un'implementazione di BindingSource che supporta l'associazione da diversi thread Puoi lavorare su quel codice per renderlo più adatto alle tue esigenze – nightcoder

2

Ecco qualcosa che ho scritto qualche tempo fa; dovrebbe funzionare ragionevolmente bene, ma nota il costo di un sacco di aggiornamenti ...

using System.ComponentModel; 
using System.Threading; 
public class ThreadedBindingList<T> : BindingList<T> { 
    SynchronizationContext ctx = SynchronizationContext.Current; 
    protected override void OnAddingNew(AddingNewEventArgs e) { 
     if (ctx == null) { BaseAddingNew(e); } 
     else { ctx.Send(delegate { BaseAddingNew(e); }, null); } 
    } 
    protected override void OnListChanged(ListChangedEventArgs e) { 
     if (ctx == null) { BaseListChanged(e); } 
     else { ctx.Send(delegate { BaseListChanged(e); }, null); } 
    } 
    void BaseListChanged(ListChangedEventArgs e) { base.OnListChanged(e); } 
    void BaseAddingNew(AddingNewEventArgs e) { base.OnAddingNew(e); } 
} 
+0

Implementazione interessante Marc, ma poiché ciò consente di progettare male, penso che dovrebbe essere usato solo in determinati scenari dove è effettivamente necessario il controllo da aggiornare mentre l'azione è in elaborazione. –

6

La gente a volte dimentica che il gestore di eventi è un MultiCastDelegate e, come tale, ha tutte le informazioni relative a ciascun abbonato che abbiamo è necessario gestire questa situazione con garbo senza imporre inutilmente la penalizzazione delle prestazioni Invoke + Synchronization. Sto usando il codice come questo per le età:

using System.ComponentModel; 
// ... 

public event PropertyChangedEventHandler PropertyChanged; 

protected virtual void OnPropertyChanged(string propertyName) 
{ 
    var handler = PropertyChanged; 
    if (handler != null) 
    { 
     var e = new PropertyChangedEventArgs(propertyName); 
     foreach (EventHandler h in handler.GetInvocationList()) 
     { 
      var synch = h.Target as ISynchronizeInvoke; 
      if (synch != null && synch.InvokeRequired) 
       synch.Invoke(h, new object[] { this, e }); 
      else 
       h(this, e); 
     } 
    } 
} 

Ciò che non è semplice, ma ricordo che ho quasi ruppi il mio cervello torna poi cercando di trovare il modo migliore per farlo.

Prima "afferra" il gestore eventi su una proprietà locale per evitare qualsiasi condizione di gara.

Se il gestore non è null (al momento dell'affitto esiste un sottoscrittore) prepara gli argomenti di evento e quindi scorre attraverso l'elenco di chiamate di questo delegato multicast.

L'elenco di chiamate ha la proprietà target, che è il sottoscrittore dell'evento.Se questo sottoscrittore implementa ISynchronizeInvoke (tutti i controlli dell'interfaccia utente lo implementano), quindi controlliamo la sua proprietà InvokeRequired, ed è vero che abbiamo appena richiamato il delegato e i parametri. Chiamandolo in questo modo sincronizzerà la chiamata nel thread dell'interfaccia utente.

In caso contrario, è sufficiente chiamare direttamente il delegato del gestore eventi.

+2

Ho dovuto rinominare "EventHandler' a' PropertyChangedEventHandler' perché stavo ottenendo un 'System.InvalidCastException' con il dettaglio' {"Impossibile eseguire il cast dell'oggetto di tipo 'System.ComponentModel.PropertyChangedEventHandler' per scrivere 'System.EventHandler'." } ' Ho un BindingList creato nel thread dell'interfaccia utente che sottoscrive l'evento internamente, ma la variabile sync restituisce sempre null perché h.Target è null. –

+0

Sto riscontrando lo stesso problema di @RickShealer. Notando la data mi chiedo se questo è un problema con le versioni più recenti di .Net? Questa sembra una soluzione molto elegante al problema di cross-thread INotifyPropertyChanged, quindi spero che riusciremo a farlo funzionare. – Jacob

+0

@Jacob Mi rivolgerò ai framework più recenti per vedere se fallisce. Puoi dirmi la versione del tuo framework di riferimento del progetto o qualsiasi altra informazione che ritieni pertinente? – Loudenvier