Laden...

2 DataGrids kontinuierlich aktualisieren

Erstellt von C#Matze vor 9 Jahren Letzter Beitrag vor 9 Jahren 3.614 Views
C
C#Matze Themenstarter:in
31 Beiträge seit 2014
vor 9 Jahren
2 DataGrids kontinuierlich aktualisieren

Hallo zusammen,

ich versuche, Daten aus 2 Queues (von einem parallelen Thread gefüllt) im UI-Thread zu visualisieren (WPF, .NET4.0).

Damit die Visualisierung nicht zu schnell ist, nutze ich einen Timer, der alle 500 ms aufgerufen wird und die Queues abarbeitet:

private void dispatcherTimer_Tick(object sender, EventArgs e)
{
	for (int i = 0; i < _guiBufferQueueLogNOK.Count; i++)
	{
		_globalData.DataBufferGUI_Lock.AddElement(_dtLogNOK_Lock, GlobalTypesDatabase.GUIDataType.LogDataNOK,
			 _guiBufferQueueLogNOK.Dequeue());
	}

	for (int i = 0; i < _guiBufferQueueLogMain.Count; i++)
	{
		_globalData.DataBufferGUI_Lock.AddElement(_dtLogMain_Lock, GlobalTypesDatabase.GUIDataType.LogDataMain,
			_guiBufferQueueLogMain.Dequeue());
	}
}}

Bei "_dtLogNOK_Lock" und "_dtLogMain_Lock" handelt es sich um je eine DataTable, die über Binding mit je einem dataGrid verbunden ist.

Die Methode "AddElement()" fügt einzelne DataRows einer DataTable hinzu und sieht wie folgt aus:

public void AddElement(DataTable refDt, GlobalTypesDatabase.GUIDataType guiType, DataRow dataTableRow)
{
	const int bufferSize = 100;

	// ring buffer to have always 100 elements within the DataGrid
	lock (refDt)
	{
		Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
			{
				// add element
				refDt.Rows.InsertAt(dataTableRow, 0);

				for (int i = refDt.Rows.Count - 1; i > bufferSize - 1; i--)
				{
					// remove last element
					refDt.Rows.RemoveAt(i);
				}
			}));
	}
}

Lasse ich "Dispatcher.BeginInvoke()" hier weg, reagiert die Anwendung auf Benutzereingaben sehr träge. Wenn ich es verwende, reagiert die Anwendung hervorragend, aber die beiden DataGrids werden nacheinander gefüllt und nicht parallel (sie warten auf sich gegenseitig).
Verwende ich stattdessen "Dispatcher.BeginInvoke()" im Timer (so dass beide For-Schleifen eingeschlossen sind), werden beide DataGrids parallel aktualisiert, aber nicht fortlaufend, sondern langsamer als der Timer selbst und die Anwendung reagiert wieder sehr träge auf Benutzereingaben.

Wie ihr sicher merkt, verstehe ich noch nicht, wie die Synchronisierung mit dem UI-Thread genau funktioniert. Ich habe mir einige Tutorials angeguckt und sogar ein Buch gewälzt, aber mit den 2 DataGrids funktioniert das irgendwie nicht.

Kann mir jemand von euch erklären, was ich machen muss, damit die 2 DataGrids kontinuierlich und gleichzeitig aktualisiert werden und kurz die Theorie dazu erläutern?
Vielleicht verstehe ich es dann.

Hoffnungsvoll grüßt
Matze

F
10.010 Beiträge seit 2004
vor 9 Jahren

Was ist da ausser
[Artikel] Multi-Threaded Programmierung und
[FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke)
zu erklären?

Wenn ich deinen Code anschaue wird aber klar, das du den 2. Link nicht so wirklich durchgearbeitet hast.

_globalData.DataBufferGUI_Lock.AddElement(_dtLogNOK_Lock, GlobalTypesDatabase.GUIDataType.LogDataNOK,
             _guiBufferQueueLogNOK.Dequeue());

Und das Zeigt das du alles viel zu complex und wahrscheinlich vollkommen entgegen jeder empfohlenen Architektur machst.

  1. Benutze ein UI Element das einen Virtuellen modus unterstützt.
  2. Jedes BeginInvoke sendet eine Nachricht in die Windows/Dispatcher MessageQueue und wird dann nach und nach abgearbietet.
  3. Daraus folgt, das du alle Adds in einem Invoke machen solltest.
  4. Da die DGV gebunden sind, machen sie bei jeder Änderung ein komplettes Update.
