none
form1, class, event = System.InvalidOperationException

    Frage

  • Hallo, allerseits!

    Ich möchte gerne Buttons eines Controllers (XBox360 über Bluetooth-Adapter) abfragen und deren Status auf einem Form (Checkbox) darstellen.

    Das Einlesen der Werte geschieht über eine Klasse, die diese Werte in Variablen speichert und über Property zur Verfügung stellt.

    Erster Ansatz war also:

    form1
      private x as new x_class
      timer_tick
        chk01.checked = x.a
        chk02.checked = x.b   [ usw. ]

    x_class
      private _a as boolean
      private _b as boolean   [ usw. ]
      public readonly property a as boolean
        get
          getvalues()
          return _a
      public readonly property b as boolean   [ usw. ]
      private sub getvalues()
        _a = …
        _b = …   [ usw. ]

    Das funktionierte soweit auch ganz gut, aber mich störte das getvalue() in jeder einzelnen Property, um die jeweils aktuellen Werte zu erhalten. Also habe ich einen Thread gebaut, der die Werte kontinuierlich im Hintergrund aktualisiert:

    x_class
      public sub new()
      _poll = new thread(addressof polltask)
      _poll.isbackground = true
      _poll.start()
      private sub polltask()
        _a = …
        _b = …   [ usw. ]

    Auch das funktionierte prima. Jetzt wollte ich aber noch einen Schritt weiter gehen. Das Hauptprogramm (also Form1) setzt ja immer und immer wieder in einer Endlos-Schleife die Werte. Da finde ich es ja besser, wenn dort nur Aktualisierungen gemacht werden, wenn sich Werte ändern. Also hab ich mir mal die Event-Auslösung und den Parameter EventArgs angesehen. Heraus kam eine von EventArgs abgeleitete Klasse, das RaiseEvent beim Auslesen der Werte (wenn sie sich ändern) und die Handler im Hauptprogramm.

    In den EventArgs speichere ich eine ID, den Namen und den Wert. Über die ID kann ich dann später die CheckBox bestimmen, die gesetzt werden soll.

    x_eventargs
      inherits eventargs

      private _b As integer
      private _bn As string
      private _bv As Boolean
      public readonly property b as integer
        get
          return _b
        end get
        [ Properties für _bn und _bv ]
    x_class
      private sub polltask()
      dim e as new x_eventargs
        [ wenn neuer Wert <> altem Wert ]
          e.b = …
          e.bn = …
          e.bv = …
          raiseevent change(me, e)

    form1
      private sub x_change(sender as object, e as x_eventargs) handles x.change
        dim o as checkbox
        if (e.b = 1) then o = chk01   [ tatsächlich per select case … ]
        o.checked = e.bv

    Aber das geht jetzt nicht mehr, und ich verstehe nicht so recht, warum nicht. Es kommt zu einem Fehler system.invalidoperationexception, weil die CheckBox in einem anderen Thread gesetzt werden soll, als dem, in dem sie erzeugt wurde. Aber das würde je bedeuten, daß der Handler in einem anderen Thread läuft. Irgendwo gesagt habe ich das m.W.n. aber nicht. Ich dachte eigentlich, das Einzige, was in einem anderen Thread läuft, ist das Abfragen der Werte und Setzen der internen Variablen in der x_class. Das Abfragen dieser Variablen geschieht doch wieder im Haupt-Thread über die Properties, oder etwa nicht?

    Wo ist da jetzt mein Gedankenfehler und wie korrigiere ich das?

    Gruß, Michael


    Dienstag, 6. November 2018 05:04

