Exception bei Aufruf von Steuerelement trotz Threadsicherer Abhandlung

  • VB.NET
  • .NET (FX) 4.5–4.8

Es gibt 19 Antworten in diesem Thema. Der letzte Beitrag () ist von RodFromGermany.

    Exception bei Aufruf von Steuerelement trotz Threadsicherer Abhandlung

    Hallo Zusammen

    Ich bin gerade etwas ratlos bezüglich eines Multi-Threading Problems. Ich starte meinen Arbeitsprozess in einem separatem Thread, wobei ich von diesem auf Form-Steuerelemente meines initialen Threads zugreifen will. Habe mich darüber schlau gemacht und eine entsprechende Lösung mittels InvokeRequired usw. entwickelt.

    Mein Code sieht wie folgt aus:


    Thread erstellen und starten

    Quellcode

    1. Public Class Start
    2. Private WithEvents backgroundWorker1 As BackgroundWorker
    3. Private Sub formLoad(sender As Object, e As EventArgs) Handles Me.Load
    4. End Sub
    5. Private Sub btnStartFromBeginning_Click(sender As Object, e As EventArgs) Handles btnStartFromBeginning.Click
    6. Dim migrationThread As System.Threading.Thread
    7. migrationThread = New System.Threading.Thread(AddressOf MigrationController.startMigration)
    8. Debug.Print("Starting Migration...")
    9. migrationThread.Start() 'Vorgang starten
    10. End Sub
    11. End Class


    Durch Thread aufgerufener Sub

    Quellcode

    1. Module MigrationController
    2. Sub startMigration()
    3. '## CONNECT SERVICES
    4. CreateConnection.connectServices()
    5. End Sub
    6. End Module


    Module ThreadInvokeHelper wird aufgerufen, um den Label Text sicher zu verändern

    Quellcode

    1. Module CreateConnection
    2. Sub connectServices()
    3. '### Connect SmarTeam
    4. connectSmarTeam()
    5. End Sub
    6. Private Sub connectSmarTeam()
    7. Dim sSmarTeamState As String = Functions.connectToSmarTeam()
    8. If sSmarTeamState.Contains("Error") Then
    9. ThreadInvokeHelper.setText(Start, Start.lblStateSmarTeam, "† SmarTeam: " & sSmarTeamState)
    10. Else
    11. ThreadInvokeHelper.setText(Start, Start.lblStateSmarTeam, "✓ SmarTeam: " & sSmarTeamState)
    12. End If
    13. End Sub
    14. End Module


    Das Modul, welches mittels übergebener Form und Control Element den Text im Label anpassen soll

    Quellcode

    1. Module ThreadInvokeHelper
    2. Delegate Sub setTextCallback(ByVal form As Form, ByVal ctrl As Control, ByVal sText As String)
    3. Public Sub setText(ByVal form As Form, ByVal ctrl As Control, ByVal sText As String)
    4. If ctrl.InvokeRequired Then
    5. 'If Invoke Requred --> Make Callback
    6. Dim callback As New setTextCallback(AddressOf setText)
    7. form.Invoke(callback, New Object() {form, ctrl, sText})
    8. Else
    9. 'Set Text directly
    10. ctrl.Text = sText
    11. End If
    12. End Sub
    13. End Module


    Mein Problem ist nun, dass ich trotz (aus meiner Sicht) Threadsicherer Abhandlung nach Microsoft Seite (Link) den folgenden Fehler bekomme:

    An unhandled exception of type 'System.InvalidOperationException' occurred in AstrolabMigration.exe

    Additional information: Fehler beim Erstellen des Formulars. Weitere Informationen finden Sie in Exception.InnerException. Fehler: Ungültiger threadübergreifender Vorgang: Der Zugriff auf das Steuerelement erfolgte von einem anderen Thread als dem Thread, für den es erstellt wurde..


    Hat jemand eine Idee an was das liegen könnte? ?(

    Vielen Dank für eure Hilfe.

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „vb123“ ()

    ThreadInvokeHelper.setText(Start, Start.lblStateSmarTeam, "† SmarTeam: " & sSmarTeamState)
    Wie ist Start deklariert und definiert? Was Start darstellen soll, ist mir klar, aber wie kommt die Variable zu ihrem Wert?
    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.
    Hallo VaporiZed

    Das hätte ich noch sagen sollen: Start ist meine Form (auf der sich die Steuerelemente befinden, welche ich ändern will), und die Funktion setText() kommt durch den Aufruf im Sub connectSmarTeam() zum Wert für die Variable form. (Und die Form kann man ja logischerweise wie eine Klasse von überall aufrufen)

    vb123 schrieb:

    Start ist meine Form (auf der sich die Steuerelemente befinden, welche ich ändern will)

    VaporiZed schrieb:

    Was Start darstellen soll, ist mir klar


    vb123 schrieb:

    die Funktion setText() kommt durch den Aufruf im Sub connectSmarTeam() zum Wert für die Variable form.

    Das ist so nicht vollständig, da Du die Sub connectSmarTeam() abgebildet hast (Post#1, Block#3, ab Zeile#11). Da wird zwar Start verwendet, aber es gibt nirgends ein Start = . Oder meintest Du connectToSmarTeam() (Block#3, Zeile#13? Hoffentlich nicht. Es ist eben die Frage, wie jene Zeile in Deinem Code aussieht, die genau diesen Teil enthält: Start = , also die Wertzuweisung an Start. Denn daran wird es m.E. hapern/scheitern.
    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.
    @vb123 Probier mal dies:

    VB.NET-Quellcode

    1. Module ThreadInvokeHelper
    2. Public Sub setText(ByVal ctrl As Control, ByVal sText As String)
    3. If ctrl.InvokeRequired Then
    4. ' ruft sich selbst auf!
    5. Dim act = New Action(Of Control, String)(AddressOf setText)
    6. ctrl.Invoke(act, ctrl, sText)
    7. Return
    8. End If
    9. 'Set Text directly
    10. ctrl.Text = sText
    11. End Sub
    12. End Module

    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 schrieb:

    Das ist so nicht vollständig, da Du die Sub connectSmarTeam() abgebildet hast (Post#1, Block#3, ab Zeile#11). Da wird zwar Start verwendet, aber es gibt nirgends ein Start = .


    Ich habe nun in Block #1 und #2 die Module bzw. Klasse ergänzt, damit dieser Aufbau klarer wird. Wie du siehst ist Start eine Klasse, nämlich die Startform meiner Windows Forms Application.

    RodFromGermany schrieb:

    @vb123 Probier mal dies:


    Habe ich getestet, resultiert leider im gleichen Fehler.

    Was ich im Debugger gesehen habe: Die Abfrage InvokeRequired gibt mir immer False zurück, ein Invoke soll also anscheinend nie benögigt werden (was natürlich nicht stimmen kann). Wenn ich die If-Abfrage in meinem ThreadInvokHelper Module entfernen und immer "Invoke", bekomme ich den Fehler auch.
    Da freut sich RfG, denn da geht's los: Start ist eine Klasse, also eine Art Bauplan. Daher darfst Du später nicht coden: ThreadInvokeHelper.setText(Start, Start.lblStateSmarTeam, "† SmarTeam: " & sSmarTeamState), da eben Start nur der Name einer Klasse ist, aber nicht einer Klasseninstanz. Es geht leider schon, da Mikrosaft wohl aus Kompatibilitätsgründen sowas in VB.Net noch zulässt, aber!: Das Verwenden des Form-Namens in solch einer Zeile ist nur deshalb möglich, da im Hintergrund eine Property mit jenem Klassennamen erstellt wird, was Dir IntelliSense durch draufzeigen auf jenen Abschnitt bestätigt:

    Das Ganze ist aber tricky: Ich nenne das »thread-dependent property«, denn z.B. das Form-Handle ist abhängig davon, in welchem Thread Du Dich befindest, was dazu führt, dass Du im Mainthread ein anderes Form über Start erreichst als im Nebenthread, siehe Anhang des gleichen Durchlaufs. Damit ist mir m.E. auch erstmal klar, warum InvokeRequired = False ist. Es wird ja diesbezüglich nicht außerhalb des Threads was geändert.
    Bilder
    • Handle im MainThread.png

      11,16 kB, 599×119, 198 mal angesehen
    • Handle im Neben-Thread.png

      17,73 kB, 612×308, 174 mal angesehen
    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.

    vb123 schrieb:

    Habe ich getestet, resultiert leider im gleichen Fehler.
    Kannst Du mal ein äquivalentes Test-Projekt posten, das diesen Fehler reproduziert?
    @VaporiZed Deswegen hab ich den Parameter Form ersatzlos gestrichen. :D
    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 da Form von Control erbt kannst du auch Form übergeben.
    Das Problem ist vermutlich genau das was @VaporiZed beschreibt.
    Die VB.NET Default Forminstanz wird für jeden Thread erzeugt. Man muss wenn dann die explizite Instanz übergeben (bzw am besten eigentlich das konkrete Control).

    LG
    Das ist meine Signatur und sie wird wunderbar sein!

    RodFromGermany schrieb:

    Kannst Du mal ein äquivalentes Test-Projekt posten, das diesen Fehler reproduziert?


    Habe es versucht in einem anderen Projekt zu rekonstruieren:
    - In meinem Hauptprojekt bekomme ich den Fehler, wenn ich das Programm ohne Haltepunkt laufen lasse. Gehe ich jedoch langsam im Einzelschritt durch kommt zwar kein Fehler (wieso auch immer? ?( ), aber die Änderung des Label-Textes hat keinen Effekt.
    - Im neuen Testprojekt läuft es mit deinem Code immer ohne Fehler durch (auch ohne Haltepunkte), die Änderung hat jedoch auch hier keinen Effekt auf meine Form (Frage ich nach der Änderung per If-Abfrage den Text des Labels ab, so entspricht dieser dem neuen, also eigentlich dem korrekten)

    VaporiZed schrieb:

    Das Ganze ist aber tricky: Ich nenne das »thread-dependent property«, denn z.B. das Form-Handle ist abhängig davon, in welchem Thread Du Dich befindest, was dazu führt, dass Du im Mainthread ein anderes Form über Start erreichst als im Nebenthread, siehe Anhang des gleichen Durchlaufs. Damit ist mir m.E. auch erstmal klar, warum InvokeRequired = False ist. Es wird ja diesbezüglich nicht außerhalb des Threads was geändert.


    Wusste ich nicht, aber macht Sinn. Danke :)

    Mono schrieb:

    Man muss wenn dann die explizite Instanz übergeben (bzw am besten eigentlich das konkrete Control).


    Das habe ich nach dem Input von @VaporiZed ausprobiert. Hat das nun aber nicht den gleichen Effekt wie direkt die Form zu übergeben?

    Quellcode

    1. Delegate Sub setTextCallback(ByVal ctrl As Control, ByVal sText As String)
    2. Public Sub setText(ByVal ctrl As Control, ByVal sText As String)
    3. If ctrl.InvokeRequired Then
    4. ' Invoke Requred --> Make Callback
    5. Dim callback As New setTextCallback(AddressOf setText)
    6. ctrl.FindForm.Invoke(callback, New Object() {ctrl, sText})
    7. Else
    8. 'Set Text directly
    9. ctrl.Text = sText
    10. End If
    11. End Sub

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

    Und Start ist das Problem genau wie @VaporiZed beschrieben hat.

    Dein Konstrukt mit Modul und Thread ist sowieso suboptimal.
    Welches Net Framework verwendest du?

    Und als Anmerkung. Du brauchst nicht meinen Post direkt über deinem nochmal vollständig zitieren. Es ist sowieso klar worum es geht ;)
    LG
    Das ist meine Signatur und sie wird wunderbar sein!
    Habe zuvor noch nie mit Threads gearbeitet, welcher Aufbau wäre denn "optimal"? Könntest du mir ein kleines Beispiel machen? Danke.

    Mono schrieb:

    Und Start ist das Problem genau wie @VaporiZed beschrieben hat.


    Auf welchem Weg müste ich dann das Steuerelement übergeben, damit das funktioniert? Ich muss ja über Start darauf zugreifen, oder gibt es andere Wege?

    Mono schrieb:

    da Form von Control erbt kannst du auch Form übergeben.
    Ich will aber den Text des Controls setzen, nicht aber der Form. ;)
    ======
    @vb123 Nutze die Dateianhangs-Funktionalität des Forums:
    Erweiterte Antwort => Dateianhänge => Hochladen.
    Und
    bereinige Dein Projekt, obj und bin gehören da nicht ein.
    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!
    @vb123 Dein Fehler besteht darin, dass Du in der Prozedur connectSmarTeam() die ranz mist sch... VB6-Kompatibilitätsinstanz Deiner Form verwendest:

    VB.NET-Quellcode

    1. Private Sub connectSmarTeam()
    2. ' Die Verwendung von Start ist der Fehler!
    3. ThreadInvokeHelper.setText(Start.Label1, "Hallo, ich bin der neue Text!")
    4. End Sub
    Teste mal dies:

    VB.NET-Quellcode

    1. Public Class Start
    2. Private Sub btnStartMigration_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btnStartMigration.Click
    3. Dim migrationThread As System.Threading.Thread
    4. migrationThread = New System.Threading.Thread(AddressOf startMigration)
    5. Debug.Print("Starting Migration...")
    6. migrationThread.Start() 'Vorgang starten
    7. End Sub
    8. Sub startMigration()
    9. ThreadInvokeHelper.setText(Me.Label1, "Hallo, ich bin der neue Text!")
    10. End Sub
    11. End Class
    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!
    Ich glaub ich würde es heute so machen:

    VB.NET-Quellcode

    1. Private Async Function dosmth(pr As IProgress(Of String)) As Task
    2. Await Task.Run(Sub()
    3. For a as Integer = 1 To 1000
    4. Threading.Thread.Sleep(1000)
    5. pr.Report("Counting in Background" & a.ToString)
    6. Next
    7. End Sub)
    8. End Function
    9. Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    10. Dim pr = New Progress(Of String)(AddressOf UpdateForm)
    11. Await dosmth(pr)
    12. End Sub
    13. Private Sub UpdateForm(text As String)
    14. Me.Label1.Text = text
    15. End Sub
    Das ist meine Signatur und sie wird wunderbar sein!

    RodFromGermany schrieb:

    Teste mal dies:
    VB.NET-Quellcode
    Public Class Start
    Private Sub btnStartMigration_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btnStartMigration.Click
    Dim migrationThread As System.Threading.Thread
    migrationThread = New System.Threading.Thread(AddressOf startMigration)
    Debug.Print("Starting Migration...")
    migrationThread.Start() 'Vorgang starten
    End Sub
    Sub startMigration()
    ThreadInvokeHelper.setText(Me.Label1, "Hallo, ich bin der neue Text!")
    End Sub
    End Class


    Danke, das funktioniert so. Das bedeutet jetzt aber im Umkehrschluss, dass es keine Möglichkeit gibt, die Funktion ThreadInvokeHelper.setText() aus einer anderen Klasse bzw. Modul aufzurufen, denn da kann ich ja mein Steuerelement nicht mit Me.[xy] abfragen.
    Habe ich das richtig verstanden?

    vb123 schrieb:

    Habe ich das richtig verstanden?
    Du müsstest das betreffende Control in den Thread hineinreichen.
    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!