C
C#Matze Themenstarter:in
31 Beiträge seit 2014
vor 9 Jahren

Hallo,

dankeschön für die Erklärung. Ich arbeite den 2. Link durch und schaue mal, wie weit ich komme.

Grüße
Matze

C
C#Matze Themenstarter:in
31 Beiträge seit 2014
vor 9 Jahren

Es tut mir leid, wenn ich nerve. 🙁

Ich habe den Link durchgearbeitet, aber sehe nicht, was ich falsch mache.

  1. Daraus folgt, das du alle Adds in einem Invoke machen solltest.

Das habe ich ja versucht, aber dann ruckelt die Anzeige enorm.

Ich habe es testweise ohne "RemoveAt()" probiert:

Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
{
	lock (_dtLogNOK_Lock)
	{
		int bufferSize = _globalData.Config._configData.ConfigGUI.mainTables.rowCountLogNOK;
		for (int i = 0; i < _guiBufferQueueLogNOK.Count; i++)
		{
			_dtLogNOK_Lock.Rows.InsertAt(_guiBufferQueueLogNOK.Dequeue(), 0);

			for (int iRowIdx = _dtLogNOK_Lock.Rows.Count - 1; iRowIdx > bufferSize - 1; iRowIdx--)
			{
				// remove last element
				//_dtLogNOK_Lock.Rows.RemoveAt(iRowIdx);
			}
		}
	}

	lock (_dtLogMain_Lock)
	{
		int bufferSize = _globalData.Config._configData.ConfigGUI.mainTables.rowCountLogAll;
		for (int i = 0; i < _guiBufferQueueLogMain.Count; i++)
		{
			_dtLogMain_Lock.Rows.InsertAt(_guiBufferQueueLogMain.Dequeue(), 0);

			for (int iRowIdx = _dtLogMain_Lock.Rows.Count - 1; iRowIdx > bufferSize - 1; iRowIdx--)
			{
				// remove last element
				//_dtLogMain_Lock.Rows.RemoveAt(iRowIdx);
			}
		}
	}
}));

Und so würde alles gehen. Wenn ich "RemoveAt()" wieder einkommentiere, ruckelt alles.

Zum Hintergrund, vielleicht ist mein Grundaufbau auch komplett falsch. Nur die ganzen Bücher und Tutorials beschränken sich immer nur auf einfache Strukturen und nicht auf komplexere Themen:

Über 2 Plugins (Dlls) werden Daten von 2 Systemen per TCP/IP empfangen. Beide Dll-Instanzen laufen in je einem Thread und empfangen kontinuierlich Daten. Werden Daten empfangen, bereitet die Dll die Daten auf und sendet Sie über je eine Queue an die Hauptanwendung.
Da ein Handshake zur Dll benötigt wird, antwortet die Hauptanwendung über eine weitere Queue (feste Größe von 1) der Dll, dass die Erfassung fortgesetzt werden darf.
In der Hauptanwendung polle ich (mit Events war die Performance schlecht) die Queue in einem weiteren Thread. Sind Daten vorhanden, werden diese gespeichert und gesammelt mit 2 weiteren Queues (die im Quellcode oben) für die Visualisierung bereitgestellt.

Vermutlich schüttelt ihr nun den Kopf (sicherlich zu Recht).
Vielleicht wird vieles einfacher, wenn ich wüsste, wie man so eine Anwendung umsetzt. Gibt's hierfür Tipps?
Das Erzeuger-verbraucher-System wäre vielleicht sinnvoll und das arbeitet ja eigentlich mit Queues.

Grüße
Matze

T
461 Beiträge seit 2013
vor 9 Jahren

Ist jetzt kurz geschaut nur eine Vermutung aber kann der Grund das DispatcherPriority.Background sein ?

Ich habe den Titel mal angepasst, so dass Suchende auch etwas damit anfangen können. EDIT: Ich sollte beim Wort "Shift" im Titel das "f" nicht vergessen... 😄

C
C#Matze Themenstarter:in
31 Beiträge seit 2014
vor 9 Jahren