Alle Antworten

  • Hi Michael,
    polltask läuft in einem separaten thread. In diesem thread wird RaiseEvent ausgelöst. Ein Event (Ereignis) ist intern nichts anderes als ein delegate, der die Adresse der Ereignisroutine enthält (falls das Ereignis abonniert wurde). Beim RaiseEvent wird diese Ereignisroutine aufgerufen und abgearbeitet (wenn abonniert). Diese Abarbeitung läuft im Kontext des threads, in dem polltask läuft. Wenn in dieser Ereignisroutine z.B. auf Eigenschaften der Oberfläche zugegriffen wird, gibt es wegen thread-übergreifenden Zugriff einen Fehler.

    Das Problem kann man lösen, indem der Zugriff auf die Oberflächen-Elemente im thread ausgeführt wird, in welchem das Oberflächen-Element erzeugt wurde, das bedeutet im Normalfall im Haupt-Thread, in welchem das Programm gestartet wurde. Das kann man auf verschiedene Art und Weise erreichen:

    1. BackgroundWorkers anstelle _poll.Start

    2. Invoke-Methode des Steuerelementes (in der Ereignis-Routine)

    3. SynchronizationContext.Post anstelle RaiseEvent


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks

    Dienstag, 6. November 2018 05:30
  • Hallo, Peter!

    Zunächst erstmal recht herzlichen Dank für Deine schnelle Antwort. Ich bin bereits am Basteln und Probieren. Wenn ich ein Ergebnis habe, melde ich mich auf alle Fälle nochmal.

    Zwischenstand bis dahin:

    Backgroundworker hab ich schonmal irgendwo benutzt. Ich glaube, das war bei irgendeiner elend langen SQL-Abfrage. Das Ding such ich nochmal, könnte aber tatsächlich eine gute Methode sein.

    Invoke, muß ich ehrlich sagen, verstehe ich noch nicht so recht. Oder es macht nicht das, was ich möchte / brauche. Wenn ich es mit Invoke schaffe, das Event im Main-Thread auszulösen, dann wär alles gut. Wenn ich im Main-Thread aber nur per Invoke das Checkbox1.Checked setzen kann, dann ist das irgendwie nicht so das Wahre. Ich möchte sozusagen "wie gewohnt" im Main auf ein Event reagieren, ohne dort jedesmal dran denken zu müssen, ob das Event aus dem gleichen Thread kommt, oder nicht.

    SynchronizationContext.Post kenne ich noch gar nicht, werd ich mir aber auch mal ansehen. Aber z.Z. tendiere ich zum Bgw.

    Gruß, Michael

    Donnerstag, 8. November 2018 15:43
  • Hi Michael,
    schau Dir mal einen Beitrag von mir an unter:

    <Tipps und Tricks - informtools - Tipp 233>


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks

    Freitag, 9. November 2018 06:12
  • Hallo, Peter!

    Also, kurz gesagt: Das Problem besteht weiterhin.

    a) BackgroundWorker: Ich hab meinen alten Source gefunden und es ging wirklich um eine SQL-Abfrage, die länger dauert und deswegen im Hintergrund ablaufen sollte. Vereinfacht: Im bgw_DoWork() wurde die Abfrage gestartet und im bgw_RunWorkerCompleted() wurde das Ergebnis in eine ListBox übertragen. Damals hatte ich aber wohl auch die Komponente auf's Formular gezogen. Ok, läßt sich ja auch zur Laufzeit erstelllen.

    Ablauf bisher: class x - New() - PollTask() als Thread - dort wird ggf. ein RaiseEvent() gemacht.

    Ablauf jetzt: class x - New() - RunWorkerAsync() - DoWork() - dort wird ggf. ein RaiseEvent() gemacht.

    (Sowohl im PollTask() wie auch im DoWork() wird in einer Do..Loop-Endlosschleife die Sub Check() aufgerufen - genau genomen wird _dort_ das RaiseEvent durchgeführt.)

    Und die Fehlermeldung ist exakt die gleiche, wenn ich auf das Event in Form1 reagieren will, indem ich eine CheckBox1.Checked ändere: Falscher Thread

    b) Invoke: Ich krieg's nicht hin, befürchte aber auch, daß ich den falschen Ansatz habe. Und ich befürchte weiterhin, daß der richtige Ansatz nicht das ist, was ich haben möchte.

    Was ich möchte: In Form1 den einen oder anderen Event-Handler:

        Private Sub x_Event1(Sender As Object, e As x_Args) Handles x.myEvent1
            CheckBox1.Checked = True
        End Sub

    Und der soll auch wirklich nur so aussehen. Ich möchte mich dort nicht darum kümmern müssen, ob das Event im korrekten Thread ausgeführt wird, oder nicht. Und ob ich dort eine CheckBox ändere, einen Sound abspiele oder den Rechner ausschalte, entscheide ich erst und nur dort. Die x-Klasse soll sich darum nicht kümmern.

    Wenn ich das mit dem Invoke aber richtig verstehe, dann muß ich dort bereits die CheckBox1 angeben. Und das ist eben das, was nicht geht. Ich will nur wissen, ob eine Taste gedrückt oder losgelassen wird, genau wie KeyDown() oder MouseUp()-Ereignsse.

    Also: Wie löse ich das Event (und nur das) so aus, daß es im Thread des Form1 aufschlägt und nicht in einem Hintergrund-Thread? Also so, daß die weitere Bearbeitung des Events dort ohne weitere Maßnahmen Zugriff auf die Formular-Elemente zuläßt?

    Wo ist die Kneifzange? Ich scheine 'n Haufen Bretter vor den Kopf genagelt zu haben.

    Gruß, Michael

    Sonntag, 11. November 2018 00:04
  • Hi Michael,
    wenn Du den BackgroundWorker nutzt, dann läuft DoWork in einem separaten Thread. Aus diesem dort laufenden Code kannst Du nicht auf Steuerelemente zugreifen, da in diesem Fall ein Fehler wegen thread-übergreifendem Zugriff geworfen wird. Die Ereignisse für den Fortschritte und Completed des BackgroundWorkers werden im Kontext das Ausgangs-Threadfs ausgeführt und da gibt es keine Fehler wegen thread-übergreifendem Zugriff.

    Wenn Du auf Steuerelemente im Haupt-Thread aus einem anderen Thread zugreifen willst, dann kannst du Die Invoke-Methode des betreffenden Steuerelementes nutzen. Das kann bei mehreren Steuerelementen etwas Aufwändig werden, weshalb in solchem Fällen besser der SynchrnizationContext genutzt werden kann. 

    Beispiel eines Zugriffs auf eine Checkbox:

    ' Beispiel für den Thread
       Sub myThread()
        Do
          System.Threading.Thread.Sleep(100)
          SetValue(true)
        Loop
      End Sub
    
    ' irgendwo ist die Checkbox deklariert, z.B. im Designer-File
    Private cb As New CheckBox
    
    
    ' Deklaration des Typs des Sprungbefehls
      Delegate Sub SetValueDeleg(value As Boolean)
    
    ' Wert im Steuerelement setzen
      Sub SetValue(value As Boolean)
    ' prüfen, ob aus anderem Thread zugegriffen wird.
        If cb.InvokeRequired Then
    ' wenn ja, dann ein Invoke ausführen
          cb.Invoke(New SetValueDeleg(AddressOf SetValue), value)
        Else
          cb.IsChecked = Value
        End If
      End Sub
    
    

    Mit dem SynchronizationContext kann man beispielsweise so arbeiten:

    Option Infer On
    Option Strict Off
    
    Imports System.Threading
    
    ''' <summary>Klasse mit gekapselten Methoden, 
    ''' die in separaten thread abgearbeitet werden</summary>
    Public Class Class1
    
      ''' <summary>Ereignis zum Rückmelden des Zustandes</summary>
      Public Event Ereignis(ByVal txt As String)
    
      ''' <summary>Context das Ausgangsthreads merken</summary>
      Private sc As SynchronizationContext
    
      ''' <summary>Konstruktor, in dem der Einfachkeit halber gleich der thread gestartet wird</summary>
      Sub New()
        sc = SynchronizationContext.Current
        Call (New Thread(AddressOf thSub)).Start()
      End Sub
    
      ''' <summary>Methode, die als thread gestartet wird</summary>
      Private Sub thSub()
    ' Hier irgendwas machen und dann Ereignis auslösen
          sc.Post(New SendOrPostCallback(AddressOf RaiseEreignis), "Ereignis " & i.ToString)
        Next
      End Sub
    
      ''' <summary>Ereignis auslösen im Context des Ausgangsthreads</summary>
      Private Sub RaiseEreignis(ByVal state As Object)
        RaiseEvent Ereignis(state.ToString)
      End Sub
    
    End Class


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks

    Sonntag, 11. November 2018 10:51
  • Hallo, Peter!

    Hmmm... Also... a) Es funktioniert b) nicht so, wie ich eigentlich möchte. Aber grundsätzlich ist Invoke() wohl noch der geringste Aufwand:

    Public Class Form1
    
        Private WithEvents xb360 As New Xbox360Controller
    
        Delegate Sub setCheckBoxDelegate(n As Integer, v As Boolean)
        Delegate Sub setProgressBarDelegate(n As Integer, v As Single)
    
        Private Sub setCheckBox(n As Integer, v As Boolean)
            Dim o1 As CheckBox
            Select Case n
                Case 1 : o1 = Me.chk_B_A
                ' ... 14
                Case Else
                    o1 = Nothing
                    'Debug.Print("unknown button: " & n)
            End Select
            If (Not o1 Is Nothing) Then
                If (o1.InvokeRequired) Then
                    o1.Invoke(New setCheckBoxDelegate(AddressOf setCheckBox), n, v)
                Else
                    o1.Checked = v
                End If
            End If
        End Sub
    
        Private Sub setProgressBar(n As Integer, v As Single)
            Dim o1 As ProgressBar : Dim o2 As Label : Dim v2 As Single
            Select Case n
                Case 1 : o1 = Me.prg_LeftX : o2 = Me.lbl_LeftX : v2 = Int(v * 50) + 50
                ' ... 6
                Case Else
                    o1 = Nothing : o2 = Nothing : v2 = 0
                    'Debug.Print("unknown axis: " & n)
            End Select
            If (Not o1 Is Nothing) Then
                If (o1.InvokeRequired) Then
                    o1.Invoke(New setProgressBarDelegate(AddressOf setProgressBar), n, v)
                Else
                    o1.Value = v2 : o2.Text = v
                End If
            End If
        End Sub
    
        Private Sub xb360_ButtonDown(Sender As Object, e As xb360EventArgs) Handles xb360.ButtonDown
            'Debug.Print("Event: " & e.ButtonName & " (" & e.ButtonNumber & ") down")
            setCheckBox(e.ButtonNumber, True)
        End Sub
    
        Private Sub xb360_ButtonUp(Sender As Object, e As xb360EventArgs) Handles xb360.ButtonUp
            'Debug.Print("Event: " & e.ButtonName & " (" & e.ButtonNumber & ") up")
            setCheckBox(e.ButtonNumber, False)
        End Sub
    
        Private Sub xb360_AxisMove(Sender As Object, e As xb360EventArgs) Handles xb360.AxisMove
            'Debug.Print("Event: " & e.AxisName & " (" & e.AxisNumber & ") = " & e.AxisValue)
            setProgressBar(e.AxisNumber, e.AxisValue)
        End Sub
    
    End Class
    

    Mich stört eben Folgendes:

        Delegate Sub setCheckBoxDelegate(n As Integer, v As Boolean)
        Delegate Sub setProgressBarDelegate(n As Integer, v As Single)
    
    [ ... ]
    
                If (o1.InvokeRequired) Then
                    o1.Invoke(New setCheckBoxDelegate(AddressOf setCheckBox), n, v)
                Else
                    o1.Checked = v
                End If
    
    [ ... ]
    
                If (o1.InvokeRequired) Then
                    o1.Invoke(New setProgressBarDelegate(AddressOf setProgressBar), n, v)
                Else
                    o1.Value = v2 : o2.Text = v
                End If
    

    Ich muß im Form1 immer daran denken, die Delegates zu deklarieren und den Zugriff auf die Formular-Elemente mit dem InvokeRequired zu kapseln. Geht das denn partout nicht auch ohne das?

    Aber ok, wenn es wirklich nicht anders geht, dann muß ich mich eben daran gewöhnen - irgendwo in der Klasse dann ein paar Kommentarzeilen mit dem Krams als Gedankenstütze oder so. ;-)

    Vielen Dank in jedem Fall für die Hilfe. Ich schau mal, welches Posting am besten als "Antwort" zu markieren ist.

    Gruß, Michael

    Sonntag, 11. November 2018 18:24
  • Hallo nochmal, Peter!

    Nachtrag: Ich werde nochmal etwas mit dem ReportProgress() für den BackgroundWorker experimentieren. Vielleicht spare ich mir das Invoke() dann ja doch noch. Ich melde mich dann nochmal... ;-)

    Wenn das funktioniert, dann war der entscheidende Hinweis, daß nicht nur Completed, sondern auch Progress bereits wieder im Haupt-Thread laufen. Den Hinweis hätte ich beinahe überlesen.

    Gruß, Michael

    Sonntag, 11. November 2018 18:38
  • Hi Michael,
    natürlich geht das auch anders. Ein Beispiel dazu hatte ich Dir gezeigt. Du kapselst alles zum separaten Thread in einer Klasse, die vom UI-Thread instanziiert wird. Beim Instanziieren merkst Du Dir den aktuellen SynchonizationContext. Alle CallBacks aus dieser Klasse, die auf den UI-Thread wirken, rufst Du mit SynchonizationContext.Post auf.

    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks

    Sonntag, 11. November 2018 18:58
  • Hi Michael,
    blöd ist nur, dass im Progress-Ereignis nur eine Zahl (Fortschritts-Prozent) übergeben werden kann.

    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks

    Sonntag, 11. November 2018 19:06
  • Hallo, Peter!

    Ja, ich schätze, darüber stolpere ich gerade. Ok, ich kann pauschal ReportProgress(50) machen, wenn ich die zu verarbeitenden Werte denn woanders liegen habe. Dachte ich...

    Mein Ansatz bis eben: Die Check() Routine löst die Events nicht direkt selbst aus, sondern legt die EventArgs in eine Klassen-globale private List(Of xb360EventArgs) ab und übergibt ein True zurück, wenn mind. ein Event neu dazu gekommen ist.

    DoWork ruft Check() und bei True dann direkt ReportProgress() auf und wartet dann in einer While-Schleife mit DoEvents() darauf, daß die List() wieder leer ist. Das ganze wiederholt sich dann in einer Do..Loop.

    ProgressChange ruft dann für jedes Element der List() das passende RaiseEvent auf und löscht das Element dann aus der Liste. Soweit der Plan...

    In der ProgressChange: Der List.Count ist > 0 (i.a. 1), aber die Elemente sind irgendwie leer. Z.B. die List(0).ButtonNumber (sollte 1..14) ist immer 0. Es wird zwar das Event ausgelöst, aber wegen der 0 passiert nix.

    Löse ich in der Check() das RaiseEvent direkt aus, geht alles wie gewollt. Also scheint irgendwas beim List.Add vom einen Thread (BackgroundWorker) in den anderen (Form1) verloren zu gehen. Merkwürdigerweise packt er ja ein Element in die Liste, aber die Member sind auf 0. Mal sehen, ob ich dem auch noch auf die Spur komme...

    Gruß, Michael

    Sonntag, 11. November 2018 22:21
  • Hi Michael,
    und warum nutzt Du nicht die Variante mit dem SynchronizationContext? Nach Deinen Schilderungen würde sie am besten zu Deinen Anforderungen passen.

    Auf DoEvents solltest Du vollständig verzichten. Das ist bei passendem Programmdesign nicht erforderlich. Mit DoEvents hältst Du nur den aktuellen Thread an, damit im aktuellen Thread anhängige abzuarbeitende Routinen ausgeführt werden. Früher wurde DoEvents üblicherweise im UI Thread genutzt, um einem RefResh der Oberfläche Zeit zu geben.

    Wenn Du den Thread mit DoWork des BackgroundWorkers nutzt und darin Deine Check-Routine aufrufst, dann nutze in der Ckeck-Routine das SynchronizationContext.Post. Die möglichen Sprungziele (delegates) kannst Du vor dem Start des BackgroundWorkers festlegen, ähnlich dem Abonnieren eines Ereignisses. Die Check-Routine prüft den Verarbeitungszustand der Schleife im Dowork und wählt das betreffenden Sprungziel (delegate) aus.


    --
    Viele Grüsse
    Peter Fleischer (ehem. MVP für Developer Technologies)
    Meine Homepage mit Tipps und Tricks

    Montag, 12. November 2018 03:52