2009-03-02 23 views
10

Attualmente sto progettando/rilavorando la parte di associazione dati di un'applicazione che fa un uso pesante di WinCat Databinding e aggiornamenti provenienti da un thread in background (una volta al secondo su> 100 record).Scenario databinding multi-thread WinForms, best practice?

Supponiamo che l'applicazione sia un'applicazione di compravendita di azioni, in cui un thread in background monitora le modifiche dei dati e li inserisce negli oggetti dati. Questi oggetti sono memorizzati in un BindingList<> e implementano INotifyPropertyChanged per propagare le modifiche tramite associazione dati ai controlli Winforms. Inoltre gli oggetti dati stanno attualmente eseguendo il marshalling delle modifiche tramite WinformsSynchronizationContext.Send nel thread dell'interfaccia utente. L'utente è in grado di inserire alcuni dei valori nell'interfaccia utente, il che significa che alcuni valori possono essere modificati da entrambi i lati. E i valori utente non dovrebbero essere sovrascritti dagli aggiornamenti.

Quindi ci sono diversi questione venire in mente:

  • C'è un generale disegno-guildline come fare (aggiornamenti in background in associazione dati)?
  • Quando e come effettuare il marshalling sul thread dell'interfaccia utente?
  • Qual è il modo migliore del thread in background per interagire con gli oggetti binding/dati ?
  • Quali classi/interfacce dovrebbero essere utilizzate? (BindingSource, ...)
  • ...

L'interfaccia utente in realtà non sanno che c'è un thread in background, che aggiorna il controllo, ed a partire da mia comprensione in scenari di associazione dati del shouldn UI' so da dove provengono i dati ... Puoi pensare al thread in background come a qualcosa che spinge i dati nell'interfaccia utente, quindi non sono sicuro che il backgroundworker sia l'opzione che sto cercando.

A volte si desidera ottenere una risposta dell'interfaccia utente durante un'operazione nell'oggetto dati/business (ad esempio l'impostazione dello sfondo durante i ricalcoli). Aumentare un propertychanged su una proprietà status legata allo sfondo non è sufficiente, in quanto il controllo viene ridipinto dopo che il calcolo è terminato? La mia idea sarebbe quella di agganciare l'evento propertychanged e chiamare .update() sul controllo ... Qualche altra idea al riguardo?

risposta

6

Questo è un problema difficile poiché la maggior parte delle "soluzioni" porta a un sacco di codice personalizzato e molte chiamate a BeginInvoke() o System.ComponentModel.BackgroundWorker (che di per sé è solo un involucro sottile su BeginInvoke).

In passato, ho anche scoperto che desideri ritardare l'invio degli eventi INotifyPropertyChanged finché i dati non sono stabili. Il codice che gestisce un evento cambiato proprietà spesso ha bisogno di leggere altre proprietà. Spesso hai anche un controllo che deve ridisegnarsi da solo ogni volta che cambia lo stato di una delle tante proprietà, e non si perde il controllo per ridisegnarsi troppo spesso.

In primo luogo, ogni controllo WinForms personalizzati dovrebbe leggere tutti i dati di cui ha bisogno per dipingere se stessa nel gestore PropertyChanged eventi, in modo da non avere bisogno di bloccare eventuali oggetti dati, quando era un (OnPaint) Messaggio WM_PAINT. Il controllo non dovrebbe ridisegnarsi immediatamente quando riceve nuovi dati; invece, dovrebbe chiamare Control.Invalidate(). Windows combina i messaggi WM_PAINT nel minor numero possibile di richieste e li invia solo quando il thread dell'interfaccia utente non ha altro da fare. Questo riduce al minimo il numero di ridisegni e il tempo in cui gli oggetti dati sono bloccati. (I controlli standard lo fanno per lo più con l'associazione dei dati)

Gli oggetti dati devono registrare ciò che è cambiato quando vengono apportate le modifiche, quindi una volta che una serie di modifiche è stata completata, "kick" il thread dell'interfaccia utente nel chiamare lo SendChangeEvents metodo che chiama quindi il gestore di eventi PropertyChanged (sul thread dell'interfaccia utente) per tutte le proprietà che sono state modificate. Mentre è in esecuzione il metodo SendChangeEvents(), gli oggetti dati devono essere bloccati per impedire ai thread in background di aggiornarli.

