Odpovědět Programm verhält sich merkwürdig

  • 17. února 2012 18:31
     
      Obsahuje kód

    Hallo,

    ich habe ein simples Testprogramm bestehend aus einer ListBox, einer TextBox und einem Button. Nach dem Starten des Programmes fülle ich beim Laden des Formulars die ListBox mit 20 Strings. Ich wähle anschließend einige Zeilen aus, sagen wir mal 15, und clicke auf den Button. Das Click-Event wird abgearbeitet und die ausgewählten Zeilen werden in die TextBox kopiert. Während die For-Schleife abgearbeitet wird, wähle ich aus der ListBox nur eine Zeile aus. Die For-Schleife bricht die Ausführung ab und wird nicht zu Ende geführt. Der Quellcode ist nachfolgend aufgelistet:

    Imports System.Threading
     
    Public Class Form1
     
        Private Sub Form1_Load(sender As System.Object, e As System.EventArgsHandles MyBase.Load
            For i As Integer = 0 To 20
                Me.ListBox1.Items.Add(String.Format("Test{0}", i))
            Next
            Me.ListBox1.SelectionMode = SelectionMode.MultiExtended
        End Sub
     
        Private Sub Button1_Click(sender As System.Object, e As System.EventArgsHandles Button1.Click
            Dim selectedIndices As ListBox.SelectedIndexCollection = Me.ListBox1.SelectedIndices
            For Each index As Integer In selectedIndices
                Me.TextBox1.AppendText(Me.ListBox1.Items(index).ToString())
                Thread.Sleep(500)
                Application.DoEvents()
                Me.TextBox1.AppendText(" OK" & Environment.NewLine)
                Thread.Sleep(500)
            Next
        End Sub
     
    End Class
    

    Was läuft denn da schief? Hat jemand eine Idee?

    Gruss, LittleBlueBird

Všechny reakce

  • 18. února 2012 9:43
    Přispěvatel
     
      Obsahuje kód

    Hallo,

    wenn ich den Code hier laufen lasse, funktioniert er erwartungsgemäß.

    Da Du dort (testhalber) intensiv mit Pausen arbeitest, vermute ich mal,
    Dir "funkt" ein Timer oder ähnliches Konstrukt dazwischen und verändert die Auswahl.
    Und gegen manuelle Eingriffe ist das natürlich nie nicht abgesichert.

    Ein Application.DoEvents könnte z. B. Windows Timer aktivieren.
    Beim Thread.Sleep könnte im Extremfall zwischenzeitlich ene andere Anwendung die Kontrolle übernehmen.

    Ich würde eh den Code kompakter gestalten und auf mehrere AppendText verzichten, z. B.:

    Private Sub AppendSelection()
       Dim builder = New StringBuilder()
       For Each item In Me.listBox1.SelectedItems
    	builder.Append(item)
            builder.AppendLine(" OK")
       Next
       Me.textBox1.AppendText(builder.ToString())
    End Sub

    so kämen weniger Verzweigungen in die MessageLoop zustande - wobei es mehr eine Symptombekämpfung wäre.
    Wenn andere Komponenten eingreifen, sollte man danach suchen und es beheben.

    Gruß Elmar

  • 18. února 2012 13:30
     
      Obsahuje kód

    Hallo Elmar,

    vielen Dank für die Antwort und die Zeit, die Du Dir für das Testen genommen hast. Seltsam, dass es bei mir nicht richtig läuft. Das Testporgramm habe ich erstellt, weil mein Originalprogramm diesen Fehler hatte. Anstelle der Thread.Sleep Aufrufe werden in meinem Originalprogramm rechenintensive Methoden ausgeführt. Damit der Benutzer nicht denkt, das Programm sei hängen geblieben, habe ich erstens die Textmeldungen eingefügt, die den Fortschritt der Operationen an den Benutzer melden, und zweitens Application.DoEvents aufgerufen, damit das GUI ansprechbar bleibt. Auf die einzelnen AppendText kann ich daher nicht verzichten.

    Nach Deinen Ausführungen zu Application.DoEvents habe ich mich darüber mal schlau gemacht. Microsoft schreibt dazu:

    Vorsicht

    Das Aufrufen dieser Methode bewirkt, dass der aktuelle Thread angehalten wird, während alle wartenden Fenstermeldungen verarbeitet werden. Wenn eine Meldung bewirkt, dass ein Ereignis ausgelöst wird, dann werden andere Bereiche des Anwendungscodes möglicherweise ausgeführt. Dies kann bewirken, dass die Anwendung unerwartete Verhaltensweisen zeigt, die schwierig zu debuggen sind. Wenn Sie Vorgänge oder Berechnungen ausführen, die eine lange Zeit brauchen, ist es oft vorzuziehen, jene Vorgänge auf einem neuen Thread auszuführen. Asynchronous Programming Overview." id="mt17">Weitere Informationen über asynchrone Programmierung finden Sie unter Übersicht über die asynchrone Programmierung.

    Ich habe mich daher entschlossen, das Programm zu ändern und die Operationen auf einem anderen Thread auszuführen. Ich habe dafür den BackgroundWorker gewählt. Der neue Code ist hier zu sehen:

    Imports System.Threading
    Imports System.ComponentModel
     
    Public Class Form1
     
        Private WithEvents BackgroundWorker1 As BackgroundWorker
     
        Private Sub Form1_Load(sender As System.Object, e As System.EventArgsHandles MyBase.Load
            For i As Integer = 0 To 20
                Me.ListBox1.Items.Add(String.Format("Test{0}", i))
            Next
            Me.ListBox1.SelectionMode = SelectionMode.MultiExtended
            Me.BackgroundWorker1 = New BackgroundWorker
        End Sub
     
        Private Sub Button1_Click(sender As System.Object, e As System.EventArgsHandles Button1.Click
            If (Not Me.BackgroundWorker1.IsBusy) Then
                Me.BackgroundWorker1.RunWorkerAsync(Me.ListBox1)
            End If
        End Sub
     
        Private Sub BackgroundWorker1_DoEvent(ByVal sender As Object, e As DoWorkEventArgsHandles BackgroundWorker1.DoWork
            Dim lstBox As ListBox = CType(e.Argument, ListBox)
            Dim selectedIndices As ListBox.SelectedIndexCollection = lstBox.SelectedIndices
            For Each index As Integer In selectedIndices
                UpdateTextBox(CStr(lstBox.Items(index)))
                Thread.Sleep(500)
                UpdateTextBox("OK")
                Thread.Sleep(500)
            Next
        End Sub
     
        Private Delegate Sub UpdateTextBoxDelegate(ByVal text As String)
     
        Private Sub UpdateTextBox(ByVal text As String)
            If Me.TextBox1.InvokeRequired Then
                Me.Invoke(New UpdateTextBoxDelegate(AddressOf UpdateTextBox), New Object() {text})
            Else
                Me.TextBox1.AppendText(text & Environment.NewLine)
                Me.TextBox1.Update()
                Me.TextBox1.ScrollToCaret()
            End If
        End Sub
     
    End Class
    

    Wenn ich das Programm allerdings ausführe, erhalte ich folgende Fehlermeldung:

    InvalidOperationException was unhandled by user code.

    Cross-thread operation not valid: Control 'ListBox1' accessed from a thread other than the thread it was created on.

    Asynchronous Programming Overview." id="mt17">

    Wieso handelt es sich hierbei um eine Cross-Thread Operation? Ich habe ja nicht direkt auf Me.ListBox1 zugegriffen sondern die ListBox als Argument an die Methode RunWorkerAsync übergeben und in der DoWork Methode auf das übergebene Argument nach dem Casten zugegriffen. Zumindest habe ich das in einem Tutorial im Internet so gesehen. Was ist hier falsch?

    Gruss, LittleBlueBird

  • 18. února 2012 17:19
    Přispěvatel
     
     Odpovědět

    Hallo,

    grundsätzlich gilt für eine Auswahl im Steuerelementen, dass sie nur so lange gültig ist,
    wie der Anwender sich mit seinen Fingern (oder Mäuschen) daran zu schaffen macht

    Ob das nun eine TextBox- oder Listbox-Auswahl ist spielt keine Rolle.
    Das gilt immer wenn die Windows-Meldungsschleife bedient wird
    und dabei reicht bereits ein Application.DoEvents.

    Deswegen sollte man in den Fällen  bei denen der Anwender sich sich an Steuerelementen
    zu schaffen machen soll, diese über die Enabled-Eigenschaft deaktivieren.
    Sind es mehrere Steuerlemente, so bietet sich an, diese in ein Panel zu packen,
    und das zu deaktivieren, oder gleich das ganze Formular.
    Denn die Enabled-Eigenschaft für Container blockiert Eingaben für alle enthaltenene Steuelemente.

    Alternativ heisst es die Daten in eigene Variablen zu sichern.
    Für die Listbox-Auswahl z. B. in eine List(Of String).

    Das Aktualisieren aus einem anderen Thread ist eine andere Geschichte.
    Wenn Du einen BackgroundWorker verwendest, solltest Du das Aktualisieren
    über das ReportProgress-Ereignis erledigen, siehe Beispiel zur Klasse.

    Wobei damit - wie auch bei Control.Invoke - wieder die Meldungsschleife bedient wird,
    das obige ebenso zutrifft.

    Was die Ausnahme angeht:
    Auch der lesenden Zugriff - mehrfach auf die listbox (SelectedIndices, Items) - ist nicht zulässig,
    nicht nur das Verändern von Eigenschaften.
    Auch hier ist obiges Vorgehen zu empfehlen: Die Daten bereits vor dem Starten auslesen
    und als List(Of String) an den BackgroundWorker (als Argument für DoWork) übergeben.

    Gruß Elmar

  • 18. února 2012 18:26
     
     

    Hallo Elmar,

    ich danke Dir sehr, dass Du mir hilfst. Es ist recht frustrierend, wenn man nicht weiter kommt und Goggle auch keine Unterstützung bietet (liegt vielleicht auch an mir, wenn ich die falschen Stichworte eingebe ;-)). Da hilft es schon, wenn man mit gleichgesinnten darüber diskutieren kann und wertvolle Hinweise erhält. Man bekommt dadurch nicht nur das Problem gelöst, sondern lernt auch einiges dazu.

    Ich habe die ListBox nicht deaktiviert, weil ich nicht damit gerechnet habe, dass die For Each-Schleife abgebrochen wird, wenn ich abermalls auf die ListBox clicke. Schließlich habe ich die Indizes der ausgewählten Zeilen in die Variable selectedIndices gespeichert und für die Schleife verwendet. Damit dürfte die For Each-Schleife nicht unterbrochen werden, wenn ich zwischendurch andere Zeilen aus der ListBox auswähle, zumal ich nicht erneut auf Button1 geclickt habe. Offensichtlich führt aber Application.DoEvents dazu, dass die Windows-Meldungsschleife neu bedient und das Click-Ereignis des Button1 neu ausgelöst wird. Eine andere Erklärung habe ich nicht.

    Ich wusste übrigens nicht, dass die Controls deaktiviert werden, wenn man den Container deaktiviert. Wieder was gelernt.

    Die Methode ReportProgress des BackgroundWorkers akzeptiert aber nur Integerwerte und im MSDN-Beispiel wird auch nur ein Integerwert verwendet. Wie soll ich meine Textmeldung dann an die TextBox schicken? Deswegen habe ich eine Delegate-Methode verwendet.

    Ich dachte, dass die Argumentübergabe über die Methode RunWorkerAsync Thread-Sicher ist. Du hast mir aber mit dem Stichwort "Mehrfachzugriff" die Augen geöffnet. In den vorhin erwähnten Beispiel aus dem Internet wird nur ein Wert aus der Liste übergeben, der dann im DoEvent verwendet wird. Ein Zugriff auf die ListBox aus DoWork erfolgt nicht.

    Ich danke Dir nochmal für die Unterstützung. Jetzt kann ich mich daran setzen und den Code umgestalten.

    Gruss, LittleBLueBird


  • 19. února 2012 9:04
    Přispěvatel
     
     Odpovědět

    Hallo,

    ReportProgress hat ein zweites Argument (vom Typ Object), wo Du alles mögliche übergeben kannst.
    Ein Beispiel hatte ich mal gegebenSchleife pausieren, wegen CPU ausgelastet

    Gruß Elmar

  • 19. února 2012 10:36
     
     

    Hallo Elmar,

    super! Mein Testprogramm läuft jetzt mit dem BackgroundWorker und eine Delegate-Methode benötige ich auch nicht mehr. Jetzt muss ich das nur noch in meinem Originalprogramm umsetzen.

    Vielen, vielen Dank für die Hinweise und die Unterstützung. Ich wünsche Dir noch einen schönen Sonntag.

    Gruss, LittleBlueBird