Laden...

Progressbar aus Background-Thread aktualisieren?

Erstellt von rogge vor 3 Jahren Letzter Beitrag vor 3 Jahren 1.202 Views
R
rogge Themenstarter:in
2 Beiträge seit 2020
vor 3 Jahren
Progressbar aus Background-Thread aktualisieren?

Hallo zusammen,

ich denke, dass das Thema bereits etliche Male erläutert wurde und trotzdem muss ich noch einmal nerven...

Ich versuche mich gerade zu Übungszwecken an einem Kontoauzugsimporter und möchte gerne eine Progressbar auf meinem Formular aktualisieren.

Mein Code sieht bisher jetzt folgendermaßen aus:


DataTable dtPositions;
int counter = 0;

//1. Daten in Tabelle einlesen
OpenFileDialog openFileDialog = new OpenFileDialog
{
	Multiselect = true,
	Filter = "All files (*.*)|*.*|XML files (*.xml)|*.xml|CSV files (*.csv)|*.csv|ZIP|*.zip"
};

if (openFileDialog.ShowDialog() == DialogResult.OK)
{
	int counter = 0;
	this.progressBar.Maximum = openFileDialog.FileNames.Length;
	
	Statement statements = new Statement();
	foreach (String file in openFileDialog.FileNames)
	{
		counter++;
		progressbar.Value = counter; //<= Hier soll die Progressbar bis zur Anzahl der ausgewählten Dateien laufen
		statements.Read(file);
	}
	statements.Sort();
	dtPositions = statements.dtPositions;
}

//2. Daten in Datenbank einlesen
int counter = 0;
this.progressBar.Maximum = ((dtPositions == null) ? 0 : dtPositions.Rows.Count);

Import import = new Import();
foreach (DataRow row in dtPositions.Rows)
{
	counter++;
	progressbar.Value = counter++; //<= Hier soll die Progressbar bis zur Anzahl der DataTable-Einträge laufen
	import.AddPosition(row);
}

Sämtliche "klägliche" Versuche meinerseits Backgroundworker, InvokeRequired/Invoke oder Task zu verwenden liefen leider schief.

Vll. hat jemand erbarmen und würde mich erleuchten 😃

Viele Grüße
Timm

16.827 Beiträge seit 2008
vor 3 Jahren

Jede Aktion, die im UI-Thread ausgeführt wird und die länger als ca. 100ms braucht, muss sollte in einen Task ausgelagert werden, ansonsten reagiert die UI nicht mehr.
[FAQ] Warum blockiert mein GUI?

Das, was Du hier als Verarbeitung hast, ist typisch dafür, dass dies ausgelagert werden soll und vermutlich auch muss.
Sprich, Du musst die Aktionen entsprechend in einen Task überführen und der Task muss seinen Fortschritt melden (zB über Events).
[FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke)

So wie Du das nun hast wird vermutlich die UI sich nicht aktualisieren und entsprechend der Fortschritt erst am Ende der Methode sichtbar sein.
Wenn Du mit Tasks - was dem BackgroundWorker etc vorzuziehen ist - arbeitest, dann sorgt die Synchronisation dafür, dass Du nicht Invoken musst.

Was ist denn Dein konkretes Problem?

Im Prinzip brauchst Du schematisch ja nur folgende Methode:

public async Task RunImport(... files)
{
    // Progress = 0 

    foreach(var file in files)
    {
       // Progress = file / files

    } 

    // Progress = 100
}

Ich mag Dir jetzt nicht den Code schreiben, weil Du ja was dabei lernen sollst - und nicht Copy Paste.

R
rogge Themenstarter:in
2 Beiträge seit 2020
vor 3 Jahren

Hi,

vielen Dank für die Antwort.

Ich habe jetzt die einzelnen Importphasen in eigene Funktionen gegliedert, in denen auch die Schleifen zum hochzählen der ProgressBar enthalten sind. In jeder Phase soll der Wert der ProgressBar von 0 beginnen (und eigentlich auch der LabelText angepasst werden, aber eins nach dem anderen).

Ich habe das Grundprinzip der asynchronen Programmierung nicht ganz durchdrungen und schaffe es leider nicht die etlichen Beispiele aus dem Netz auf mein Problem zu adaptieren.

Welche meiner Funktionen muss async sein und wo wird das await angewendet?
Jede Phase soll ja auf die vorherige warten?

Entschuldigt bitte die blöden Fragen 😃

Viele Grüße
Timm


