2009-03-02 5 views
10

Ich entwerfe gerade den Databinding-Teil einer Anwendung, die Winforms-Datenbindungen und -Updates nutzt, die von einem Hintergrundthread kommen (einmal pro Sekunde bei> 100 Datensätzen).WinForms Multithread-Databinding-Szenario, Best Practice?

Nehmen wir an, die Anwendung ist eine Börsenhandelsanwendung, bei der ein Hintergrundthread Datenänderungen überwacht und sie auf die Datenobjekte setzt. Diese Objekte werden in einem BindingList<> gespeichert und implementieren INotifyPropertyChanged, um die Änderungen über Datenbindung an die Winforms-Steuerelemente zu übermitteln. Außerdem ordnen die Datenobjekte derzeit die Änderungen per WinformsSynchronizationContext.Send dem UI-Thread zu. Der Benutzer kann einige Werte auf der Benutzeroberfläche eingeben, was bedeutet, dass einige Werte von beiden Seiten geändert werden können. Und die Benutzerwerte sollten nicht durch Updates überschrieben werden.

So gibt es mehrere Frage meiner Meinung nach kommen:

  • Gibt es eine allgemeine Design-Guildline, wie das zu tun (Hintergrund-Updates in Datenbindung)?
  • Wann und wie auf dem UI-Thread marshale?
  • Was ist der beste Weg des Hintergrundthreads zur Interaktion mit Binding/Datenobjekte?
  • Welche Klassen/Interfaces sollten verwendet werden? (Binding, ...)
  • ...

Die Benutzeroberfläche ist nicht wirklich wissen, dass es einen Hintergrund-Thread ist, dass die Steuerung aktualisiert, und wie mein Verständnis in Datenbindung Szenarien der UI shouldn‘ Ich weiß, woher die Daten kommen ... Sie können sich den Hintergrund-Thread als etwas vorstellen, das Daten an die Benutzeroberfläche weitergibt. Ich bin mir also nicht sicher, ob der Hintergrundarbeiter die Option ist, nach der ich suche.

Manchmal möchten Sie eine UI-Antwort während einer Operation im Daten-/Geschäftsobjekt erhalten (z. B. Hintergrund bei Neuberechnungen einstellen). Das Erhöhen einer Eigenschaft, die an einer Statuseigenschaft geändert wurde, die an den Hintergrund gebunden ist, reicht nicht aus, da die Steuerelemente nach Abschluss der Berechnung neu gezeichnet werden. Meine Idee wäre, das propertychanged Ereignis anzuhängen und .update() auf dem Steuerelement ... Irgendwelche anderen Ideen darüber zu nennen?

Antwort

6

Dies ist ein hartes Problem, da die meisten "Lösungen" zu viel benutzerdefiniertem Code und vielen Aufrufen anführenoder System.ComponentModel.BackgroundWorker (die selbst ist nur eine dünne wrapper über BeginInvoke).

In der Vergangenheit habe ich auch festgestellt, dass Sie bald Ihre INotifyPropertyChanged Ereignisse verzögern möchten, bis die Daten stabil sind. Der Code, der ein proprietäres Ereignis behandelt, muss oft andere Eigenschaften lesen. Sie haben auch häufig ein Steuerelement, das sich neu zeichnen muss, wenn sich der Status einer der vielen Eigenschaften ändert, und Sie das Steuerelement nicht zu oft selbst neu zeichnen möchten.

Erstens sollte jede benutzerdefinierte WinForms Kontrolle alle Daten lesen Sie es selbst in der PropertyChanged Event-Handler malen muss, so dass es braucht keine Datenobjekte zu sperren, wenn es sich um eine WM_PAINT (OnPaint) Nachricht war. Das Steuerelement sollte sich nicht sofort neu zeichnen, wenn es neue Daten erhält. stattdessen sollte es Control.Invalidate() anrufen. Windows wird die WM_PAINT Nachrichten in so wenige Anfragen wie möglich kombinieren und nur senden, wenn der UI-Thread nichts anderes zu tun hat. Dies minimiert die Anzahl der Neuzeichnungen und die Zeit, zu der die Datenobjekte gesperrt sind. (Standard-Steuerelemente verwenden dies meistens mit Datenbindung)

Die Datenobjekte müssen aufzeichnen, was sich geändert hat, sobald die Änderungen vorgenommen wurden, und nach Abschluss einer Reihe von Änderungen den UI-Thread zum Aufruf der SendChangeEvents "kicken" Methode, die dann den Ereignishandler PropertyChanged (im UI-Thread) für alle Eigenschaften aufruft, die sich geändert haben. Während die SendChangeEvents()-Methode ausgeführt wird, müssen die Datenobjekte gesperrt werden, um zu verhindern, dass die Hintergrundthreads sie aktualisieren.

