Laden...

Wie ein WinForms Ladefenster anzeigen während anderer Thread arbeitet & wieder "zurückspringen"?

Erstellt von timbu42 vor 3 Jahren Letzter Beitrag vor 3 Jahren 1.874 Views
T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 3 Jahren
Wie ein WinForms Ladefenster anzeigen während anderer Thread arbeitet & wieder "zurückspringen"?

Hallo Zusammen,

ich möchte in einer WinForms Anwendung ein kleines Fenster anzeigen, während Daten geladen werden (~10 Sek). Diese WaitingForm enthält einen Hinweis und einen Ladebalken (ProgressBar) und soll danach von selbst verschwinden. Nach dem Laden werden die Daten in der MainForm angezeigt (Chart für Daten, ComboBox zu weiteren Ansicht, etc.).

Die GUI darf währenddessen nicht blockiert werden, damit die ProgressBar sich stets aktualisiert (und damit das Fenster nicht einfriert, das wirkt immer so unschön).

So habe ich es bisher (nach einigem Googlen) gelöst; es funktioniert auch, aber ich würde gern wissen, ob es eine bessere Möglichkeit gibt:

------- WaitingForm.cs (Ausschnitt) -------


public partial class WaitingForm : Form
{

    public static void ShowModal(Action<Action<int>> workload, Action wennFertig)
    {
        WaitingForm form = new WaitingForm();
        form.Show();

        Action action = new Action(() =>
        {
            workload(form.SetProgress);
            form.Invoke(new MethodInvoker(() => wennFertig()));                
            form.Invoke(new MethodInvoker(() => form.Close()));
        });
        Task task = new Task(action);
        task.Start();

        // Marker XYZ
    }

    public void SetProgress(int progressInPercent)
    {
        progressBar1.Invoke(new MethodInvoker(() => progressBar1.Value = progressInPercent));
    }
}

------- MainForm.cs (Ausschnitt) -------


public partial class MainForm : Form
{

    public void DoSomething(Action<int> updateStatus)
    {
        updateStatus(25);
        updateStatus(50);   // Fake
        updateStatus(75);
        updateStatus(100);
    }

    private void buttonOK_Click(object sender, EventArgs e)
    {
        Action<Action<int>> action = (upd => DoSomething(upd));

        Action wennFertig = () =>
        {
            // Charts updaten etc...
        };

        WaitingForm.ShowModal(action, wennFertig);

        // Marker XYZ
    }
}

------- ENDE Quelltexte -------

Es kommt mir nicht optimal vor, weil ich die Anweisungen, die nach dem Laden passieren sollen (Action wennFertig) via form.Invoke auf den Hauptthread schiebe. Gibt es eine bessere Möglichkeit, den "Arbeitsfluss" nach getaner Arbeit wieder an den Hauptthread zu geben? Theoretisch wäre das ja an beiden mit Kommentar "Marker XYZ" markierten Stellen im Quelltext oben, aber da darf ja nichts mehr passieren, damit die GUI nicht blockiert ist.

Vermutlich geht das irgendwie mit await / async, aber da kenn ich mich zu wenig aus.

Würde mich freuen, wenn jemand einen Tipp hat.

Viele Grüße, Tim

T
2.219 Beiträge seit 2008
vor 3 Jahren

Du solltest anstelle von C&P Code mal etwas in die Doku schauen.
Für asynchrone Programmierung gibt es mit async/await entsprechende Ansätze.
Damit du aber den Progress in der ProgressBar aktualisieren kannst, musst du das entsprechende Control per Invoke aufrufen.
Den dein asynchroner Task läuft dann in einem eigenen Thread aber nur der UI Thread kann die Controls ansprechen.

async/await

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.

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 3 Jahren

Hallo T-Virus,

Danke für die Antwort, auch wenn sie mich etwas verwundert, da

  1. mein Code zu exakt 0% C&P ist,
  2. ich ja selbst geschrieben hatte, dass ich die progressBar mit Invoke aktualisiere, und
  3. der Link mir nicht hilft, da ich kein allgemeines Problem mit async/await habe, sondern nicht weiß, wie ich das Konzept in diesem konkreten Fall am besten auf die GUI anwende. Da gibt es ja mehrere Möglichkeiten.