Il thread dell'interfaccia utente può essere "kickato" con una chiamata a BeginInvoke ogni volta che un gruppo di aggiornamenti ha il bean read dal database. Spesso è meglio avere il polling del thread UI usando un timer, poiché Windows invia il messaggio WM_TIMER solo quando la coda dei messaggi UI è vuota, quindi l'interfaccia utente sembra più reattiva.

Considerare inoltre di non utilizzare il binding di dati e l'interfaccia utente chiede a ciascun oggetto di dati "cosa è cambiato" ogni volta che il timer scatta. Il databinding sembra sempre bello, ma può diventare rapidamente parte del problema, piuttosto che parte della soluzione.

Poiché il blocco/sblocco degli oggetti dati è un problema e potrebbe non consentire agli aggiornamenti di essere letti dal database abbastanza velocemente, è possibile passare il thread dell'interfaccia utente a una copia (virtuale) degli oggetti dati. Avere l'oggetto dati essere persistente/immutabile in modo che qualsiasi modifica dell'oggetto dati restituisca un nuovo oggetto dati anziché modificare l'oggetto dati corrente possa abilitarlo.

Gli oggetti persistenti suonano molto lentamente, ma non è necessario, vedere this e that per alcuni indicatori. Guarda anche this e that su Stack Overflow.

Anche dare un'occhiata a retlang - Message-based concurrency in .NET. Il suo batch di messaggi potrebbe essere utile.

(Per WPF, avrei un View-Model che imposta nel thread dell'interfaccia utente che è stato quindi aggiornato in "batch" dal modello multi-threaded dal thread in background.Tuttavia, WPF è molto meglio nella combinazione dei dati eventi vincolanti quindi WinForms.)

2

C'è uno specifico MSDN article su questo argomento. Ma preparati a guardare VB.NET. ;)

Inoltre, è possibile utilizzare System.ComponentModel.BackgroundWorker anziché un secondo thread generico, poiché formalizza in modo appropriato il tipo di interazione con il thread di background generato che si sta descrivendo. L'esempio fornito nella libreria MSDN è abbastanza decente, quindi guardalo per un suggerimento su come usarlo.

Modifica: Nota: non è richiesto il marshalling se si utilizza l'evento ProgressChanged per comunicare nuovamente al thread dell'interfaccia utente. Il thread in background chiama ReportProgress ogni volta che ha la necessità di comunicare con l'interfaccia utente. Poiché è possibile allegare qualsiasi oggetto a quell'evento, non c'è motivo di effettuare il marshalling manuale. Il progresso viene comunicato tramite un'altra operazione asincrona, quindi non c'è bisogno di preoccuparsi né della velocità con cui l'interfaccia utente può gestire gli eventi di avanzamento né se il thread in background viene interrotto attendendo che l'evento finisca.

Se si dimostra che il thread in background sta aumentando il progresso dell'evento modificato troppo velocemente, si potrebbe voler guardare a Pull vs. Push models for UI updates un eccellente articolo di Ayende.

+1

System.ComponentModel.BackgroundWorker, può essere parte della soluzione, ma il problema più difficile è come mantenere i dati aggiorna velocemente senza guardare fuori dal thread dell'interfaccia utente. Fare quanto sopra senza che ogni altra riga di codice debba pensare al blocco o alle chiamate incrociate. –

+0

Se guardi il Backgroundworker non c'è molta differenza per generare un thread e maresciallo con WindowsFormsSynchronizationContext il BW fa lo stesso. –

+0

Vedere la modifica per la risposta. – Dun3

0

Questo è un problema risolto in Update Controls. Vi presento questo per non suggerire di riscrivere il codice, ma di darvi qualche fonte per cercare idee.

La tecnica che ho usato in WPF era usare Dispatcher.BeginInvoke per notificare il thread in primo piano di una modifica. Puoi fare la stessa cosa in Winforms con Control.BeginInvoke. Sfortunatamente, devi passare un riferimento a un oggetto Form nel tuo oggetto dati.

Una volta eseguito, è possibile passare un'azione a BeginInvoke che attiva PropertyChanged. Ad esempio:

_form.BeginInvoke(new Action(() => NotifyPropertyChanged(propertyName)))); 

È necessario bloccare le proprietà nell'oggetto dati per renderle thread-safe.

2