Der UI-Thread kann mit einem Aufruf von BeginInvoke "gekickt" werden, wenn ein Satz von Updates aus der Datenbank gelesen wurde. Häufig ist es besser, wenn die UI-Thread-Abfrage einen Timer verwendet, da Windows nur die Nachricht WM_TIMER sendet, wenn die UI-Nachrichtenwarteschlange leer ist, was dazu führt, dass das UI besser reagiert.

Denken Sie auch daran, die Datenbindung überhaupt nicht zu verwenden und die Benutzeroberfläche jedes Datenobjekt "Was hat sich geändert" jedes Mal zu fragen, wenn der Timer ausgelöst wird. Databinding sieht immer gut aus, kann aber schnell Teil des Problems werden, anstatt Teil der Lösung.

Da das Sperren/Entsperren der Datenobjekte sehr mühsam ist und die Aktualisierungen möglicherweise nicht schnell genug aus der Datenbank gelesen werden können, empfiehlt es sich, dem UI-Thread eine (virtuelle) Kopie der Datenobjekte zu übergeben. Wenn das Datenobjekt persistent/unveränderlich ist, sodass Änderungen am Datenobjekt ein neues Datenobjekt zurückgeben, anstatt das aktuelle Datenobjekt zu ändern, kann dies aktiviert werden.

Persistente Objekte klingen sehr langsam, aber müssen nicht sein, siehe this und that für einige Zeiger. Sehen Sie sich auch this und that auf Stack Overflow.

Schauen Sie sich auch retlang - Message-based concurrency in .NET. Das Message-Batching kann nützlich sein.

(Für WPF hätte ich ein View-Model im UI-Thread, das dann in 'Batches' vom Multi-Thread-Modell durch den Hintergrund-Thread aktualisiert wurde. Allerdings ist WPF viel besser beim Kombinieren von Daten Bindungsereignisse dann WinForms.)

2

Es gibt eine MSDN article spezifische zu diesem Thema. Aber seien Sie bereit, VB.NET zu betrachten. ;)

Zusätzlich könnten Sie vielleicht System.ComponentModel.BackgroundWorker anstelle eines generischen zweiten Threads verwenden, da es die Art der Interaktion mit dem erzeugten Hintergrundthread, den Sie beschreiben, schön formalisiert. Das Beispiel, das in der MSDN-Bibliothek angegeben wird, ist ziemlich anständig, also schauen Sie es für einen Hinweis an, wie man es benutzt.

Bearbeiten: Hinweis: Es ist kein Marshalling erforderlich, wenn Sie das ProgressChanged-Ereignis verwenden, um zurück zum UI-Thread zu kommunizieren. Der Hintergrundthread ruft ReportProgress auf, wenn er mit der Benutzeroberfläche kommunizieren muss. Da es möglich ist, ein beliebiges Objekt an dieses Ereignis anzuhängen, gibt es keinen Grund für ein manuelles Marshalling. Der Fortschritt wird über eine andere asynchrone Operation kommuniziert. Sie müssen sich also weder darum kümmern, wie schnell die Benutzeroberfläche die Fortschrittsereignisse verarbeiten kann, noch ob der Hintergrundthread unterbrochen wird, indem Sie auf das Ende des Ereignisses warten.

Wenn Sie beweisen, dass der Hintergrund-Thread das Fortschrittsänderungs-Ereignis viel zu schnell anhebt, dann sollten Sie vielleicht einen exzellenten Artikel von Ayende unter Pull vs. Push models for UI updates betrachten.

+1

System.ComponentModel.BackgroundWorker, kann einen Teil der Lösung sein, aber das harte Problem ist, wie die Daten zu halten aktualisiert schnell, ohne den UI-Thread Blick. Tun Sie das oben, ohne dass jede andere Codezeile über Sperren oder Cross-Thread-Aufrufe nachdenken muss. –

+0

Wenn man sich den Backgroundworker anschaut, gibt es keinen großen Unterschied, einen Thread zu erzeugen und mit WindowsFormsSynchronizationContext zu marshallen. Das BW macht das gleiche. –

+0

Siehe Bearbeiten für Antwort. – Dun3

0

Dies ist ein Problem, das ich in Update Controls gelöst habe. Ich bringe das nicht dazu, Ihnen vorzuschlagen, Ihren Code neu zu schreiben, sondern Ihnen eine Quelle zu geben, auf der Sie nach Ideen suchen können.