Vielleicht habe ich mich da schlecht ausgedrückt.

Ich habe es jetzt so gelöst:


public partial class WaitingForm : Form
{
    public static async Task ShowModal(Action<Action<int>> workload)
    {
        WaitingForm form = new WaitingForm();
        form.Show();

        await Task.Run(() =>
        {
            workload(form.SetProgress);
        });
        
        form.Close();
    }

    public void SetProgress(int progressInPercent)
    {
        progressBar.Invoke(new MethodInvoker(() => progressBar.Value = progressInPercent));
    }

}

public partial class MainForm : Form
{
    private async void buttonOK_Click(object sender, EventArgs e)
    {
        await WaitingForm.ShowModal(updProgress => Rechenarbeit(updProgress));

         // Hier das Ergebnis von Rechenarbeit in der GUI anzeigen
    }
    
    public void Rechenarbeit(Action<int> updProgress)
    {
        Thread.Sleep(1000);
        updProgress(25);
        Thread.Sleep(1000);
        updProgress(50);
        Thread.Sleep(1000);
        updProgress(75);
        Thread.Sleep(1000);
        updProgress(100);
    }

}

Das funktioniert soweit. Sieht jemand da noch etwas Problematisches / nicht Optimales?

Viele Grüße,
Tim

16.806 Beiträge seit 2008
vor 3 Jahren

Rechenarbeit sollte den Fortschritt über einen Event zur Verfügung stellen und nicht über eine Action.
Die Action dreht nämlich in der Form die Abhängigkeit von UI->Logik auf Logik->UI.

Das widerspricht der Modularisierung und dem Prinzip von [Artikel] Drei-Schichten-Architektur

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 3 Jahren

Hallo Abt,

ist das wirklich so? Ist es für die Richtung der Abhängigkeit nicht egal, ob Rechenarbeit über ein Event über Progress informiert oder über eine Methode? Umgekehrte Abhängigkeit würde ich nur dann sehen, wenn Rechenarbeit() die Methode SetProgress() von der WaitingForm direkt aufruft... das wäre natürlich ein No-Go.

Nichtsdestotrotz kann ich mir natürlich trotzdem vorstellen, dass ein Event eleganter wäre, aber dann müsste ich eine Klasse draus machen, oder? Ich möchte schon dabei bleiben, dass die WaitingForm einen Job in Form einer Methode ausführt.

Hallo david.m,

ich schau mir das mal an, kenne ich bisher nicht, Danke für den Tipp!

5.657 Beiträge seit 2006
vor 3 Jahren

ist das wirklich so?

Ja. Die Erklärung dafür gibt es ja auch im verlinkten Artikel.

Weeks of programming can save you hours of planning

4.931 Beiträge seit 2008
vor 3 Jahren

Das sehe ich aber auch so wie timbu42.
Ob nun direkt ein Delegate (z.B. Action<...> oder Func<...>) benutzt wird oder aber ein Event (das nichts anderes als ein verpackter Delegat ist) ändert nichts an der Unabhängigkeit der verwendeten Klassen (bzw. Methoden).
Das einzige ist, daß man aufpassen muß, wenn man das Delegat aufruft (ob auch wirklich ein Delegat-Objekt übergeben wurde und nicht null), aber das ist bei einem Event ja auch so (wenn dieses niemals abonniert wurde):


updProgress?.Invoke(...);

16.806 Beiträge seit 2008
vor 3 Jahren

Der große und der wichtige unterschied ist, dass bei einem Delegate die Verarbeitungsverantwortung in der Logik liegt, was absolut nicht sein darf.

D.h. die Logik ist plötzlich in der Verantwortung und "benötigt das Wissen", wie später das Delegate implementiert wird (zB. Thread-Safety).
Das widerspricht ganz klar der Verantwortungstrennung.

Auch andere Dinge wie das Exception Handling müssen verlagert werden.

4.931 Beiträge seit 2008
vor 3 Jahren

Der Aufruf-Kontext bei einem Delegate und einem Event ist vollkommen identisch.
Der einzige Unterschied bei einem Event zu einem Delegate besteht dadrin, daß man zusätzlich die Methoden Add und Remove überschreiben kann und ein Event (im Gegensatz zu einem Delegat als Klassenmember) nur von der Klasse selbst aus aufgerufen werden kann.