Hallo

Ist jetzt kurz geschaut nur eine Vermutung aber kann der Grund das DispatcherPriority.Background sein ?

Nein, mit den Prioritäten habe ich bereits herumgetestet. Auch ".Normal" u.ä. ändern leider nichts an dem Verhalten.

211 Beiträge seit 2008
vor 9 Jahren

Von welchen Datenmengen sprichst du denn ? 100 ? 1000 ? 100000 ?
Wie sieht dein XAML dazu aus?
WPF geht auch irgendwann in die Knie bei genügend Daten, es hilft dabei auch auf eine eigenes ObservableCollection zu gehen, die nicht bei jedem neuen Item sofort ein CollectionChanged ausführt.

Kontakt & Blog: www.giesswein-apps.at

C
C#Matze Themenstarter:in
31 Beiträge seit 2014
vor 9 Jahren

Hallo,

es sind lediglich 100 Einträge mit momentan ca. 20 Spalten.

Das XAML dazu sieht wie folgt aus:

<DataGrid Grid.Row="6" Grid.Column="1" AutoGenerateColumns="True" AutoGeneratingColumn="OnAutoGeneratingColumn"
	HorizontalAlignment="Stretch"
	Name="dataGridMain" VerticalAlignment="Stretch" EnableRowVirtualization="True"
	EnableColumnVirtualization="True" IsReadOnly="True"
    AlternatingRowBackground="#DDDDDD" AlternationCount="2" BorderThickness="0"
    FrozenColumnCount="3" VerticalGridLinesBrush="{StaticResource GrayGridLine}" HorizontalGridLinesBrush="{StaticResource GrayGridLine}"
	ItemsSource="{Binding _dvLogMain, Mode=OneWay, UpdateSourceTrigger=Explicit, IsAsync=True}"
	RowHeaderWidth="0">
</DataGrid>

Auch wenn ItemsSource auf "{Binding _dvLogMain}" gesetzt ist, ändert sich nichts.
Ich sehe gerade, wenn ich "OnAutoGeneratingColumn" nicht verwende, ist die Aktualisierung minimal schneller.
Aber wie gesagt, "InsertAt()" geht flott, "RevoveAt()" braucht sehr lange.

Nutze ich "RemoveAt(1)", entferne ich also immer das 2. Elemente von oben, geht's auch schnell.
Also ob beim Entfernen der untersten Zeile das komplette DataGrid neu gezeichnet wird. 🤔

Die gebundenen Daten sind wie folgt definiert:

public DataView _dvLogMain
{
	get {
		lock (_dtLogMain_Lock)
			return _dtLogMain_Lock.DefaultView;
	}
}

Grüße

F
10.010 Beiträge seit 2004
vor 9 Jahren
  1. Jede Art von Polling ist im allgemeinen sehr langsam. Benutze z.b. die SyncQueue, dann musst du nicht Pollen.

_globalData.Config._configData.ConfigGUI.mainTables.rowCountLogNOK

Soetwas zeigt das du keine echte OOP machst, sondern lediglich alles irhgendwie in Klassen packst.
Wenn du mehr als einen Punkt in einer Variablen hast, läuft da schon etwas falsch.

  1. Du hast Punkt 4 nicht gelesen, oder nicht genau verstanden was ich meinte.
    In WPF, what is the equivelent of Suspend/ResumeLayout() and BackgroundWorker() from Windows Forms
C
C#Matze Themenstarter:in
31 Beiträge seit 2014
vor 9 Jahren

Hallo

  1. Jede Art von Polling ist im allgemeinen sehr langsam. Benutze z.b. die
    >
    , dann musst du nicht Pollen.

Danke, die SyncQueue schaue ich mir an.

_globalData.Config._configData.ConfigGUI.mainTables.rowCountLogNOK  

Soetwas zeigt das du keine echte OOP machst, sondern lediglich alles irhgendwie in Klassen packst.
Wenn du mehr als einen Punkt in einer Variablen hast, läuft da schon etwas falsch.

Oha ok. Ich wollte eine logische Unterteilung schaffen und verwende daher Unterklassen (wie früher die Strukturen, also ohne Vererbung etc.). Ich werde mich informieren, um zu verstehen, was du meinst. Natürlich könnte ich alle Variablen in meine "Hauptklasse" packen, aber das wird dann ja sehr unübersichtlich.

