Hallo!
Ich möchte innerhalb einer asynchronen Abfrage auf eine MessageBox-Result warten, meine Ansätze waren leider nicht erfolgreich.
Ich habe die asynchrone Methode die auf die Antwort der MessageBox warten soll:
private async void PKDataRestoreExecuted(object obj)
{
WndOpacity = 0.7; // Transparenz der View absenken
arWPF.Compression.ZIP.ZIPErgebnis Erg = await RunRückSicherung(); // Rücksicherung ausführen
if (Erg.Abbruch) ... // Benutzer-Abbruch
WndOpacity = 1; // Fenster-Transparenz zurücksetzen
}
Es soll also auf die Methode RunRücksicherung gewartet werden. In ihr habe ich eine MessageBox in der der Benutzer etwas auswählen soll:
private async Task<arWPF.Compression.ZIP.ZIPErgebnis> RunRückSicherung()
{
arWPF.Compression.ZIP.ZIPErgebnis Erg; // Ergebnis-Indikator deklarieren
switch (MessageBox.Show(Mldg, "Bitte wählen ...", MessageBoxButton.YesNoCancel))
{
case MessageBoxResult.No: // Benutzer-Auswahl der Sicherungs-Datei
Erg = Methode1(); break; // Methode1 ausführen
case MessageBoxResult.Yes: // Letzte Datensicherung zurücksichern
Erg = Methode2(); break; // Methode2 ausführen
default: { Erg = new(); Erg.Abbruch = true; } break; // Vorgang abbrechen
}
await Task.Delay(100);
return Erg;
}
Zur Laufzeit wird die Methode RunRücksicherung schon aufgerufen, aber ich befinde mich immer noch im Hauptthread und auch die Absenkung der View-Opacity wird nicht ausgeführt.
Woran liegt das und wie kann ich es ändern?
Wenn Du schon deutschen Code hast, lass wenigstens die Umlaute weg... is ja schrecklich 😦
Damit das funktioniert muss a) Dein Zeug die Async/Await unterstützten und b) das Zeug auch richtig umgesetzt sein.
Ich seh hier ein async void, was in der Form nur bei Events erlaubt ist. Das sorgt aber dafür, dass der async-Flow gebrochen wird - es gibt kein Task, auf den "gewartet" werden kann. Auch ist das mit dem Feld "Erg" etwas weird...
Zeig die ganze Nutzung.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Hallo Abt!!!
Mit den Umlauten bessere ich mich ...
Das async void stellt für mich ein Dummy für asynchrone Methoden dar, die eigentlich keine await Methode haben. Wie in diesem Fall die De-Kompressesions-Methoden.
Apropos hier die Methode aus meiner Bibliothek:
/// <summary>
/// Extrahiert den Inhalt der Archiv-Datei in das (übergebene) Ziel-Verzeichnis.
/// </summary>
/// <param name="ZielVerzeichnis">Das Verzeichnis, in das der Inhalt der Archiv-Datei entpackt werden soll.</param>
/// <param name="ArchivDateiName">Der Name (+Verzeichnis) der Archiv-Datei, dessen Inhalt in das Ziel-Verzeichnis entpackt werden soll. Standard: Leer -> Benutzer-Auswahl</param>
/// <param name="StartArchivVerzeichnis">Das Verzeichnis, von dem aus der Benutzer die Archiv-Datei auswählen kann. Standard: Das Programm-Daten-Verzeichnis des Benutzers.</param>
/// <param name="DateienÜberschreiben">Indikator: Sollen bestehende Dateien im Ziel-Verzeichnis überschrieben werden? Standard: Nein</param>
/// <returns>Indikator: Der Inhalt der Archiv-Datei wurde erfolgreich in das Ziel-Verzeichnis extrahiert (oder nicht).</returns>
public static ZIPErgebnis ArchivToVerzeichnis(string ZielVerzeichnis, string ArchivDateiName = "", string StartArchivVerzeichnis = "" , bool DateienÜberschreiben = false)
{
if (string.IsNullOrEmpty(StartArchivVerzeichnis)) // Wurde das Start Archiv-Verzeichnis nicht angegeben, Standard Archiv-Verzeichnis benutzen
StartArchivVerzeichnis = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
ZIPErgebnis ergebnis = new(ZielVerzeichnis, false, StartArchivVerzeichnis, ArchivDateiName); // Zip-Ergebnis initalisieren
if (string.IsNullOrEmpty(ZielVerzeichnis)) // Wurde ein Ziel-Verzeichnis angegeben? Nein -> abbrechen
{
ergebnis.Fehler = new("Es wurde kein Ziel-Verzeichnis (in das der Inhalt der Archiv-Datei entpackt werden soll) angegeben!");
return ergebnis;
}
else // Das Ziel-Verzeichnis wurde angegeben
{
if (!DateienÜberschreiben) // Wurde angegeben, dass die Dateien im Ziel-Verzeichnis nicht überschrieben werden sollen?
if (Directory.Exists(ZielVerzeichnis)) // Ist das Ziel-Verzeichnis schon vorhanden?
if (Directory.GetFiles(ZielVerzeichnis).Length > 0) // Enthält das Ziel-Verzeichnis Datei(en)?
{
ergebnis.Fehler = new("Das Ziel-Verzeichnis: " + ZielVerzeichnis + " exestiert und enthält Dateien!" +
Environment.NewLine + Environment.NewLine +
"Die Option: Dateien nicht überschreiben verhindert eine Fortführung des Vorganges!");
return ergebnis;
}
} // Das Ziel-Verzeichnis kann genutzt werden!
if (string.IsNullOrEmpty(ArchivDateiName)) // Wenn der Archiv (Pfad +) Dateiname nicht übergeben wurde, Archiv-Datei vom Benutzer auswählen lassen
{
Microsoft.Win32.OpenFileDialog dlgDateiAuswahl = new() // Datei Auswahl-Dialog deklarieren
{
Title = "Archiv-Datei auswählen...", // Titel des Datei-Auswahlfensters
InitialDirectory = StartArchivVerzeichnis, // Ausgangsverzeichnis (Eigene Dokumente)
DefaultExt = "zip", // Standard Erweiterung
Filter = "Zip Datei(en)|*.zip", // Filter der Datei-Erweiterungen
ValidateNames = false, // Nicht prüfen, ob die Datei bereits geöffnet oder schreibgeschützt ist
AddExtension = true, // Datei-Erweiterung autom. ergänzen
Multiselect = false // Nur eine Datei auswählbar
};
if (dlgDateiAuswahl.ShowDialog() == true)
{
ArchivDateiName = dlgDateiAuswahl.FileName; // Datei-Auswahl-Dialog aufrufen -> Archiv-Namen Auswahl übernehmen
ergebnis.ArchivDateiName = Path.GetFileName(ArchivDateiName); // Ergebnis Aktualisieren
ergebnis.ArchivVerzeichnis = Path.GetDirectoryName(ArchivDateiName); // Ergebnis Aktualisieren
}
else // Abbruch der Auswahl ...
{
ergebnis.ArchivDateiName = ArchivDateiName; // Ergebnis Aktualisieren
ergebnis.Abbruch = true; // Ergebnis Aktualisieren
return ergebnis; // Bei Abbruch der Archiv-Dateinamen-Auswahl, Vorgang abbrechen
}
} // Archiv-Dateiname ist definiert
try { ZipFile.ExtractToDirectory(ArchivDateiName, ZielVerzeichnis, DateienÜberschreiben); } // Archiv-Datei in das Ziel-Verzeichnis entpacken
catch (Exception ex)
{
ergebnis.Fehler = new("Bei der Extraktion der Archiv-Datei in das Ziel-Verzeichnisses: " +
ZielVerzeichnis + " ist ein Fehler aufgetreten!" +
Environment.NewLine + Environment.NewLine + "Fehler:" +
Environment.NewLine + Environment.NewLine + ex.Message); // Ergebnis Aktualisieren
return ergebnis;
}
ergebnis.Erfolgreich = true; // Ergebnis Aktualisieren
return ergebnis;
}
Das Ergebnis-Feld ist nur eine Hilfs-Klasse, damit ich die Informationen des Vorgangs als ein Objekt zurückgeben kann:
public class ZIPErgebnis
{
/// <summary>
/// Indikator: War der Vorgang erfolgreich?
/// </summary>
public bool Erfolgreich { get; set; } = false;
/// <summary>
/// Das Quell- oder Ziel-Verzeichnis
/// </summary>
public string Verzeichnis { get; set; } = string.Empty;
/// <summary>
/// Das (gewählte) Archiv-Verzeichnis?
/// </summary>
public string ArchivVerzeichnis { get; set; } = string.Empty;
/// <summary>
/// Der (gewählte) Archiv-Datei-Name?
/// </summary>
public string ArchivDateiName { get; set; } = string.Empty;
/// <summary>
/// Fehler der gegebenfalls bei dem Vorgang aufgetreten ist?
/// </summary>
public Exception Fehler { get; set; } = new();
/// <summary>
/// Indikator: Wurde der Vorgang durch den Benutzer abgebrochen?
/// </summary>
public bool Abbruch { get; set; } = false;
public ZIPErgebnis() { }
public ZIPErgebnis(string verzeichnis, bool erfolgreich, string archivVerzeichnis, string archivDateiName, Exception fehler = null, bool abbruch = false)
{
Erfolgreich = erfolgreich; ArchivVerzeichnis = archivVerzeichnis;
ArchivDateiName = archivDateiName; Fehler = fehler; Abbruch = abbruch;
Verzeichnis = verzeichnis;
}
}
Mehr gibt es meines Erachtens nicht.
Zitat von perlfred
Das async void stellt für mich ein Dummy für asynchrone Methoden dar, die eigentlich keine await Methode haben. Wie in diesem Fall die De-Kompressesions-Methoden.
async void sorgt dafür, dass der Async-Await Flow nicht mehr funktioniet. Daher gilt es als Pitfall und darf nur in ganz spezifischen Situationen verwendet werden.
Ich seh in Deinem Code kein nirgens, wo PKDataRestoreExecuted aufgerufen / deklariert wird.
Du wirst irgendwo einen async/await-Flow-Fehler haben. Async/await muss zu 100% korrekt angewendet werden, oder Du wirst Überraschungen haben. Selbst Dinge erfinden ist da ne ganz schlechte Idee.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Hallo Abt!
Das
private async void PKDataRestoreExecuted(object obj)
ist die Command-Routine des Commands das ich an die Rücksichern-Schaltfläche binde.
public ICommand PKDataRestoreCommand { get; private set; } // Daten zurücksichern Command
PKDataRestoreCommand = new ActionCommand(PKDataRestoreExecuted, PKDataRestoreCanExecute); // Programm-Konfiguration: // Daten zurücksichern Command, initalisieren
Ist für mich erst einmal Standard, von einem Command ein async/await aufzurufen.
Das Problem muss irgendwie mit der MessageBox zusammenhängen.
Die Übrige Konstellation: Command → Async/Await → Methode die await Task.Delay() als Dummy benutzt, habe ich schon sehr oft ohne Fehler benutzt.
Trotzdem Danke! für deine Hinweise!
Welche ActionCommand
-Implementierung verwendest du denn? Ist diese überhaupt asynchron implementiert, wie z.B. MVVM - Going async with async command?
Ich versteh auch nicht, warum du eine MessageBox
überhaupt asynchron aufrufen willst (diese läuft in einer von Hauptfenster unabhängigen eigenen Nachrichtenschleife - und das Hauptfenster ist doch sowieso währenddessen blockiert)?
Oder soll statt dem Task.Delay
dort noch ein anderer zeitaufwendigerer asynchroner Vorgang ausgeführt werden?
Hallo Th69!
Bin gerade ein wenig in der Krise und muss auch noch etwas testen ...
Erst einmal grundsätzlich, muss die MessageBox eigentlich nicht asynchron aufgerufen werden.
Ich wollte dem Benutzer nur signalisieren, dass er den Rücksicherungsvorgang eingeleitet hat und über die MessageBox soll er auswählen, welchen Umfang die Rücksicherung haben soll.
Mein Ausgangsproblem war, dass sich die View-Opacity bei der Auslösung des Commands nicht geändert hat!
Dabei ist mir dann aufgefallen, dass sich die RunRücksicherungs-Methode im Hauptthread befindet. Deshalb wollte ich die MessageBox dann auch asynchron abfragen.
Ich habe für die asynchrone MessageBox jetzt noch eine "Lösung" im Internet gefunden, die die Problematik noch mehr verwirrt.
await Task.Run(() =>
{
WndOpacity = 0.7; // Transparenz der View absenken
MessageBoxResult dialogResult = MessageBox.Show("Ja -> Opacity = 1, Nein -> Opacity bleibt bei 0,7", "Titel",
MessageBoxButton.YesNo);
if (dialogResult == MessageBoxResult.Yes)
{
_ = MessageBox.Show("Ja Clicked");
WndOpacity = 1;
}
else
MessageBox.Show("Nein Clicked");
});
Diese rufe ich innerhalb meiner Command-Methode (die ich fast vollkommen leer gemacht habe) auf.
Wenn ich diese Debugge sehe ich, dass die Task.Run-Methode in einem ArbeitsThread läuft! (siehe Bild)
Und trotzdem wird die Opacity der View nicht geändert!!!!!!
Das muss ich jetzt erst einmal untersuchen.
Zu deiner Frage noch, das (gleiche, da dies in der Basisklasse definiert ist) ActionCommand verwende ich in der gleichen Applikation auch an anderen Stellen asynchron und da ist alles korrekt (Allerdings ohne MessageBox!).
Also mein erster Gedanke war ja, wenn er schon unbedingt deutsche Bezeichner nimmt, dann wenigstens konsequent auch mit Umlauten wo welche hingehören 😉
Rueckgabe .... sieht doch furchtbar aus.
Mein zweiter Gedanke war, die Kommentare irgendwo weit weit hinten in den Zeilen machen in weniger breiten Ansichten Probleme, sind in dieser Intensität sicher nicht nötig (oft erzählen sie nur das was doch sowieso schon da steht) und das Konzept sie immer auf gleiche Höhe zu setzen wird sicherlich schnell lästig.
Ich wollte dem Benutzer nur signalisieren, dass er den Rücksicherungsvorgang eingeleitet hat und über die MessageBox soll er auswählen, welchen Umfang die Rücksicherung haben soll.
Wat denn nu, wurde der Vorgang schon eingeleitet oder muss noch was dazu ausgewählt werden?
Ohne deinen genauen Fall zu kennen würde ich vorschlagen, der Benutzer soll doch einfach vor dem Start schon alles festlegen was passieren soll.
Der erste Punkt ist schon, dass Du UI-Code in Deiner Logik hast. Die Message-Box gehört da nicht hin. Frag doch den User vor dem Start der Logik.
Das ist die Wurzel der Situation. Behebst Du das, dann hast Du 90% der Probleme hier gelöst.
Zitat von perlfred
Erst einmal grundsätzlich, muss die MessageBox eigentlich nicht asynchron aufgerufen werden.
Zweitens sagt das niemand, und funktioniert das technisch auch nicht.
Aber Dein Gesamtkonstrukt muss korrekt sein, ansonsten stimmt der gesamte Sync Context nicht mehr ⇒ Race Condition.
Sehr vereinfacht ausgedrückt:
Aktuell ruft Dein Command über einen synchronen Weg eine asynchrone Methode auf und verwirft den Task (durch das void). Im Endeffekt ein Fire-and-Forget.
Willst Du async/await korrekt aufrufen, muss der Command-Call bereits über einen asynchronen Weg erfolgen (was es von Haus aus nicht gibt, siehe Link Th69).
Zu deiner Frage noch, das (gleiche, da dies in der Basisklasse definiert ist) ActionCommand verwende ich in der gleichen Applikation auch an anderen Stellen asynchron und da ist alles korrekt (Allerdings ohne MessageBox!).
Das würde ich als purer Zufall bezeichnen, weil Du offenbar bisher Fälle hattest, in der es keine Race Conditions gab und Dir der Context Sync viel geschenkt hat.
Hier ist aber ein Fall, bei der Du sofort eine Race Condition hast, wenn Du async/await falsch anwendest - mit entsprechendem Resultat.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Hallo Abt, Hallo Th69!
Die Ursache lag (natürlich) ganz, ganz wo Anders. Das Binding zur View-Opacity war Schlicht und Einfach korrumpiert (gibt bestimmt auch noch einen knallige Fachbezeichnung!). Ich hatte die View-Opacity einmal an eine Viewmodel Eigenschaft und einmal über eine (in XAML definierte) Animation "gebunden". Wenn ich dann die Animation einmal ausgeführt hatte war es um beide "Bindings" geschehen! Und das nicht reagieren der View-Opacity war ja der Ausgangspunkt meiner ganzen Überlegungen.
Danke Th69 deine Erklärung zur MessageBox:
diese läuft in einer von Hauptfenster unabhängigen eigenen Nachrichtenschleife
brachte mich zum Suchen in andere Richtungen!
Was bleibt ist, dass die RunRückSicherung-Methode nach wie vor im Hauptthread läuft. Da der Vorgang aber sowieso erst fortgeführt werden kann, wenn die Datenrücksicherung abgeschlossen ist, stört mich die Hauptthread-Blockade erst einmal nicht.
Bei der Lösung wo das MessageBox-Ergebnis im Abeits-Thread abgefragt werden kann:
await Task.Run(() =>
{
_ = AnimiereViewHelligkeit(((MainWindow)obj).Resources, "Dunkel"); // View Helligkeit Dunkel (Animation ausführen)
MessageBoxResult dialogResult = MessageBox.Show("Ja -> Opacity = 1, Nein -> Opacity bleibt bei 0,7", "Titel",
MessageBoxButton.YesNo);
if (dialogResult == MessageBoxResult.Yes)
{
_ = MessageBox.Show("Ja Clicked");
_ = AnimiereViewHelligkeit(((MainWindow)obj).Resources); // View Helligkeit Hell (Animation ausführen)
}
else
MessageBox.Show("Nein Clicked");
});
muss man noch mit Invoke arbeiten, um den Zugriff zu den Resourcen zu ermöglichen.
Vielen Dank für die vielen Hinweise!!!
Das ist halt weiterhin ein Workaround, und nicht die korrekte Anwendung von async/await in WPF...
muss man noch mit Invoke arbeiten, um den Zugriff zu den Resourcen zu ermöglichen.
Was man nicht muss, wenn man es korrekt anwendet 😃
Deshalb gibts den Sync-Context.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Hallo Abt, Hallo chilic!
Ihr sprecht beide das Gleiche an, aber wie soll es denn richtig realisiert werden?
Ich habe eine Schaltfläche die eine Rücksicherung einleiten soll. Sprich Rücksicherungs-Command.
Der Benutzer soll mit dem Auslösen des Commands eine optische Bestätigung, dass er einen Vorgang initiiert hat, bekommen.
Entspricht meiner Command-Routine:
private async void PKDataRestoreExecuted(object obj)
{
WndOpacity = 0.7; // Transparenz der View absenken
arWPF.Compression.ZIP.ZIPErgebnis Erg = await RunRückSicherung(); // Rücksicherung ausführen
if (Erg.Abbruch) ... // Benutzer-Abbruch
WndOpacity = 1; // Fenster-Transparenz zurücksetzen
}
Die Logik in der RunRücksicherung -Methode habe ich nun auch noch in die Command-Methode übernommen. Bleibt für den Daten-Thread nur noch das (Parameter gesteuerte) De-Komprimieren der Zip-Datei.
An dieser Stelle müsste ich auch noch einmal "nachschärfen", da ich in meiner Bibliothek in einem Fall die Abfrage des Zielverzeichnisses integriert habe. Da stellt sich die Frage, ob meine Bibliothek dann so viel bringt.
Aber dann ... wäre es korrekt? Oder?
Jetzt heißt es Kopf abkühlen Danke! und gute Nacht.
Du brauchst und solltest bei einer UI-Aktion (Edit: hier Anzeigen der MessageBox
) kein async/await
verwenden. Jede UI-Aktion muß zwingend im UI-Thread, d.h. Hauptthread, erfolgen.
Laß also das await Task.Run()
komplett weg!
Zitat von Th69
Du brauchst und solltest bei einer UI-Aktion kein
async/await
verwenden. Jede UI-Aktion muß zwingend im UI-Thread, d.h. Hauptthread, erfolgen.
Glaube das kann in der Form so missverstanden werden.
Ja, die UI-Operation selbst muss zwangsweise im UI-Thread erfolgen; aber natürlich darf eine UI eine asynchrone Aktion auslösen.
Das hat sogar den Vorteil, dass man sich aufgrund des Sync Context (bei WPF eben der DispatcherSynchronizationContext ) sich in 99% der Fälle um UI-Synchronisation kümmern muss.
Siehe auch [FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke)
Rick Strahl hat dazu einen guten Beitrag, wie mit await Dispatcher.InvokeAsync() einige eigenartige Verhalten in WPF behoben werden können.
Das Fazit bleibt: async/await muss korrekt und durchgängig angewandt werden, wie auch sollte man UI-Zeugs nich in irgendwelche Operationen wursteln.
- performance is a feature -
Microsoft MVP - @Website - @AzureStuttgart - github.com/BenjaminAbt - Sustainable Code
Das ist mir klar und ich habe es gerade oben noch editiert. Mit UI-Aktion meine ich nicht das Auslösen, sondern das Verwenden.
Es gibt bei dem Code von perlfred hier ja gar keine asynchrone Operation (wie z.B. eine längerfristige Datenzugriffsoperation).
Ich hätte hier mit await Task.Yield()
gearbeitet, das funktioniert bei mir mit den Dialogen immer sehr gut.
Hat die Blume einen Knick, war der Schmetterling zu dick.
Der Benutzer soll mit dem Auslösen des Commands eine optische Bestätigung, dass er einen Vorgang initiiert hat, bekommen.
Jetzt das große Aber,wenn der Benutzer nach diesem Auslösen nochmal was angeben muss, dann hat er doch den Vorgang noch nicht wirklich initiiert.
Dazu kannst du sicher verschiedene Meinungen bekommen, aber für mich ist es vor allem bei längeren Operationen wichtig dass ich sehe: jetzt läufts von selbst und will nichts mehr von mir. Dann kann man sich nämlich etwas anderem widmen.
Ich finde es unheimlich lästig wenn ich nach einiger Zeit wieder an den Rechner komme und sehe dass letztendlich noch überhaupt nichts passiert ist, weil ein paar Sekunden nachdem ich mich weggedreht habe, das Programm doch nochmal was von mir wollte.
Unabhängig von all dem frage ich mich, warum die MessageBox unbedingt in den asynchronen Ablauf hinein soll. So wie ich das verstehe kommt die zu Beginn des Ablaufs? Lässt sich das nicht alles vorher direkt abfragen und die eigentliche Aufgabe startet dann im asynchronen Teil?
Oder ist es wirklich so dass erst mal ein Teil der Arbeit passiert und sich zwischendrin dann erst eine Frage stellt?