Bei Standard-Methoden wie List<T>.Sort(Comparison<T>) oder den Linq-Methoden wie z.B. OrderBy<TSource,TKey>(IEnumerable<TSource>, Func<TSource,TKey>) wird ja auch ein Delegate als Parameter übergeben (und keiner erwartet da ein Event).

16.806 Beiträge seit 2008
vor 3 Jahren

Es geht nicht um das Delegate per sé, sondern um den entsprechenden Einsatz.
Ein Delegate bei einem Sort hat mehr Sinn (und erfüllt entsprechend seine Aufgabe) als ein entsprechenden UI-Aufruf für die Aktualisierung eines Fortschritts.
Bei einer Schichtüberwindung sind die Anforderung an eine Ereignisverarbeitung eine ganz andere als an eine Funktion der Ausführung.

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 3 Jahren

Danke für die vielen Antworten! Jetzt wird's ja richtig interessant 😃

Der große und der wichtige unterschied ist, dass bei einem Delegate die Verarbeitungsverantwortung in der Logik liegt, was absolut nicht sein darf.

D.h. die Logik ist plötzlich in der Verantwortung und "benötigt das Wissen", wie später das Delegate implementiert wird (zB. Thread-Safety).
Das widerspricht ganz klar der Verantwortungstrennung.

Auch andere Dinge wie das Exception Handling müssen verlagert werden.

Ich verstehe auch trotz der Erklärungen nicht den Unterschied.
Meine Implementierung sagt: "hier ist eine Methode, die etwas berechnet und immer zwischendurch wenn es einen Fortschritt gibt, eine bestimmte Methode unbekannter Implementation aufruft".
Mit einem Objekt mit Event wäre es: "hier ist ein Objekt mit einer Methode, die etwas berechnet und immer zwischendurch wenn es einen Fortschritt gibt, ein Event auslöst (was ebenfalls Methoden unbekannter Implementation aufruft)".

Welchen Einfluss genau hat dieser kleine Unterschied also beispielsweise auf die Thread-Safety?
Ich könnte verstehen, dass es ein Unterschied wäre, wenn die via Event ausgelösten Methoden von demjenigen Thread ausgeführt werden, in dem das Event mit += gefüttert wurde, allerdings habe ich es gerade ausprobiert und dem ist nicht so (das hätte auch viele weitere Fragen aufgeworfen m.E.)

ist das wirklich so?

Ja. Die Erklärung dafür gibt es ja auch im verlinkten Artikel.

Das sehe ich nicht so, denn die "Methoden-Lösung" hat auch vollständig getrennte Schichten. Ich kann die Logik-DLL unabhängig von der GUI kompilieren.

Der von david.m verlinkte Artikel

https://devblogs.microsoft.com/dotnet/async-in-4-5-enabling-progress-and-cancellation-in-async-apis/

macht es im Übrigen auch ohne Event.

16.806 Beiträge seit 2008
vor 3 Jahren

Das sehe ich nicht so, denn die "Methoden-Lösung" hat auch vollständig getrennte Schichten. Ich kann die Logik-DLL unabhängig von der GUI kompilieren.

Mit Kompilieren hat das nichts zutun. Es geht auch nicht um das Delegate an sich. Es geht um die Verantwortung der Ausführung.
Und mit der Methoden-Lösung verlagerst Du die Verantwortung von der UI in die Logik und schaffst damit eine Abhängigkeit.

Welchen Einfluss genau hat dieser kleine Unterschied also beispielsweise auf die Thread-Safety?

Es geht darum, dass aus Konzeptionssicht die Logik eine Schnittstelle zur Verfügung stellt, um über Ereignisse zu informieren.

Ein **Event **wäre:

Hey, hier habe ICH eine Aktualisierung - mach was immer DU damit willst!

> Die UI ist damit in der Verantwortung der Ausführung

Die **Action **wäre:

Hey, gib mir doch eine Methode, die ICH für DICH ausführe.

> Die Logik ist in der Verantwortung der Ausführung

Dass beides funktioniert: keine Frage.
Aber es sind einfach unterschiedliche Konzepte.

Der von david.m verlinkte Artikel macht es im Übrigen auch ohne Event.