Du hast Punkt 4 nicht gelesen, oder nicht genau verstanden was ich meinte.

>

Ich habe bereits "using (var d = Dispatcher.DisableProcessing())" versucht und den BackgroundWorker auch bereits getestet. Optisch gesehen funktioniert alles gleich: Sobald "RemoveAt()" aufgerufen wird, ruckelt die Darstellung und die Oberfläche reagiert sehr träge. Ohne "RemoveAt()" ist alles flüssig.
Ich kann mir das nur so erklären, dass beim Entfernen der untersten Zeile das komplette DataGrid aktualisiert wird, was langsam ist.

Ziel ist ein Ringpuffer: Neue Einträge werden oben hinzugefügt und die alten unten entfernt.

Eure Foren-Tutorials sind echt klasse. Nur fällt es mir leider sehr schwer, die einzelnen Themen auf komplexere Themen anzuwenden.

Grüße

211 Beiträge seit 2008
vor 9 Jahren

Ich hab das gerade mal eben "schnell" ausprobiert, und stimmt - bei der Verwendung von RemoveAt knickt die Performace ziemlich ein. Hab hier leider Reflector nicht installiert um reinzuschauen, aber mit ner Quick&Dirty ObservableCollection konnte ich die Performance ohne Probleme halten.

Wobei ich hier das ColectionChanged nur nach der vollständigen Änderung aufrufe, und auch noch genau angebe welche Elemente weg sind.

Kontakt & Blog: www.giesswein-apps.at

C
C#Matze Themenstarter:in
31 Beiträge seit 2014
vor 9 Jahren

Hallo

... aber mit ner Quick&Dirty ObservableCollection konnte ich die Performance ohne Probleme halten.

Wobei ich hier das ColectionChanged nur nach der vollständigen Änderung aufrufe, und auch noch genau angebe welche Elemente weg sind.

Das ist sehr nett, vielen Dank.
Die Spalten stehen bei mir jedoch zur Designtime noch nicht fest. Die sind bei mir frei konfigurierbar und müssen das auch bleiben.
Wenn ich es richtig verstanden habe, kann ich eine ObservableCollection nur mit zur Designtime bekannten Properties anlegen oder geht das auch dynamisch?
Wenn nicht, muss ich vermutlich bei der DataTable bleiben ...

Grüße

211 Beiträge seit 2008
vor 9 Jahren

Eine ObservableCollection nimmt jeden Datentyp, da kannst du sogar ein dynamic verwenden (ObservableCollection<T>).

Im Falle, dass es dir gänzlich unbekannt ist, welches Object daherkommt, verwende eine ObservableCollection<object> und das DataGrid verwendet sowieso Reflection um darauf zuzugreifen und die Properties aufzulösen - das kannst du über AutoGenerateColumns steuern.

Kontakt & Blog: www.giesswein-apps.at

C
C#Matze Themenstarter:in
31 Beiträge seit 2014
vor 9 Jahren

OK danke. Ich schaue mal, ob ich's hinbekomme.

Was ich nun feststellen musste ist, dass die Aktualisierung des DataGrids im Debugger (vom Visual Studio aus gestartet) viel langsamer ist, als wenn ich die Debug-Exe oder die Release-Exe starte. Dass ein Overhead da ist, ist klar, aber dass sich das so extrem auswirkt, wusste ich nicht.

Grüße

C
C#Matze Themenstarter:in
31 Beiträge seit 2014
vor 9 Jahren

Hallo zusammen,

ich habe es mit einer ObservableCollection hinbekommen nach dieser Anleitung.
Prinzipiell funktioniert's auch, nur wenn ich dort per "RemoveAt()" den untersten Eintrag entferne, leert sich kurz das komplette DataGrid. D.h. auch hier wird beim Entfernen das komplette DataGrid aktualisiert.

Wie kann man das denn verhindern?
Das Einfügen oben klappt. Dann muss das Entfernen am Ende doch auch gehen ohne alles neu zu zeichnen.

211 Beiträge seit 2008
vor 9 Jahren

Also ohne Code können wir hier nun leider nur raten 😃 ?

Kontakt & Blog: www.giesswein-apps.at