strutture Sì tutti i libri mostrano filettato e invoca ecc che è perfettamente corretto, ecc, ma può essere un dolore al codice, e spesso difficile da organizzare in modo da poter effettuare i test decente per esso

un'interfaccia utente unica ha bisogno di essere aggiornato tante volte al secondo, quindi le prestazioni non sono mai un problema, e il polling funzionerà bene

Mi piace usare un oggetto grafico che viene continuamente aggiornato da un pool di thread in background. Controllano le modifiche effettive nei valori dei dati e quando notano un cambiamento effettivo aggiornano un contatore di versione sulla radice del grafico dell'oggetto (o su ogni elemento principale ha più senso) e aggiorna i valori

Quindi il processo in primo piano può avere un timer (come il thread UI di default) per sparare una volta al secondo e controllare il contatore delle versioni, e se cambia, blocca (per interrompere gli aggiornamenti parziali) e poi aggiorna il display

Questa semplice tecnica isola completamente il thread dell'interfaccia utente dai thread di sfondo

+0

Ho trovato che i timer funzionano bene in passato. –

1

Ho appena combattuto una situazione simile: thread di badkground che aggiornava l'interfaccia utente tramite BeginInvokes. Lo sfondo ha un ritardo di 10 ms su ogni ciclo, ma lungo la strada mi sono imbattuto in problemi in cui gli aggiornamenti dell'interfaccia utente che a volte vengono attivati ​​ogni volta su quel ciclo, non riescono a tenere il passo con gli aggiornamenti e l'app smette efficacemente di funzionare (non sono sicuro di cosa succederà, ha fatto esplodere una pila?).

Ho finito per aggiungere una bandiera nell'oggetto passato sopra la chiamata, che era solo una bandiera pronta. Avrei impostato su false prima di chiamare invoke, e quindi il thread bg non avrebbe più eseguito aggiornamenti ui fino a quando questo flag non è tornato su true. Il thread dell'interfaccia utente farebbe gli aggiornamenti dello schermo, ecc, e quindi impostare questa var su true.

Ciò ha permesso al filo di bg di continuare a sgranocchiare, ma ha permesso all'interfaccia utente di interrompere il flusso finché non era pronto per altri.

+0

l'unica parte in cui non si è sicuri di ciò è il motivo per cui il flag può essere accessibile da entrambi i thread. ma sembra funzionare. –

0

Creare un nuovo UserControl, aggiungere il controllo e formattarlo (forse dock = fill) e aggiungere una proprietà. ora configura la proprietà per invocare usercontrol e aggiornare il tuo elemento, ogni volta che modifichi la proprietà da qualsiasi thread vuoi!

thats il mio soluzione:

private long value; 
    public long Value 
    { 
     get { return this.value; } 
     set 
     { 
      this.value = value; 

      UpdateTextBox(); 
     } 
    } 

    private delegate void Delegate(); 
    private void UpdateTextBox() 
    { 
     if (this.InvokeRequired) 
     { 
      this.Invoke(new Delegate(UpdateTextBox), new object[] {}); 
     } 
     else 
     { 
      textBox1.Text = this.value.ToString(); 
     } 
    } 

sulla mia forma mi legano mio avviso

viewTx.DataBindings.Add(new Binding("Value", ptx.CounterTX, "ReturnValue")); 
0

Questo post è vecchio, ma ho pensato di dare le opzioni per gli altri. Sembra che una volta avviata la programmazione asincrona e l'associazione dei dati Windows Form si ottengano problemi nell'aggiornamento dell'origine dati Bindingsource o degli elenchi di aggiornamento associati al controllo dei moduli Windows. Ho intenzione di provare a utilizzare la classe AsyncEnumerator di Jeffrey Richters dai suoi strumenti di powerthreading su wintellect.

Motivo: 1.La sua classe AsyncEnumerator esegue automaticamente il marshalling di thread in background in thread UI in modo da poter aggiornare i controlli come si farebbe con il codice sincrono. 2. AsyncEnumerator semplifica la programmazione Async. Lo fa automaticamente, quindi scrivi il tuo codice in modo sincrono, ma il codice è ancora in esecuzione in modo asincrono.

Jeffrey Richter ha un video su Channel 9 MSDN, che spiega AsyncEnumerator.

Auguratemi buona fortuna.

-R