//phase 1: read bank statement positions
private Statement ReadStatements(string[] files)
{
	try
	{
		Statement statements = new Statement();
		this.progressBar.Minimum = 0;
		this.progressBar.Maximum = files.Length;
		foreach (String file in files)
		{
			this.progressBar.Value++;
			statements.Read(file);
		}
		statements.Sort();
		return statements;
	}
	catch (Exception ex)
	{
		Helper.Errorhandler(ex.Message);
		return null;
	}
}

//phase phase 2: import to database
private void ImportStatements(Statement statements)
{
	try
	{
		Import import = new Import();
		this.progressBar.Minimum = 0;
		this.progressBar.Maximum = ((statements.dtHeader == null) ? 0 : statements.dtHeader.Rows.Count) +
								   ((statements.dtPositions == null) ? 0 : statements.dtPositions.Rows.Count) +
								   ((statements.dtBalances == null) ? 0 : statements.dtBalances.Rows.Count);
		//Header
		foreach (DataRow row in statements.dtHeader.Rows)
		{
			this.progressBar.Value++;
			import.AddHeader(row);
		}

		//position
		foreach (DataRow row in statements.dtPositions.Rows)
		{
			this.progressBar.Value++;
			import.AddPosition(row);
		}

		//balance
		foreach (DataRow row in statements.dtBalances.Rows)
		{
			this.progressBar.Value++;
			import.AddBalance(row);
		}
	}
	catch (Exception ex)
	{
		Helper.Errorhandler(ex.Message);
	}
}

//phase 3: allocate open postions
private Allocation AllocateStatements()
{
	try
	{
		Allocation allocation = new Allocation();
		this.progressBar.Minimum = 0;
		this.progressBar.Maximum = (allocation.dtOpenPositions == null) ? 0 : allocation.dtOpenPositions.Rows.Count;
		foreach (DataRow row in allocation.dtOpenPositions.Rows)
		{
			this.progressBar.Value++;
			allocation.Allocate(new IMPORT_POS(Convert.ToInt32(row["id"].ToString())));
		}
		return allocation;
	}
	catch (Exception ex)
	{
		Helper.Errorhandler(ex.Message);
		return null;
	}
}

//call functions
private void ImportBankStatement()
{
	OpenFileDialog openFileDialog = new OpenFileDialog
	{
		Multiselect = true,
		Filter = "All files (*.*)|*.*|XML files (*.xml)|*.xml|CSV files (*.csv)|*.csv|ZIP|*.zip"
	};

	if (openFileDialog.ShowDialog() == DialogResult.OK)
	{
		this.progressBar.Visible = true;
		Statement statements = null;
		Allocation allocation = null;

		//--------------------------------------------
		//phase 1: read bank statement positions
		this.labelPhase.Text = "phase 1: read bank statement positions";
		statements = ReadStatements(openFileDialog.FileNames);

		//--------------------------------------------
		//phase 2: import to database
		this.labelPhase.Text = "phase 2: import to database";
		ImportStatements(statements);
		
		//--------------------------------------------
		//phase 3: allocate open postions
		this.labelPhase.Text = "phase 3: allocate open postions";
		allocation = AllocateStatements();

		//--------------------------------------------
		if (allocation.PositionsOpen > 0)
		{
			MessageBox.Show(
				"Offene Positionen: " + allocation.PositionsOpen + Environment.NewLine +
				"  - zugeordnet: " + allocation.PositionsAllocated + Environment.NewLine +
				"  - nicht zugeordnet: " + allocation.PositionsNoneAllocated + Environment.NewLine +
				"  - neu angelegt: " + allocation.PositionsNew
			  , "Import abgeschlossen", MessageBoxButtons.OK, MessageBoxIcon.Information);
		}
		else
		{
			MessageBox.Show("Keine offenen Posten vorhanden", "Import abgeschlossen", MessageBoxButtons.OK, MessageBoxIcon.Information);

		}
		this.labelPhase.Text = "";
	}
}

G
16 Beiträge seit 2019
vor 3 Jahren

Ich würde dir dafür folgendes Video empfehlen. async/await by Tim Corey
Gibt dann auch noch ein advanced Video aber ich denke dass brauchst du in diesem Fall gar nicht.

T
2.222 Beiträge seit 2008
vor 3 Jahren

Anstelle von Methoden würde ich den ganzen Verarbeitungspart in eine eigene Klasse auslagern.
Im Bestfall schaust du dir auch das Drei-Schichten-Modell an um deine UI, Logik und Datenhaltung sauber zu trennen und nicht alles in die UI schiebst.

Dadurch wird dein Code nicht nur übersichtlicher sondern ein leichter zu warten und zu erweitern.

Link:
[Artikel] Drei-Schichten-Architektur

