Häufig muss der User warten, wenn er eine asynchrone Operation auslöst, und darf währenddessen nicht weiter im Programm herumklicksen.
Da ist das einfachste, einen modalen Dialog anzuzeigen - der bewirkt ja automatisch genau das: Eine Sperre des Programms, bis er (der Dialog) geschlossen wird.
Also Anforderungen:
Als erstes benötigt man natürlich überhaupt eine langdauernde Methode. Und zwar eine, die Cancellation unterstützt - hab ich beispielhaft mal so umgesetzt:
Also 100 mal schläft der Thread 20ms, um dann jeweils
Das ist nämlich der Cancellation-Mechanismus: Von ausserhalb des Threads kann das Token auf Cancel gesetzt werden, und im Thread kann es dann eine Exception werfen.
Das Exception-Werfen passiert also nicht von selbst, sondern des Tokens SelbstPrüfung muss in kurzen Abständen immer wieder aufgerufen werden!
Der Async-Pattern ermöglicht nun, im Main-Thread diese Exception zu fangen.
Er kann da übrigens alle Exceptions fangen. Also zusammen mit Cancellation ist threadübergreifendes Exception-Handling mit abgehandelt.
Bleibt noch eine Hakeligkeit: Wie kriegen wir einen Dialog modal geöffnet und gleichzeitig eine asynchrone Operation gestartet?
Öffnen wir den Dialog modal gehts erst weiter, wenn er geschlossen ist - keine Möglichkeit, noch Code auszuführen, der die async-Op dann startet.
Andersrum genauso: Die Methode, die mit
Aber es gibt einen eleganten Trick: Mit
(
Hier aber rufen wir es vom MainThread selbst auf - das ist eigenartig: Eine Aktion, delegiert vom MainThread an den MainThread.)
Ich hab dazu im BusyDialog-Form eine Shared Methode gebastelt, die sich darum kümmert:
Also sie erstellt und returnt ein IsBusyDialog-Objekt, und kümmert sich dabei um drei Dinge:
Der Dialog wird geöffnet im Using-Block (#7). Das bedeutet bekanntlich, dass er zuverlässig auch wieder geschlossen wird - nämlich wenn der UsingBlock verlassen wird.
Der TryCatch kann nun mögliche Exceptions behandeln, geworfen aus der asynchronen Methode. Hier nur die
Jedenfalls wird so unterschieden zwischen erfolgreicher async-Op (#10) und Cancellation (#12).
Beachte, wie das CancellationToken des Dialogs beim Starten der ansync-Op mit eingeht (#9).
Eine Bitterlichkeit ist ebenfalls in #9 angedeutet: Dort wird nicht wie eiglich zu erwarten wäre:
Der Grund liegt darin, dass
Also hab ich folgende Helper-Klasse basteln müssen, die kann, was Task.Run nicht kann:
Es sind zwei Überladungen, einmal für mit Rückgabewert (
Die Sourcen enthalten auch eine c#-Version
Dank an @shad - er hat den Trick mit
Und auch an @exc-jdbi - ich hätte es nie gemerkt, dass CancellationTokenSource IDisposable ist
Da ist das einfachste, einen modalen Dialog anzuzeigen - der bewirkt ja automatisch genau das: Eine Sperre des Programms, bis er (der Dialog) geschlossen wird.
Also Anforderungen:
- einen Dialog modal anzeigen, während parallel eine asynchrone Operation läuft
- der Dialog soll automatisch schliessen, wenn die Operation durchgelaufen ist
- Und auch andersrum: Wird der Dialog vom User geschlossen soll die Operation canceln
Als erstes benötigt man natürlich überhaupt eine langdauernde Methode. Und zwar eine, die Cancellation unterstützt - hab ich beispielhaft mal so umgesetzt:
tk.ThrowIfCancellationRequested()
aufzurufen.Das ist nämlich der Cancellation-Mechanismus: Von ausserhalb des Threads kann das Token auf Cancel gesetzt werden, und im Thread kann es dann eine Exception werfen.
Das Exception-Werfen passiert also nicht von selbst, sondern des Tokens SelbstPrüfung muss in kurzen Abständen immer wieder aufgerufen werden!
Der Async-Pattern ermöglicht nun, im Main-Thread diese Exception zu fangen.
Er kann da übrigens alle Exceptions fangen. Also zusammen mit Cancellation ist threadübergreifendes Exception-Handling mit abgehandelt.
Bleibt noch eine Hakeligkeit: Wie kriegen wir einen Dialog modal geöffnet und gleichzeitig eine asynchrone Operation gestartet?
Öffnen wir den Dialog modal gehts erst weiter, wenn er geschlossen ist - keine Möglichkeit, noch Code auszuführen, der die async-Op dann startet.
Andersrum genauso: Die Methode, die mit
Await
die async-Op startet, wartet bis fertig - keine Möglichkeit, den Dialog noch zu öffnen.Aber es gibt einen eleganten Trick: Mit
Control.BeginInvoke()
kann man das Öffnen des Dialogs in die Windows-MessageQueue stellen, und dann noch weiteren Code ausführen, bevor in der MessageQueue das DialogÖffnen dran kommt.(
Control.BeginInvoke()
ist eigentlich ein alter Bekannter, mit dem man von NebenThreads aus Aktionen an den MainThread delegiert: Control.BeginInvoke()Hier aber rufen wir es vom MainThread selbst auf - das ist eigenartig: Eine Aktion, delegiert vom MainThread an den MainThread.)
Ich hab dazu im BusyDialog-Form eine Shared Methode gebastelt, die sich darum kümmert:
VB.NET-Quellcode
- Partial Public Class dlgIsBusy : Inherits Form
- Public Sub New()
- InitializeComponent()
- End Sub
- Public CancellationToken As CancellationToken
- ''' <summary>returns a modal opened dlgIsBusy-Instance. dlgIsBusy provides a CancellationToken for usage in async methods.
- ''' It cancels, when the user closes the dialog. Note: the caller should dispose the dlgIsBusy-Instance.</summary>
- Public Shared Function ShowBusyDialog() As dlgIsBusy
- Dim cts = New CancellationTokenSource()
- Dim dlg = New dlgIsBusy() With {.CancellationToken = cts.Token}
- Dim dummi = dlg.Handle ' enforces to create a Handle. Which is required for successfully call .BeginInvoke()
- dlg.BeginInvoke(Sub()
- dlg.ShowDialog()
- cts.Cancel()
- cts.Dispose()
- End Sub)
- Return dlg
- End Function
- End Class
- eine CancellationTokenSource erzeugen, und deren Token als von aussen abrufbares Feld bereitstellen (zeilen #12,13).
- den Dialog per
.BeginInvoke()
modal anzeigen (#15-19)
zuvor (#14) muss das Windows-Handle abgerufen werden - ist komisch, aber ist so, sonst funktioniert .BeginInvoke nicht. - Nach
.ShowDialog()
wird die TokenSource gecancelt (#17) - das war ja so geplant: Wenn der User den Dialog schließt, soll die async-OP gecancelt werden.
(Ausserdem wird sie disposed (#18), weil sie ist ja disposable)
VB.NET-Quellcode
- Imports System.Threading
- Public Class Form1
- Private Async Sub btnLongFunc_Click(sender As Object, e As EventArgs) Handles btnLongFunc.Click
- Console.WriteLine($"MainThread-ID: {Thread.CurrentThread.ManagedThreadId}")
- Using dlg = dlgIsBusy.ShowBusyDialog()
- Try
- Dim rslt = Await AsyncHelper.Run(Function() LongFunc(42, dlg.CancellationToken))
- Console.WriteLine($"Done! The result is: {rslt}")
- Catch ex As OperationCanceledException
- Console.WriteLine($"operation cancelled!")
- End Try
- End Using
- End Sub
- Private Shared Function LongFunc(arg As Integer, tk As CancellationToken) As Integer
- Console.WriteLine($"LongFunc - Thread-ID: {Thread.CurrentThread.ManagedThreadId}")
- For i = 0 To 99
- Thread.Sleep(20)
- tk.ThrowIfCancellationRequested()
- Next
- Return arg * 2
- End Function
- End Class
Der TryCatch kann nun mögliche Exceptions behandeln, geworfen aus der asynchronen Methode. Hier nur die
OperationCanceledException
- weitere Exceptions werden nicht erwartet, und können daher auch nicht behandelt werden.Jedenfalls wird so unterschieden zwischen erfolgreicher async-Op (#10) und Cancellation (#12).
Beachte, wie das CancellationToken des Dialogs beim Starten der ansync-Op mit eingeht (#9).
Eine Bitterlichkeit ist ebenfalls in #9 angedeutet: Dort wird nicht wie eiglich zu erwarten wäre:
Await Task.Run()
aufgerufen. Stattdessen Await AsyncHelper.Run()
- eine selbstgebastelte Helper-Klasse.Der Grund liegt darin, dass
Task.Run()
buggy ist! Es transportiert nur dann threadübergreifende Exceptions korrekt in den MainThread, wenn das Projekt als Release kompiliert wird. In Debug-Versionen funktioniert das Feature einfach nicht.Also hab ich folgende Helper-Klasse basteln müssen, die kann, was Task.Run nicht kann:
VB.NET-Quellcode
- Imports System.Threading.Tasks
- Public Class AsyncHelper
- ''' <summary>Queues 'func' to the thread pool and returns a Task(of TResult), that represents that operation.</summary>
- Public Shared Function Run(Of TResult)(func As Func(Of TResult)) As Task(Of TResult)
- Dim tcs = New TaskCompletionSource(Of TResult)()
- Task.Run(Sub() ' invoke the long lasting func() in parallel...
- ' Setting either the result or the exception triggers the main-thread to re-enter and continue after the 'await'-keyword
- Try
- tcs.SetResult(func())
- Catch ex As Exception
- tcs.SetException(ex)
- End Try
- End Sub)
- Return tcs.Task ' but the Completion itself returns immediately!
- End Function
- ''' <summary>Queues 'action' to the thread pool and returns a Task, that represents that operation.</summary>
- Public Shared Function Run(action As Action) As Task
- Dim tcs = New TaskCompletionSource(Of Boolean)()
- Task.Run(Sub()
- Try
- action()
- tcs.SetResult(True)
- Catch ex As Exception
- tcs.SetException(ex)
- End Try
- End Sub)
- Return tcs.Task
- End Function
- End Class
Func(Of T)
), einmal ohne (Action
)Die Sourcen enthalten auch eine c#-Version
Dank an @shad - er hat den Trick mit
.BeginInvoke()
eingebrachtUnd auch an @exc-jdbi - ich hätte es nie gemerkt, dass CancellationTokenSource IDisposable ist
Dieser Beitrag wurde bereits 11 mal editiert, zuletzt von „ErfinderDesRades“ ()