none
GUI friert ein RRS feed

  • Frage

  • Hallo,

    ich habe ein kleines WinForms Programm geschrieben, basierend auf einer DLL eines Bekannten. Das Programm lädt bestimmte Daten von einer Webseite herunter. Problem ist hierbei, dass mein GUI einfriert, wenn die Daten größer sind. Nachfolgend mein Code:

        Private Sub ButtonDownload_Click(sender As Object, e As EventArgs) Handles ButtonDownload.Click
            Me.ProgressBarStatus.Value = 0
            Try
                If (data IsNot Nothing) Then
                    Dim dataFolder As String = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
                    Dim dataFile As String = Path.Combine(dataFolder, Me.TextBoxName.Text & data.Extension)
                    Me.LabelDataFolder.Text = dataFile
                    Dim dataDownloader As New DataDownloader(data, dataFile)
                    AddHandler dataDownloader.DownloadProgressChanged, AddressOf dataDownloader_ProgressChanged
                    dataDownloader.Execute()
                End If
            Catch wex As System.Net.WebException
                MessageBox.Show(wex.Message, My.Application.Info.Title, MessageBoxButtons.OK, MessageBoxIcon.Error)
            Catch ioex As System.IO.IOException
                MessageBox.Show(ioex.Message, My.Application.Info.Title, MessageBoxButtons.OK, MessageBoxIcon.Error)
            End Try
        End Sub
    

    Der Grund hierfür ist die Execute-Methode. Also habe ich die Ausführung in einen anderen Thread verschoben. Mein Code sieht jetzt folgendermassen aus:

        Private Async Sub ButtonDownload_Click(sender As Object, e As EventArgs) Handles ButtonDownload.Click
            Me.ProgressBarStatus.Value = 0
            Try
                If (data IsNot Nothing) Then
                    Dim dataFolder As String = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
                    Dim dataFile As String = Path.Combine(dataFolder, Me.TextBoxName.Text & data.Extension)
                    Me.LabelDataFolder.Text = dataFile
                    Dim dataDownloader As New DataDownloader(data, dataFile)
                    AddHandler dataDownloader.DownloadProgressChanged, AddressOf dataDownloader_ProgressChanged
                    Await Task.Run(Sub() dataDownloader.Execute())
                End If
            Catch wex As System.Net.WebException
                MessageBox.Show(wex.Message, My.Application.Info.Title, MessageBoxButtons.OK, MessageBoxIcon.Error)
            Catch ioex As System.IO.IOException
                MessageBox.Show(ioex.Message, My.Application.Info.Title, MessageBoxButtons.OK, MessageBoxIcon.Error)
            End Try
        End Sub
    

    Ein Problem gelöst, ein anderes verursacht. Das ProgressChanged Ereignis läuft auf dem GUI-Thread während die Execute-Methode auf einem anderen Thread läuft. Das führt bekanntlich zu Konflikten. Ich weiss allerdings nicht, wie ich dieses Problem lösen kann. Habt ihr vielleicht einen eleganten Vorschlag?

    Gruss,

    LBB

    Donnerstag, 25. Februar 2016 21:38