Nachtrag:
Ich hatte sowas in den letzten Wochen/Monaten auch umgesetzt.
Die Verarbeitung startest du dann z.B. über einen Task in einer asychronen Methode.
Diese instanziert dann deine Klasse und läuft durch den Task in einem eigenen Thread.
Dadurch blockiert auch deine UI nicht mehr.

Du solltest hier, wenn du eine Klasse hast, dieser einfach ein Event bereitstellen um die Progressbar zu befüllen.
In den EventArgs kannst du dann die Summe und die aktuelle Anzahl der verarbeiteten Einträge liefern.
In deiner UI kannst du dann die Progressbar updaten, hier musst dann z.B. per Invoke auf das Control zugreifen da du eben durch den Task in einem anderen Thread in das Event springst als mit deinem UI Thread.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

16.827 Beiträge seit 2008
vor 3 Jahren

Also zunächst der Hinweis, dass Du Dich in der Objekt-orientierten Programmierung befindest und daher Methoden hast und keine Funktionen.
Funktionen sind in diesem Sinne etwas anderes.

Der Hinweis von T-Virus ist zielführend, weil Du Dich durch den Mix an Code selbst behinderst und die Übersicht verlierst.

Prinzipiell ist das Stichwort hier Single-Responsibility-Prinzip - also dass Du alles aus Deiner UI raus hast, das nicht primär etwas mit der UI zutun hat.

Letzten Endes wird das also auf etwas wie folgendes hinauslaufen

    public class MyStatementReader
    {

        public async Task<Statement> Read(string file)
        {
            ....
        }
    }

    public class MyBankImporter
    {
        public MyBankImporter(IMyDatabaseConnectio dbConnection)
        {
            
        }

        public async Task Import(Statement statement)
        {
            ....
        }
    }

In der Form hättest Du dann sowas wie

        public async void OnClick_StatementFileSelectButton(...)
        {
            var files = filesFromFileDialog..;

            MyStatementReader statementReader = ...;
            List<Statement> statements = new List<Statement>();

            this.progressBar.Value = 0;
            for (int fileIndex = 0; fileIndex < files.Count; fileIndex++)
            {
                var file = files.ElementAt(fileIndex)

                var statement = await statementReader.Read(file);
                statements.Add(statement);

                this.progressBar.Value = fileIndex / files.Count;
            }
            this.progressBar.Value = 1000;

            // hier hast Du nun alle Statements gelesen
        }

... und das musst dann eben auch für den Import machen.

Wenn Du Deine Schnittstelle Datei-für-Datei aufrufst, dann kannst das ja sehr einfach direkt in der UI melden, wo Du aktuell bist.
Willst Du mehrere Dateien gleichzeitig an die Schnittstelle übergeben und in einem Rutsch bearbeiten, dann muss die Schnittstelle Events zur Verfügung stellen, damit sie Dir melden kann, wo sie aktuell ist.

Letzten Endes also nur die Kombination von ein paar Grundprinzipien, wie

  • Trennung von Code
  • Events
  • UI Benachrichtigungen
  • Asynchrone Programmierung
5.299 Beiträge seit 2008
vor 3 Jahren

Ich habe jetzt die einzelnen Importphasen in eigene Funktionen gegliedert... Dassis schoma sehr gut, und der erste Schritt.
... Welche meiner Funktionen muss async sein und wo wird das await angewendet? Wurde schon gesagt: die Zeitfresser müssen Async.
Ich hab mal auf CodeProject ein Tut gebastelt, was zeigt, wie man mit minimalem Eingriff eine Methode in einen NebenThread schubst.
Die gezeigten Code-Snippets sind zwar vb, es hat aber auch eine c#-SampleSolution.
Wie gesagt: Es trifft sich sehr gut, dass du die Kandidaten bereits in eigene Methoden isoliert hast.
Async/Await
Was anfangs als zum schreien einfach anmutet erweist sich dann doch als typischer Rattenschwanz:*Ist eine Methode Async auf den Weg gebracht, musst du so lange verhindern, dass der Button nochmal geklickst wird (Suspend Gui) *Während das Teil läuft willst du eine Progressbar oder sowas (Update Gui) *Natürlich will der User den Vorgang auch canceln können (Cancellation) *Fehler müssen (fast immer) im Gui-Thread behandelt werden (Error-Handling)

Jo - wird alles behandelt in meim Tut.
(Wenn du es brauchbar findest, rate es up - ich wunder mich immer, was für Artikel auf CP irrwitzig hochgeratet sind, während so ein Fundamental-Artikel zu einem ich finde bahnbrechend neuem Feature bei weniger als 20 ratings rumdümpelt.)

Der frühe Apfel fängt den Wurm.