Du darfst Artikel oder Code Beispiele in Dokumentationen nicht missverstehen.
In solchen Artikeln und Code-Beispielen geht es um die Funktionsweise der gezeigten Elemente - nicht um die Korrektheit der Integration.
Du findest in hunderten Beispielen in den Microsoft Docs "Architekturfehler" - einfach, weil es in den Beispielen auch nicht um die Architektur geht.

Einer der prominentesten Fehler ist die HttpClient Klasse: 99% der .NET Entwickler verwenden die Klasse falsch, weil sie ungünstig in der Dokumentation beschrieben war.
Mittlerweile ist das korrigiert; in altem Code lebt falsche Verwendung weiter.

Und nur weil .NET eine gewisse Klasse hat, die gewisse aufgaben erfüllt heisst das nicht, dass sie aus Konzeptionssicht einfach blind verwendet werden kann.
Es gibt genug (gereifte) Kritik an Elementen im der .NET Welt.

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 3 Jahren

Mit Kompilieren hat das nichts zutun. Es geht auch nicht um das Delegate an sich. Es geht um die Verantwortung der Ausführung.
Und mit der Methoden-Lösung verlagerst Du die Verantwortung von der UI in die Logik und schaffst damit eine Abhängigkeit.

Dass ich erwähnt habe, trotzdem unabhängig kompilieren zu können, bezog sich nur auf den Artikel mit der Schichtentrennung (Drei-Schichten-Architektur), in dem es ja darum ging, die Schichten zu trennen, um sie im Team unabhängig voneinander zu entwickeln. Das geht ja mit Action nach wie vor.

Ich verstehe jetzt dank deiner Ausführungen besser, was damit noch gemeint sein kann, aber nur auf einer sehr theoretischen Basis, die ich auch als sehr winzig empfinde, wenn man es etwas genauer formuliert:

Action: "Hey, gib mir doch eine Methode, die ICH für DICH ausführe, wenn ich die Action aufrufe."

Event: "Hey, gib mir doch eine Methode, die ICH für DICH ausführe, wenn ich das Event auslöse."

=> m.E. ist es beim Event auch ein "ICH für DICH", nicht nur da es im gleichen Thread geschieht (das sagt ja noch nicht viel aus) sondern einfach, da das Auslösen des Events ja alle Methoden aufruft die dranhängen. Es ist nur ein wenig indirekter. Der einzige konkrete Unterschied den ich sehe ist, dass beim Event auch mehrere oder keine Methoden aufgerufen werden könnten statt genau einer (oder keiner falls die Action = null ist und man das so implementiert).

Ich tue mich schwer damit, aufgrund theoretischer Erwägungen Code zu ändern, insbesondere wenn er komplizierter wird (in meinem Fall: eine Klasse für das Event statt nur eine Action). Dafür brauche ich schon Gründe.

Was sind denn die ganz konkreten Unterschiede, z.B. wie von dir erwähnt bzgl. Exception-Handling oder Thread-Safety?

16.806 Beiträge seit 2008
vor 3 Jahren

Exception-Handling und Thread-Safety waren ja nur Beispiele zur Verdeutlichung der konzeptionellen Verantwortung.
Erweiterbarkeit wäre das nächste.

Mit einem int-only Delegate hast Du quasi Null Erweiterungsmöglichkeite, zB. um Fortschrittdetails zB wie Datei-Namen anzuzeigen oder darum anzureichern.

Persönlich sehe ich null sinnvolle Nutzung eines solchen Delegates - oder gar irgendwelche Vorteile.
Sehe konzeptionell nur Nachteile. Der einzige Vorteil ist ja: "Hey, ich brauch 5 Zeilen weniger Code."

Die Abwägung der Dinge, ob Du es daher nutzt oder nicht - die liegt ja bei Dir.
Das Forum kann Dir ja nur Gründe für oder gegeben geben.

T
timbu42 Themenstarter:in
31 Beiträge seit 2017
vor 3 Jahren

Ok, alles klar, vielen Dank für die ausführlichen Erläuterungen!

Ich habe gedacht, es gäbe ein ganz konkretes Beispiel, wo die Lösung in Exception-Handling oder Thread-Safety schlechter ist, aber Erweiterbarkeit ist ein Argument, das stimmt 😃