Antworten

  • Hallo Tom,

    danke für die Hinweise. Ich habe bei dem Bekannten nochmal nachgehakt und es handelt sich um ein normales Event, d.h. der Eventhandler läuft im gleichen Thread wie die Execute-Methode. Ich habe die ProgressChanged-Methode angepasst und sie sieht nun wie folgt aus:

    Delegate Sub SetProgressDelegate(sender As Object, args As ProgressEventArgs)    
    
    Private Sub dataDownloader_ProgressChanged(sender As Object, args As ProgressEventArgs)
            If Me.ProgressBarStatus.InvokeRequired Then
                Dim deleg As New SetProgressDelegate(AddressOf dataDownloader_ProgressChanged)
                Me.Invoke(deleg, New Object() {sender, args})
            Else
                Me.ProgressBarStatus.Value = CInt(args.ProgressPercentage)
            End If
    End Sub
    

    Ist das korrekt? Ich habe mir zumindest für meinen Code folgendes MSDN-Beispiel angeschaut. Jetzt ist noch die Frage offen, wie es mit den Exceptions aussieht, wenn die Execute-Methode auf einem anderen Thread läuft. Da dürfte es auch Konflikte geben, oder?

    Gruss,

    LBB

    Samstag, 27. Februar 2016 15:42
  • Hallo,

    das Invoke brauchst du immer dann nicht wenn alles im selben Thread ablaufen kann. Die MessageBoxen dürften da kein Problem machen, nur wenn du auf die Fenster-UI deines Programms zugreifen willst dürfte es wieder erforderlich werden.

    Zu deinen Fragen:

    .NET hat 5 Arten von Typen: Klassen, Strukturen, Enumerationen, Schnittstellen und Delegaten
    Delegaten sind das für Funktionen, was Schnittstellen für Klassen sind - "Baupläne". Du kannst sie wie ein Objekt mit New instanziieren, da es Objekte sind die dabei entstehen. Auch wenn der Compiler es mittlerweile auch ohne New auf die Reihe bekommt zu erkennen was hinter der Zuweisung steckt.

    Wie gesagt ist ein Delegat der "Typ" einer Methode. Einer Variablen kannst du damit die Methode selbst per AddressOf mitgeben oder aber eine anonyme Lambda-Funktion wärend der Zuweisung erstellen. In beiden Fällen kannst du danach den Delegaten aufrufen, wobei die dahinter stehende Methode ausgeführt wird.


    Tom Lambert - .NET (C#) MVP
    Wozu Antworten markieren und für Beiträge abstimmen? Klicke hier.
    Nützliche Links: .NET Quellcode | C# ↔ VB.NET Konverter | Account bestätigen (Verify Your Account)
    Ich: Webseite | Code Beispiele | Facebook | Twitter | Snippets

    Sonntag, 28. Februar 2016 12:17

Alle Antworten

  • Hallo,

    dass Threadübergreifende Zugriffe Probleme machen können stimmt. Aber um diese beheben zu können muss man auch das genaue Problem kennen.

    Die Frage ist vermutlich wie DataDownloader.DownloadProgressChanged arbeitet. Wenn es ein normales Event ist und entsprechend aufgerufen wird, so läuft der Eventhandler im Thread den du für die Execute-Methode angelegt hast. In dem Fall brauchst du im Eventhandler noch die Invoke-Methode um anschließend auf den UI-Thread zu gelangen und diese zu aktualisieren. Es besteht aber auch die Möglichkeit, was ich anhand deiner Beschreibung vermute, dass der Eventhandler direkt auf den UI-Thread (per Invoke) "gedispatcht" wird. In dem Fall ist die Frage wie das genau geschieht und ob dort dann Probleme auftreten können.


    Tom Lambert - .NET (C#) MVP
    Wozu Antworten markieren und für Beiträge abstimmen? Klicke hier.
    Nützliche Links: .NET Quellcode | C# ↔ VB.NET Konverter | Account bestätigen (Verify Your Account)
    Ich: Webseite | Code Beispiele | Facebook | Twitter | Snippets

    Donnerstag, 25. Februar 2016 22:47
  • Hallo Tom,

    danke für die Hinweise. Ich habe bei dem Bekannten nochmal nachgehakt und es handelt sich um ein normales Event, d.h. der Eventhandler läuft im gleichen Thread wie die Execute-Methode. Ich habe die ProgressChanged-Methode angepasst und sie sieht nun wie folgt aus:

    Delegate Sub SetProgressDelegate(sender As Object, args As ProgressEventArgs)    
    
    Private Sub dataDownloader_ProgressChanged(sender As Object, args As ProgressEventArgs)
            If Me.ProgressBarStatus.InvokeRequired Then
                Dim deleg As New SetProgressDelegate(AddressOf dataDownloader_ProgressChanged)
                Me.Invoke(deleg, New Object() {sender, args})
            Else
                Me.ProgressBarStatus.Value = CInt(args.ProgressPercentage)
            End If
    End Sub
    

    Ist das korrekt? Ich habe mir zumindest für meinen Code folgendes MSDN-Beispiel angeschaut. Jetzt ist noch die Frage offen, wie es mit den Exceptions aussieht, wenn die Execute-Methode auf einem anderen Thread läuft. Da dürfte es auch Konflikte geben, oder?

    Gruss,

    LBB

    Samstag, 27. Februar 2016 15:42
  • Hallo,

    so wie du es gezeigt hast kann man das machen. Man könnte auch einen generischen Action-Delegat anstatt dem eigenen Typ nutzen oder einen Lambda-Ausdruck, aber das macht technisch keinen großen Unterschied.

    Bei Exceptions ist es so dass diese normalerweise nicht aus anderen Threads heraus abgefangen werden können. Diese kannst du ggf. im Delegaten von Task.Run abfangen. Dann könntest du über Invoke eine Nachricht auf der UI ausgeben. Der Thread selbst würde dann direkt danach abgebrochen werden sofern nichts weiteres in Task.Run kommt.


    Tom Lambert - .NET (C#) MVP
    Wozu Antworten markieren und für Beiträge abstimmen? Klicke hier.
    Nützliche Links: .NET Quellcode | C# ↔ VB.NET Konverter | Account bestätigen (Verify Your Account)
    Ich: Webseite | Code Beispiele | Facebook | Twitter | Snippets

    Samstag, 27. Februar 2016 16:17
  • Hi Tom,

    besten Dank für die Antworten und die Tipps.

    Zu den Exceptions habe ich ein MSDN-Beispiel gefunden. Hier wird AggregateException verwendet, um die Exception zu Behandeln. Mein Code sieht wie folgt aus:

            Try
                If (data IsNot Nothing) Then
                    ' ...
                    Await Task.Run(Sub() dataDownloader.Execute())
                End If
            Catch ae As AggregateException
                ae.Handle(Function(ex)
                              If TypeOf (ex) Is System.Net.WebException Then
                                  MessageBox.Show(ex.InnerException.Message, My.Application.Info.Title, MessageBoxButtons.OK, MessageBoxIcon.Error)
                                  Return True
                              ElseIf TypeOf (ex) Is System.IO.IOException Then
                                  MessageBox.Show(ex.InnerException.Message, My.Application.Info.Title, MessageBoxButtons.OK, MessageBoxIcon.Error)
                                  Return True
                              Else
                                  Return False
                              End If
                          End Function)
            End Try
    

    Offenbar benötigt man in diesem Fall kein Invoke, um die Nachricht auf der UI auszugeben.

    Zu den Delegaten hätte ich noch eine Frage. Mein Code sieht ja so aus:

    Delegate Sub SetProgressDelegate(sender As Object, args As ProgressEventArgs)    
    ' ...
    Dim deleg As New SetProgressDelegate(AddressOf dataDownloader_ProgressChanged)

    Wenn ich mir den Action-Delegat anschaue, dann könnte ich meinen Code so umschreiben:

    Dim deleg As SetProgressDelegate = AddressOf dataDownloader_ProgressChanged

    oder unter Verwendung des Action-Delegaten:

    Dim deleg As Action(Of Object, ProgressEventArgs) = AddressOf dataDownloader_ProgressChanged

    Die Schreibweise der letzten beiden Code-Zeilen könnte man auch so formulieren: definiere eine Variable vom Typ SetProgressDelegate bzw. Action(Of T1, T2) und weise ihr die Addresse der Methode dataDownloader_ProgressChanged zu.

    Die Schreibweise der ersten Codezeile leuchtet mit nicht ein.

    1. Wieso muss ich hier New verwenden, schließlich ist es nur eine Methode und nicht eine Klasse?

    2. Wieso muss ich die Addresse der Methode dataDownloader_ProgressChanged dem Delegaten als Argument übergeben?

    Gruss,

    LBB

    Sonntag, 28. Februar 2016 11:34
  • Hallo,

    das Invoke brauchst du immer dann nicht wenn alles im selben Thread ablaufen kann. Die MessageBoxen dürften da kein Problem machen, nur wenn du auf die Fenster-UI deines Programms zugreifen willst dürfte es wieder erforderlich werden.

    Zu deinen Fragen:

    .NET hat 5 Arten von Typen: Klassen, Strukturen, Enumerationen, Schnittstellen und Delegaten
    Delegaten sind das für Funktionen, was Schnittstellen für Klassen sind - "Baupläne". Du kannst sie wie ein Objekt mit New instanziieren, da es Objekte sind die dabei entstehen. Auch wenn der Compiler es mittlerweile auch ohne New auf die Reihe bekommt zu erkennen was hinter der Zuweisung steckt.

    Wie gesagt ist ein Delegat der "Typ" einer Methode. Einer Variablen kannst du damit die Methode selbst per AddressOf mitgeben oder aber eine anonyme Lambda-Funktion wärend der Zuweisung erstellen. In beiden Fällen kannst du danach den Delegaten aufrufen, wobei die dahinter stehende Methode ausgeführt wird.


    Tom Lambert - .NET (C#) MVP
    Wozu Antworten markieren und für Beiträge abstimmen? Klicke hier.
    Nützliche Links: .NET Quellcode | C# ↔ VB.NET Konverter | Account bestätigen (Verify Your Account)
    Ich: Webseite | Code Beispiele | Facebook | Twitter | Snippets

    Sonntag, 28. Februar 2016 12:17
  • Hallo Tom,

    besten Dank für die Hilfe und die Aufklärung. Hat mir sehr geholfen.

    Gruss,

    LBB

    Sonntag, 28. Februar 2016 19:29
  • Hallo Tom,

    meine Exception-Behandlung funktioniert nicht. Obwohl ich auf AggregateException überprüfe, erhalte ich folgende Fehlermeldung:

    System.Net.WebException wurde nicht behandelt.

    Hast Du eine Idee, woran es liegen könnte? Wieso funktioniert es im MSDN-Beispiel, bei mir jedoch nicht. Schließlich ist Task.Run nur ein Wrapper für Task.Factory.StartNew.

    Gruss,

    LBB

    Sonntag, 28. Februar 2016 21:24