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
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.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
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 = "";
}
}
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.
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.
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
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
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.