Invoke aus einer DLL heraus - aber wie?

  • VB.NET

Es gibt 15 Antworten in diesem Thema. Der letzte Beitrag () ist von Kangaroo.

    Invoke aus einer DLL heraus - aber wie?

    Hi. Ich bastel grade an einer Klassenbibliothek. Dort gibt es eine Klasse die einen Thread beinhaltet welcher dann Events raisen soll. Das Problem ist, dass der Event-Code dann im Thread abgearbeitet wird und wenn ein Benutzer z.B. GUI-Zeug anspricht bekommt er von der IDE gemeckert. Invoke ist "nicht verfügbar" da ich von der DLL aus die GUI-Klasse (Form1, wasauchimmer) ja nicht kenne. Ich fände es auch unschön wenn die DLL-Klasse diese per Konstruktor übergeben bekommen würde...

    Kleines CodeBeispiel:

    die DLL:

    VB.NET-Quellcode

    1. Class dllKlasse
    2. Public Event etwasIstPassiert()
    3. private Sub threadCode 'dieser Code läuft in einem Thread der dllKlasse
    4. '...
    5. raisevent etwasIstPassiert()
    6. end Sub
    7. End Class


    Forms-Anwendung:

    VB.NET-Quellcode

    1. Class Form1
    2. withevents dllc as new dllKlasse
    3. sub asdf() handles dllc.etwasIstPassiert
    4. label1.text = "oh gott"
    5. end sub
    6. end Class


    Da muss es doch einen geschickten Weg geben... ein BackgroundWorker kann das schließlich ja auch (ProgressChanged-Event). Habe mich mit solchen Dingen bisher nur wenig rumgeschlagen, darum sry für meine Unwissenheit xD

    lg
    Hey,

    hier kannst Du z. B. mit AsyncOperation arbeiten.

    VB.NET-Quellcode

    1. Imports System.ComponentModel
    2. Public Class ThreadedClass
    3. Private _asyncOp As AsyncOperation = AsyncOperationManager.CreateOperation(Nothing)
    4. Private Delegate Function DelegateWork() As Integer
    5. Private _delWork As New DelegateWork(AddressOf Work)
    6. Public Event WorkDone(ByVal sender As Object, ByVal result As Integer)
    7. Public Sub StartWork()
    8. _delWork.BeginInvoke(AddressOf WorkCompleted, Nothing)
    9. End Sub
    10. Private Function Work() As Integer
    11. Dim result As Integer = 0
    12. For i As Integer = 0 To 1000
    13. result += i
    14. Next
    15. Return result
    16. End Function
    17. Private Sub WorkCompleted(ByVal ar As IAsyncResult)
    18. Dim result As Integer = _delWork.EndInvoke(ar)
    19. _asyncOp.PostOperationCompleted(New Threading.SendOrPostCallback(AddressOf OnWorkCompleted), result)
    20. End Sub
    21. Private Sub OnWorkCompleted(ByVal o As Object)
    22. Dim result As Integer = DirectCast(o, Integer)
    23. RaiseEvent WorkDone(Me, result)
    24. End Sub
    25. End Class
    Die Unendlichkeit ist weit. Vor allem gegen Ende. ?(
    Manche Menschen sind gar nicht dumm. Sie haben nur Pech beim Denken. 8o

    VB.NET-Quellcode

    1. Class Form1
    2. withevents dllc as new dllKlasse
    3. sub asdf() handles dllc.etwasIstPassiert
    4. label1.text = "oh gott"
    5. end sub
    6. end Class



    VB.NET-Quellcode

    1. Class Form1
    2. withevents dllc as new dllKlasse
    3. sub asdf() handles dllc.etwasIstPassiert
    4. Me.Invoke(Sub() label1.text = "oh gott")
    5. end sub
    6. end Class


    Die dllKlasse soll nur das machen, was sie soll. Was, wenn ich die Dll in einer Konsolenanwendung verwende? Oder in einem Hintergrundthread auf eine Funktion daraus zugreife? Die Dll kann ja nicht wissen, wer oder was die Funktionen aufrufen wird. Kann sie auch nicht.
    Deshalb bleibt das Invoken bei der Anwendung, die die Dll verwendet.
    "Luckily luh... luckily it wasn't poi-"
    -- Brady in Wonderland, 23. Februar 2015, 1:56
    Desktop Pinner | ApplicationSettings | OnUtils
    mit einem Trick kann man einer externen Klasse beibringen, ihre Events im Gui-Thread zu feuern: Da es fürs Invoking egal ist, welches Control invoket, kann man einfach das erstbeste Form der Application.OpenForms - Auflistung hernehmen.
    Es gibt zwar "Glaubenskrieger", die das ganz schlimm finden, wenn der Event-Sender sich um die Thread-Delegation kümmert, anstelle des Event-Empfängers, aber ich bin halt grad vom anneren Krieger-Stamm ;)
    (Genannte GlaubensKrieger können auch nicht so recht erklären, warum genau das beim Backgroundworker, beim WebClient, beim WebBrowser etc. i.O. sein soll)

    Jdfs. auf obigem Trick beruht auch mein AsyncWorker - CodeProject.
    Also da gibts paar praktische Extensions dazu, und ausserdem eine bessere Alternative zum ollen Backgroundworker-Crap, und ein Grundsatz-Artikel zu Threading - (allerdings in der Prä-2ß12-Logik).

    Und übrigens: Control.Invoke() vs. .BeginInvoke()

    FreakJNS schrieb:

    Da muss es doch einen geschickten Weg geben... ein BackgroundWorker kann das schließlich ja auch (ProgressChanged-Event)
    Der BackgroundWorker macht es über einen sogenannten SynchronizationContext, ebenso basiert Control.Invoke darauf ( genauer gesagt der abgeleiteten Klasse WindowsSynchronizationContext).

    Du kannst das genauso machen:

    VB.NET-Quellcode

    1. Class dllKlasse
    2. Public Event etwasIstPassiert()
    3. Private _context As SynchronizationContext
    4. Public Sub New()
    5. ' context des Threads speichern in dem die Klasse angelegt wurde: kann nothing sein wenn die Klasse nicht im GUI-Thread angelegt wird
    6. _context = SynchronizationContext.Current
    7. End Sub
    8. Public Sub Start()
    9. Dim t As New Thread(Sub() threadCode()) With {.IsBackground = True}
    10. t.Start()
    11. End Sub
    12. Private Sub threadCode() 'dieser Code läuft in einem Thread der dllKlasse
    13. '...
    14. If _context IsNot Nothing Then _context.Send(Sub() RaiseEvent etwasIstPassiert(), Nothing) Else RaiseEvent etwasIstPassiert()
    15. End Sub
    16. End Class

    Die SynchronizationContext Klasse enthält 2 wichtige Methoden:
    - Send : erfolgt synchron , entspricht also einem Control.Invoke
    - Post: erfolgt asynchron, entspricht dem Control.BeginInvoke

    @Erfinder Dein sogenannter 'Trick' ist ziemlich unelegant, nicht ausreichend (NotifyIcon) oder sogar falsch (Forms mit separaten GUI-Threads).

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Kangaroo“ () aus folgendem Grund: KOmmentar geändert

    vielen Dank für diese Info (ich hoffe, sie ist verlässlich - also testweise bestätigt habich sie):

    Kangaroo schrieb:

    VB.NET-Quellcode

    1. ' context des Threads speichern in dem die Klasse angelegt wurde:
    2. ' kann nothing sein wenn die Klasse nicht im GUI-Thread angelegt wird
    ich war der SynchronisizeContext-Lösung immer abgeneigt, weil ich davon ausging, .Current gebe immer einen Context zurück, und wenn man da sone von dir gezeigte Konstruktion im falschen Thread initialisiert, habeman den falschen Context und sei besonders angemeiert.
    Aber indem bei Falsch-Initialisierung Nothing zurückgegeben wird, kann sone Klasse ja selbst ihre Funktionsfähigkeit testen, und Exception schmeißen (müssteman noch einbauen).
    Was meinst Du mit dem Begriff 'Falsch-Initialisierung' ? Ein Thread kann einen eigenen SynchronisationContext haben wenn man ihn explizit setzt, oder auch nicht. Dann ist er NULL (=Nothing).

    Ich muss zugeben: grundsätzlich bin ich eher vom einem ähnlichen 'Stamm' wie Niko Ortner und Invoke lieber gezielt da wo ich es benötige, als dass ich hilfreiche Konstruktionen wie das Speichern des anlegenden Context verwende. Wie man anhand der hässlichen VB.NET Konstruktion Form1=Klasse + Instanz sieht, passieren da manchmal sehr hässliche Fehler die man nur schwer findet.

    Aber wenn, dann bitte mit Hilfe des SynchronizationContext, so funktioniert das dann auch in einer Konsolenanwendung.

    Eigenes Invoken eines Events ist simpel, entweder so oder über eine Hilfsklasse:

    VB.NET-Quellcode

    1. Private Sub etwasIstPassiertHandler() Handles dll.etwasIstPassiert
    2. ' synchronisert auf den GUI-Thread wenn nötig
    3. If Me.InvokeRequired Then Me.Invoke(Sub() etwasIstPassiertHandler()) : Return
    4. ' ab hier laüft es auf dem richtigen Thread
    5. End Sub
    SpaceyX
    Werde ich mich dazu mal schlau machen. Aber dann die ganzen delegates-dinger und für jedes Event mehrere Subs... bei recht vielen Events verliert man da den Überblick. Jenachdem was man macht könnte das aber durchaus praktisch sein

    Niko Ortner
    Fände ich auch in Ordnung - aber es ist immer schön wenn die DLLs einem soviel wie möglich abnehmen xD

    ErfinderDesRades
    Interessanter Trick xD
    Ich denke mal der BGW etc nimmt dem Benutzer die "Arbeit" ab um möglichst einfach in der Handhabung zu sein - zumal ja in 99% der Fälle klar ist, dass in zwei BGW-Events Zugriffe auf die Form stattfinden werden^^ Deinen AsyncWorker habe ich mir vor längerem mal angesehen, schon nice das Teil! (nur bin ich leider oft faul und benutze trotzdem dem BGW oder direkt Threads, wenn keine Forms-Zugriffe erforderlich sind.)

    Kangaroo
    Ich denke deine Lösung gefällt mir für mein Problem am besten^^

    Wenn ich das richtig verstanden habe wird .Current nothing wenn man die Klassen-Konstruktion in einem anderen Thread instanziert (ein Test zeigt das auch). Da würde ich aber kein Problem sehen, da der Programmierer der die DLL verwendet weiß, dass er von dem Thread aus eh nicht direkt auf die GUI zugreifen kann.. (Ich glaube das meint der EDR mit Falsch-Initalisierung)

    Ich werde dann wohl ein misch-masch von Kangaroo und Niko Ortner benutzen..

    Jedenfalls vielen Dank an alle für die schnelle Hilfe!

    FreakJNS schrieb:

    Wenn ich das richtig verstanden habe wird .Current nothing wenn man die Klassen-Konstruktion in einem anderen Thread instanziert (ein Test zeigt das auch). Da würde ich aber kein Problem sehen, da der Programmierer der die DLL verwendet weiß, dass er von dem Thread aus eh nicht direkt auf die GUI zugreifen kann.
    Dann mach es so, es ist halt mehr eine Stilfrage. Wenn Du die DLL veröffentlichen willst, so könntest Du im Konstruktor sicherheitshalber noch abprüfen ob der Context vom Type WindowsSynchronizationContext ist, so kann man (ziemlich) sicher sein dass er aus einem GUI-Thread kommt. Nur für den Eigenverbrauch ist das ja nicht relevant.

    Ansonsten wird das Event halt ganz normal 'geraised'.

    Kangaroo schrieb:

    Was meinst Du mit dem Begriff 'Falsch-Initialisierung' ?

    Damit meine ich, wenn deine Klasse aus post#6 in einem NebenThread instanziert wird.
    Weil dann gibt SynchronisizeContext.Current (hofflich) Nothing zurück, und dann erfolgt das Event doch wieder im NebenThread und im Gui dann Boing (ich würd ja eine Exception werfen, dasses garnet so weit kommt).

    Was schlägst du eiglich vor, zu unternehmen, wenn der User das Gui schließt, der NebenThread aber noch läuft?
    (Also ich täte ja vor dem Raisen des Events in die App.Openforms-Auflistung gugge, ob noch Formse da sind ;))

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

    Ich würde im Form-Closing-Event dafür sorgen, dass der Nebenthread beendet wird. Ist es so weit, dann feuert er ein entsprechendes Event und die Form darf sich schließen. (Oder sie schließt sich einfach sofort und hofft nach "FireAndForget" darauf, dass der Thread sich schon selbst beendet und keinen Unfug mehr macht^^)
    Wenn man die GUI einfach komplett totschießt könnten aber immernoch Fehler entstehen - aber das ist ja nicht absehbar.

    FreakJNS schrieb:

    Ich denke mal der BGW etc nimmt dem Benutzer die "Arbeit" ab um möglichst einfach in der Handhabung zu sein
    Dummerweise isser garnicht möglichst einfach in der Handhabung.
    Man kann ja nichtmal typisierte Argumente in die asynchrone Verarbeitung geben.
    Ebensowenig, wie man typisierte Argumente in der Abschluss-Methode erhalten kann.

    Ärmlich ist das.




    FreakJNS schrieb:

    Ich würde...
    Najaa - vlt. doch AsyncWorker wenigstens mal angugge - was da an Mechanismen gebastelt ist für sowas.
    Den Kannmanja auch umfrickeln auf SynchronisizeContext, wennman die .OpenForms-Auflistung so gräuslich findet. Sone Umfrickelei würde das Design ja garnet berühren.

    ErfinderDesRades schrieb:

    Weil dann gibt SynchronisizeContext.Current (hofflich) Nothing zurück
    Probiers aus: wie oben gepostet macht es beim expansiven Multithreading manchmal Sinn dem Thread einen eigenen Context zu verpassen. Ansonsten ist er Null.

    ErfinderDesRades schrieb:

    Was schlägst du eiglich vor, zu unternehmen, wenn der User das Gui schließt, der NebenThread aber noch läuft?
    Gegenfrage: was meinst Du mit 'GUI schliessen' ? Du kannst zwar die Forms schliessen, damit besteht der ApplicationContext aber weiterhin. Nur wenn die VS Optionen CloseOnStartupForm oder CloseOnLastForm gesetzt sind, wird auch die Application mitsamt des Context beendet.

    ErfinderDesRades schrieb:

    Also ich täte ja vor dem Raisen des Events in die App.Openforms-Auflistung gugge, ob noch Formse da sind
    Eine Forms Anwendung hat immer einen ApplicationContext und kann - muss aber nicht - offene Forms besitzen..

    Praktisches Beispiel:
    - Application wird aus einer Sub Main per Application.Run(myApplicationContext) gestartet
    - zeigt nur ein NotifyIcon im Task Tray an oder regiert ohne visuelles Element über Hotkey
    - Formen werden nach Bedarf vom Kontextmenu des NotifyIcons geöffnet bis sie vom User geschlossen werden

    (Für ganz penible Naturen: ein Foreground Thread kann auch die Beendigung des Context überleben, dann gibts halt zu Recht einen Error weil der User vermutlich ziemlichen Schrott programmiert hat)

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

    ErfinderDesRades schrieb:

    Du drückst dich nur vor einer Antwort. Du kannst mir nicht erzählen, du würdest das angesprochene Problem nicht verstehen.
    Ich weiss nicht ob irgendjemand Deine Frage verstanden hat. Wenn Du Informationen möchtest so drück Dich präzise aus. Was ist jetzt 'GUI schliessen' in Deiner Begriffswelt ?

    Ich mach nicht umsonst Examples for Dummies. [engl: hat nix mit dumm zu tun]