Die Technik, die ich in WPF verwendet, war Dispatcher.BeginInvoke, um den Vordergrund Thread einer Änderung zu benachrichtigen. Sie können dasselbe in Winforms mit Control.BeginInvoke tun. Leider müssen Sie einen Verweis auf ein Form-Objekt in Ihr Datenobjekt übergeben.

Sobald Sie dies getan haben, können Sie eine Aktion in BeginInvoke übergeben, die PropertyChanged auslöst. Beispiel:

Sie müssen die Eigenschaften in Ihrem Datenobjekt sperren, um sie threadsicher zu machen.

2

Ja alle Bücher zeigen Gewinde Strukturen und ruft etc. Welches vollkommen richtig etc, aber es kann ein Schmerz zu Code, und oft schwer zu organisieren, so dass Sie anständig Tests für sie

A UI machen nur muss so viele Male pro Sekunde aufgefrischt werden, so dass Leistung nie ein Problem ist, und die Abfrage wird gut funktionieren

Ich mag ein Objektdiagramm, das kontinuierlich von einem Pool von Hintergrundthreads aktualisiert wird. Sie prüfen für tatsächliche Änderungen in Datenwerten, und wenn sie eine tatsächliche Veränderung bemerken, dass sie an der Wurzel des Objekts Graph, der einen Versionszähler aktualisieren (oder auf jeder Hauptsache, was mehr Sinn macht) und aktualisieren die Werte

Dann Vordergrundprozess kann einen Timer haben (gleich wie UI-Thread standardmäßig), um eine Sekunde oder so zu feuern und den Versionszähler zu überprüfen, und wenn es sich ändert, sperrt es (um partielle Updates zu stoppen) und aktualisiert dann die Anzeige

Diese einfache Technik isoliert vollständig den UI-Thread von den Hintergrundthreads

+0

Ich habe festgestellt, dass Timer in der Vergangenheit gut funktionieren. –

1

Ich kämpfte gerade eine ähnliche Situation - badkground Thread Aktualisierung der Benutzeroberfläche über BeginInvokes. Der Hintergrund hat eine Verzögerung von 10ms in jeder Schleife, aber auf der anderen Seite stieß ich auf Probleme, bei denen die UI-Updates, die manchmal bei jeder Schleife ausgelöst werden, nicht mit den vielen Updates mithalten können und die App effektiv nicht mehr funktioniert (nicht sicher, was passiert - blies einen Stapel?).

Ich fügte ein Flag in das Objekt über den Aufruf übergeben, die nur ein bereites Flag war. Ich würde dies vor dem Aufruf des Aufrufs auf false setzen, und dann würde der bg-Thread keine weiteren Aktualisierungen mehr durchführen, bis dieses Flag wieder auf true gesetzt ist. Der UI-Thread würde die Bildschirmaktualisierungen usw. ausführen und diese Variable dann auf "true" setzen.

Dies erlaubte dem bg-Thread, weiter zu knirschen, erlaubte es dem ui jedoch, den Fluss zu sperren, bis er für mehr bereit war.

+0

der einzige Teil, mit dem ich mich nicht sicher bin, ist, warum die Flagge von beiden Threads zugänglich sein darf. aber es scheint einfach zu funktionieren. –

0

Erstellen Sie ein neues UserControl, fügen Sie Ihr Steuerelement hinzu und formatieren Sie es (vielleicht dock = fill) und fügen Sie eine Eigenschaft hinzu. Konfigurieren Sie jetzt die Eigenschaft zum Aufrufen des Benutzersteuerelements und aktualisieren Sie Ihr Element jedes Mal, wenn Sie das Eigenschaftsformular eines gewünschten Threads ändern!

das ist meine Lösung:

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

auf meiner Form ich meine Ansicht binden

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

Dieser Beitrag ist alt, aber ich dachte, dass ich Optionen anderen geben würde. Wenn Sie mit der asynchronen Programmierung und der Windows Forms-Datenbindung beginnen, werden Probleme mit der Aktualisierung der Bindingsource-Datenquelle oder der Aktualisierung von Listen angezeigt, die an die Windows-Formularsteuerung gebunden sind. Ich werde versuchen, Jeffrey Richters AsyncEnumerator Klasse von seinen PowerThreading-Tools auf WinTitelect zu verwenden.

Grund: 1.Seine AsyncEnumerator-Klasse leitet Hintergrundthreads automatisch an UI-Threads weiter, sodass Sie die Steuerelemente wie bei synchronem Code aktualisieren können. 2. AsyncEnumerator vereinfacht die Async-Programmierung. Es tut dies automatisch, also schreiben Sie Ihren Code synchron, aber der Code läuft immer noch asynchron.

Jeffrey Richter hat ein Video auf Channel 9 MSDN, das AsyncEnumerator erklärt.

Wünschen Sie mir Glück.

-R