Background worker arbeiten lassen, ohne dass die Form einfriert

  • VB.NET

Es gibt 14 Antworten in diesem Thema. Der letzte Beitrag () ist von DerSmurf.

    Background worker arbeiten lassen, ohne dass die Form einfriert

    Hallo ihr lieben
    Ich möchte in meiner Anwendung eine Backupfunktion einbauen. Dabei sollen einfach verschiedene Dateien gezippt werden.
    Die Zip Funktion des Frameworks habe ich verstanden und der Code ist fertig.
    Nun soll das zipen aber (wenn möglich) im Hintergrund passieren - also dass mit dem Programm weiter gearbeitet werden kann.

    Dazu setze ich mich gerade mit dem Background Worker auseinander (zum ersten mal).
    Zum verstehen habe ich eine kleine Testanweisung geschrieben, welche eine einstellbare Menge Textdateien auf dem Desktop (im Unterordner: txtFiles) erstellt.
    Während der Erstellung, wird eine Progressbar mit dem Prozentualen Status angezeigt. <-- Funktioniert
    Außerdem soll ein Listview mit den Dateinamen, sowie ein Label mit Datei x von y angezeigt werden. <-- funktioniert nur halb
    und das abbrechen des Vorgangs, soll durch ein Button Klick möglich sein.

    So generell ist mir das alles gelungen, aber es funktioniert nur, wenn ich im DoWork Event des Background workers eine Verzögerung einbaue (System.Threading.Thread.Sleep(100))
    wenn ich diese Zeile (49) auskommentiere, funktioniert der Code und die Progressbar aktualisiert sich. Der Rest der Form ist aber wie eingefroreren.
    Also so, als würde ich das normal linear abarbeiten.
    Ist das normal, oder habe ich einen Fehler eingebaut?

    Spoiler anzeigen

    VB.NET-Quellcode

    1. Public Class frmMain
    2. Private Sub btnCancel_Click(sender As Object, e As EventArgs) Handles btnCancel.Click
    3. BackgroundWorker1.CancelAsync()
    4. End Sub
    5. Private Sub btncreateFiles_Click(sender As Object, e As EventArgs) Handles btncreateFiles.Click
    6. 'Listview und Label löschen
    7. LVtxtProcessed.Items.Clear()
    8. LBLMessage.Text = ""
    9. ' Backgroundworker starten
    10. If NUDtxtAmount.Value > 0 Then
    11. BackgroundWorker1.RunWorkerAsync(CInt(NUDtxtAmount.Value))
    12. Else
    13. MessageBox.Show("es gibt nichts zu tun")
    14. End If
    15. End Sub
    16. Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object,
    17. ByVal e As System.ComponentModel.DoWorkEventArgs) _
    18. Handles BackgroundWorker1.DoWork
    19. Dim txtPath As String = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) & "\txtFiles\"
    20. If Not IO.Directory.Exists(txtPath) Then IO.Directory.CreateDirectory(txtPath)
    21. Dim streami As System.IO.FileStream
    22. Dim txtAmount As Integer = DirectCast(e.Argument, Integer)
    23. For i = 1 To txtAmount
    24. ' Abbrechen wenn Cancel-Button gedrück wurde erlauben
    25. If BackgroundWorker1.CancellationPending Then
    26. e.Cancel = True
    27. Return
    28. End If
    29. 'txt Datei erstellen
    30. Dim txtFilename As String = "Textdatei " & i & ".txt"
    31. streami = System.IO.File.Create(txtPath & txtFilename)
    32. Dim cfile As System.IO.StreamWriter = New System.IO.StreamWriter(streami, System.Text.Encoding.Default)
    33. cfile.WriteLine("Textdatei " & i.ToString)
    34. cfile.Close()
    35. ' Fortschritt in Prozent melden und Datei übergeben
    36. BackgroundWorker1.ReportProgress(CInt((i) * 100 / (txtAmount + 1)), New Object() {txtFilename, i.ToString, txtAmount.ToString})
    37. 'ggf. System schlafen legen, um aktualisierung zu erreichen
    38. System.Threading.Thread.Sleep(100)
    39. Next
    40. End Sub
    41. Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) _
    42. Handles BackgroundWorker1.ProgressChanged
    43. 'Paramter abfragen
    44. Dim obj As Object() = DirectCast(e.UserState, Object())
    45. Dim txtFileName As String = obj(0).ToString()
    46. Dim i As String = obj(1).ToString()
    47. Dim txtAmount As String = obj(2).ToString()
    48. ' erstellte txt in Listview ausgeben
    49. Dim item As System.Windows.Forms.ListViewItem
    50. item = LVtxtProcessed.Items.Add(LVtxtProcessed.Items.Count.ToString, txtFileName, "")
    51. ' Status in Progressbbar ausgeben
    52. ProgressBar1.Value = e.ProgressPercentage
    53. 'Messagebox aktualisieren
    54. LBLMessage.Text = "Datei " & i & " von " & txtAmount & " erstellt."
    55. End Sub
    56. End Class
    Dateien

    DerSmurf schrieb:

    Nun soll das zipen aber (wenn möglich) im Hintergrund passieren - also dass mit dem Programm weiter gearbeitet werden kann.
    BackGroundWorker ist Steinkohle.
    Nutze es das Async-Await-Pattern.
    docs.microsoft.com/de-de/dotne…ing-guide/concepts/async/
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Hmm. Also ich habe kleinere Verständnisprobleme mit der Umsetzung.
    Meine StartMethode sieht nun so aus:

    VB.NET-Quellcode

    1. Private Async Sub btncreateFilesasync_Click(sender As Object, e As EventArgs) Handles btncreateFilesasync.Click
    2. 'Listview und Label löschen
    3. LVtxtProcessed.Items.Clear()
    4. LBLMessage.Text = ""
    5. ' Async starten
    6. If NUDtxtAmount.Value > 0 Then
    7. Await Task.Run(Sub() CreateTXTFiles(CInt(NUDtxtAmount.Value)))
    8. Else
    9. MessageBox.Show("es gibt nichts zu tun")
    10. End If
    11. End Sub

    Das klappt, die Textdateien werden schön im Hintergrund erzeugt.
    Ich kann aber innerhalb meiner CreateTXTFiles Sub nicht auf Steuerelemente zugreifen.

    VB.NET-Quellcode

    1. Private Sub CreateTXTFiles(txtAmount As Integer)
    2. Dim txtPath As String = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) & "\txtFiles\"
    3. If Not IO.Directory.Exists(txtPath) Then IO.Directory.CreateDirectory(txtPath)
    4. Dim streami As System.IO.FileStream
    5. For i = 1 To txtAmount
    6. 'TODO Abbrechen implementieren
    7. ' Abbrechen wenn Cancel-Button gedrück wurde erlauben
    8. 'If BackgroundWorker1.CancellationPending Then
    9. ' e.Cancel = True
    10. ' Return
    11. 'End If
    12. 'txt Datei erstellen
    13. Dim txtFilename As String = "Textdatei " & i & ".txt"
    14. streami = System.IO.File.Create(txtPath & txtFilename)
    15. Dim cfile As System.IO.StreamWriter = New System.IO.StreamWriter(streami, System.Text.Encoding.Default)
    16. cfile.WriteLine("Textdatei " & i.ToString)
    17. cfile.Close()
    18. ' Fortschritt in Prozent ausrechnen
    19. Dim progress = CInt((i) * 100 / (txtAmount + 1))
    20. '' erstellte txt in Listview ausgeben
    21. 'Dim item As System.Windows.Forms.ListViewItem
    22. 'item = LVtxtProcessed.Items.Add(LVtxtProcessed.Items.Count.ToString, txtFilename, "")
    23. '' Status in Progressbbar ausgeben
    24. 'ProgressBar1.Value = progress
    25. ''Messagebox aktualisieren
    26. 'LBLMessage.Text = "Datei " & i & " von " & txtAmount & " erstellt."
    27. Next
    28. End Sub


    Also innerhalb der Zeilen 28 bis 36 kommt es zum Fehler "Ungültiger threadübergreifender Vorgang: Der Zugriff auf das Steuerelement LVtxtProcessed erfolgte von einem anderen Thread als dem Thread, für den es erstellt wurde."

    Wenn ich die Erklärungen richtig verstanden habe, kommt dieser Fehler, weil jetzt mein GUI Thread irgendwie ein anderer ist als der Task den ich erstellt habe. Die können wohl nicht mehr miteinander reden.
    Ich bräuchte also einen Rückgabewert von der Sub (oder dann Function) CreateTXTFIles?
    Aber das bringt mir doch auch nichts, denn die Aufrufsub . also das Button Click Event wird ja verlassen, während die CreateTXT Sub noch läuft.

    Wie löse ich das Problem, dass meine Async Operation das GUI "updaten" kann?

    Edit: Ich schmeiß die Solution nochmal ran. Achtung der Button Async abbrechen hat noch keine Funktion.
    Dateien

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „DerSmurf“ ()

    DerSmurf schrieb:

    Also innerhalb der Zeilen 28 bis 36 kommt es zum Fehler "Ungültiger threadübergreifender Vorgang
    Klar.
    Du willst etwas nebenläufig abarbeiten, also läuft das in einem anderen Thread.
    Auch ein BackGroundWorker läuft in einem anderen Thread.
    Wenn Du während der Arbeit in einem Thread etwas an der GUI darstellen willst, musst Du diese GUI-Ausgabe invoken.
    Aber:
    Überlege, ob es sinnvoll ist, Arbeit auszulagern und dann jeden Schritt an der GUI verfolgen zu wollen.
    Das dauert dann ggf. länger als den Thread in Ruhe ohne GUI-Ausgaben seine Arbeit tun zu lassen.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    RodFromGermany schrieb:

    Überlege, ob es sinnvoll ist, Arbeit auszulagern und dann jeden Schritt an der GUI verfolgen zu wollen.

    Jain Also generell halte ich es für unsinnig, aber wenn ich z.B. 5000 Txt Dateien erstelle, ist es doch schön, wenn der User in einer Progressbar über den Status informiert wird.
    Das ändern eines Labels, sowie das schreiben der Dateinamen in das Listview ist nur eine sinnlose Spielerei (bzw. ein Test).
    Aber wie gesagt, Progressbar halte ich schon für nicht ganz unsinnig.

    RodFromGermany schrieb:

    Wenn Du während der Arbeit in einem Thread etwas an der GUI darstellen willst, musst Du diese GUI-Ausgabe invoken.

    Hast du hier ein Beispiel?
    invoken macht man bei Async mit der progress(Of T)-Klasse.

    DerSmurf schrieb:

    Beispiel?

    Async, Await und Task
    allerdings auf CodeProject hab ich eine verbesserte Progress-Klasse, die immer mindestens 300ms verstreichen lässt, ehe sie den nächsten Progress ausgibt.
    muss man sich da nicht auch noch drum kümmern.
    codeproject.com/Articles/10296…ithout-any-additional-Lin

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „ErfinderDesRades“ ()

    DerSmurf schrieb:

    Hast du hier ein Beispiel?
    833 Posts im Forum und Du bist nicht in der Lage, selbst ein Beispiel zu finden?
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!

    RodFromGermany schrieb:

    833 Posts im Forum und Du bist nicht in der Lage, selbst ein Beispiel zu finden?

    Doch natürlich. Ich bin jedoch nicht in der Lage zu beurteilen, ob es gut ist, oder nicht.
    Ich sage nur, statt Await, habe ich mich mit dem Background worker beschäftigt...

    Mein Code sieht jetzt so aus. Ist das wirklich alles?
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Private WithEvents _ProgressPercent As New Progress(Of Integer)
    2. Private Sub Progress_ProgressChanged(sender As Object, e As Integer) Handles _ProgressPercent.ProgressChanged
    3. ProgressBar1.Value = e
    4. End Sub
    5. Private Async Sub btncreateFilesasync_Click(sender As Object, e As EventArgs) Handles btncreateFilesasync.Click
    6. 'Listview und Label löschen
    7. LVtxtProcessed.Items.Clear()
    8. LBLMessage.Text = "Textdateien werden erestellt"
    9. ' Backgroundworker starten
    10. If NUDtxtAmount.Value > 0 Then
    11. Await Task.Run(Sub() CreateTXTFiles(CInt(NUDtxtAmount.Value)))
    12. Else
    13. MessageBox.Show("es gibt nichts zu tun")
    14. End If
    15. LBLMessage.Text = "fertig"
    16. End Sub
    17. Private Sub CreateTXTFiles(txtAmount As Integer)
    18. Dim txtPath As String = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) & "\txtFiles\"
    19. If Not IO.Directory.Exists(txtPath) Then IO.Directory.CreateDirectory(txtPath)
    20. Dim streami As System.IO.FileStream
    21. For i = 1 To txtAmount
    22. 'txt Datei erstellen
    23. Dim txtFilename As String = "Textdatei " & i & ".txt"
    24. streami = System.IO.File.Create(txtPath & txtFilename)
    25. Dim cfile As System.IO.StreamWriter = New System.IO.StreamWriter(streami, System.Text.Encoding.Default)
    26. cfile.WriteLine("Textdatei " & i.ToString)
    27. cfile.Close()
    28. ' Fortschritt in Prozent ausrechnen
    29. 'Dim progress = CInt((i) * 100 / (txtAmount + 1))
    30. Dim prgs As IProgress(Of Integer) = _ProgressPercent
    31. System.Threading.Thread.Sleep(5)
    32. prgs.Report(CInt((i) * 100 / (txtAmount + 1)))
    33. Next
    34. End Sub

    und ich bekomme es nicht hin, ein abbrechen zu implementieren.
    Das Demoprojekt aus @ErfinderDesRades codeproject post bekomme ich nicht zum laufen.
    Wenn ich mir den Code, den ich im Laufe des lesens Stück für Stück verändert, bzw. erweitert habe verwende,
    dann bleibe ich an der Stelle mit dem abbrechen Token hängen, weil ich nur noch Fehler einbaue.

    Könntest du mir das abbrechen einmal auf meine Anwendung hier ummüntzen? Damit ich es nachvollziehen kann

    Edit: Sorry, hab den Upload vergessen. Der kommt morgen früh.

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „DerSmurf“ ()

    DerSmurf schrieb:

    Ist das wirklich alles?
    Es sieht so aus.

    DerSmurf schrieb:

    Dim prgs As IProgress(Of Integer) = _ProgressPercent
    Diese Zeile kannst Du vor die For-Schleife packen.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    Abbrechen geht auch einfach mit dem CancellationToken.

    docs.microsoft.com/de-de/dotne…c-task-or-a-list-of-tasks
    Beispiele gibt es einige, auch in VB.Net, hier zum Beispiel: dotnetcurry.com/ShowArticle.aspx?ID=493
    Das Beispiel ist insofern blöd, weil man die CancellationTokenSource ja doch eher nicht in der Methode, sodern in der Klasse (Form) verfürbar haben will,sonst kannst ja nicht mit einem Abbruch-Button cTokenSource.Cancel() auslösen (hier die Variable aus dem Beispiel).
    In den Asyncen Tasks fragst du dann jeweils an den geeigneten Stellen (z.B. nach jedem Durchlauf einer For-Schleife) den Token ab und reagierst entsprechend.

    VB.NET-Quellcode

    1. If ct.IsCancellationRequested Then
    2. Exit For
    3. End If

    RodFromGermany schrieb:

    Diese Zeile kannst Du vor die For-Schleife packen.

    Oh ja, danke. Die sollte ja sogar davor.

    @Dksksm ok, dann teste ich da noch ein bisschen mit rum.
    Im Codeprojekt Beispiel vom @ErfinderDesRades sieht der Code mit abbrechen aber deutlich komplexer aus, als der ohne.
    Da ich das Beispiel aber nicht zum laufen bekomme (und nicht schrittweise durchgehen kann), hab ich so meine Verständnisprobleme.
    Aber dann versuchs ichs mal anhander deiner Links zu verstehen.

    Hier hängt jetzt erstmal die Solution dran.

    Und eine Grundsätzliche Frage habe ich noch.
    Wenn ich das ganze Async mal gescheit verstanden habe, gibt es einige Stellen, wo ich es anwenden könnte.
    Z.b. wird hier auf einem recht langsamen Rechner beim starten eines Programmes (im Form Load Event) eine recht große xml ins DataSet geladen.
    Das dauert so ca. 20-30 Sekunden. In der Zeit läd sich die Form natürlich nicht. Das müsste ja mit Async und einer kleinen ToolStripProgressBar schöner werden.
    Kann ich auch dem Form Load Event das Async Schlüsselwort mitgeben, oder wäre das unsauber?
    Dateien

    DerSmurf schrieb:

    Das dauert so ca. 20-30 Sekunden
    Das klingt verdächtig. Mach Dir mal ein Testprogramm, in dem diese Datei nur geladen, aber nicht ans GUI gebunden wird, also die Daten nirgends angezeigt werden. In fast allen Fällen liegt die lange Wartezeit an einer permanenten GUI-Aktualisierung, während Daten ins tDS geladen werden.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

    DerSmurf schrieb:


    Z.b. wird hier auf einem recht langsamen Rechner beim starten eines Programmes (im Form Load Event) eine recht große xml ins DataSet geladen.


    Selbst große Datenmengen von einer halben Million Datensätze mit umfangreicher Struktur brauchen bei mir nicht soo lange. Da ist was faul und ich unterschreibe @VaporiZed's Anmerkung dazu.

    Im Form_Load Event würde ich gar nicht reinschreiben, ggf. Icon setzen für die Form aber mehr auch nicht.
    Wenn schon so ein Event, dann nimm Form_Shown aber niemals Form_Load. Das hat nämlich die unangenehme Eigenschaft Exeptions gar nicht erst zu werfen.
    Du denkst alles sei gut, weil keine Fehler geworfen werfen, aber weit gefehlt.

    DerSmurf schrieb:

    Kann ich auch dem Form Load Event das Async Schlüsselwort mitgeben, oder wäre das unsauber?
    Das Form-Load-Event wird vom System synchron aufgerufen.
    Auch wenn Du dort Deine Prozedur mit Async-Await aufrufst, würde die GUI nicht fertig aktualisiert werden, auch wenn sie nicht blockiert ist.
    Hier müsstest Du tatsächlich einen Thread anwerfen, in dem geladen wird.
    Du solltest da mal einige Experimente mit machen und dann hier Deine Ergebnisse vorstellen.
    Falls Du diesen Code kopierst, achte auf die C&P-Bremse.
    Jede einzelne Zeile Deines Programms, die Du nicht explizit getestet hast, ist falsch :!:
    Ein guter .NET-Snippetkonverter (der ist verfügbar).
    Programmierfragen über PN / Konversation werden ignoriert!
    @VaporiZed und @Dksksm
    Danke für eure Anmerkungen, aber ich denke der Code ist ok. Letztlich wird nur das Dataset aus einer XML (auf meinem NAS) gelesen und in einem gebundenen DataSet angezeigt.
    Habe das Binding entfernt, aber das brachte (gefühlt) keine Veränderung.
    Allerdings läuft das Programm sonst auf einem Surface (welches gerade in der "DisplayIstNachBodensturzKaputt" Werkstatt ist).
    Hier ist die Startzeit vollkommen ok. Also 1 bis 2 Sekunden - nicht störend lange.
    Nun habe ich Ersatzweise einen alten Laptop (wirklich alt alt) hingestellt, den ich noch Zuhause hatte.
    Da hier auch das starten einer Anwendung (Browser z.b.) Ewigkeiten dauert, schiebe ichs in dem Fall auf den PC und nicht auf meine Programmierkünste :)

    Zum Await habe ich ja jetzt erstmal genug zum lesen und testen.
    @RodFromGermany wenn ich Hilfe brauche, oder sobald ich gigantomanische Erkenntnisse habe, gebe ich hier laut.

    @Dksksm zum Form Load und Shown, werd ich dann auch nochmal googlen.
    Denn bisher bin ich scheinbar ein Freund des Load Events. Das nehm ich immer, Shown gibt's in meiner Welt nicht.
    Also packe ich da dann demnächst Mal meinen Background Worker rein und verabschiede mich von diesem Duett