Zugriff auf ListView von externen Thread

  • VB.NET

Es gibt 24 Antworten in diesem Thema. Der letzte Beitrag () ist von pts.

    Zugriff auf ListView von externen Thread

    Hallo,

    ich möchte auf ein ListView aus einem anderen gestarteten Thread zugreifen. Deshalb habe ich das folgende Sub erstellt, auf welches der neue Thread aufruft:


    VB.NET-Quellcode

    1. Friend Sub AddToList(LVI As ListViewItem)
    2. SyncLock TestListView
    3. TestListView.Items.Add(LVI)
    4. MsgBox(TestListView.Items.Count.toString)
    5. End SyncLock
    6. End Sub


    Mein ListView füllt sich auch, wie ich mit MsgBox erkennen kann. Leider bleibt das Listview trotzdem leer...

    Ich habe schon an TestListView.Refresh oder Me.Refresh gedacht, aber leider führt keine der beiden Methoden zu einer Anzeige...

    Wo liegt mein Fehler? ;(

    In richtiges Unterforum verschoben. ~fufu

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

    Interessant, dass Du soweit gekommen bist. Wenn ich versuche, ein ListView mithilfe eines anderen Threads zu befüllen, erhalte ich das von mir Erwartete:

    Visual Studio schrieb:

    System.InvalidOperationException: "Ungültiger threadübergreifender Vorgang: Der Zugriff auf das Steuerelement TestListView erfolgte von einem anderen Thread als dem Thread, für den es erstellt wurde."

    Hast Du etwa CheckForIllegalCrossThreadCalls = False irgendwo stehen?
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Häufig von mir verwendete Abkürzungen: CEs = control elements (Labels, Buttons, DGVs, ...) und tDS (typisiertes DataSet)
    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht in den Spekulatiusmodus gehen.
    Dazu 5 Gedanken - jeder ist substanziell, also achte drauf, dass, wenn du mit einem Gedanken dich auseinandergesetzt hast, dir auch die anneren vorzunehmen.
    Meist beschäftigen sich Fragesteller nur mit einem Gedanken, und 5 Posts weiter muss man sie dann drauf hinweisen, dass man das alles bereits in Post#2 beantwortet hat.
    1. aus einem NebenThread kann man nicht auf Controls zugreifen - das geht aus technischen Gründen nicht.
    2. Sondern man muss den Zugriff per Control.BeginInvoke() an den MainThread zurück-delegieren.
      Controls Threadsicher machen
      Lass dir nix anneres einreden - extrem weit verbreitet ist, Control.Invoke zu empfehlen, was allerdings gegenüber .BeginInvoke ganz unnütz Performance verschenkt.
    3. Nun wäre es bei MassenOperationen (etwa Befüllung eines ListViews) verheerend, für jedes Item einen Threadwechsel vorzunehmen.
      Daher stellt man in NebenThreads MassenDaten bereit - das Einfügen erfolgt aber summarisch wieder im MainThread
    4. Ein sehr fortschrittliches Konzept ist Async/Await - das kommt ohne Control.BeginInvoke aus
    5. Meist ist garkein Threading erforderlich
      Etwa die einfache Anwendung von ListView.BeginUpdate/.EndUpdate kann einen Ladevorgang u.U. um Faktor 1000 beschleunigen.
    Danke, ich werde mich mal mit dem Thema Invoke auseinandersetzen.

    @VaporiZed CheckForIllegalCrossThreadCalls = False habe ich nirgends stehen. Die Meldung hatte ich eher schon, als ich dem Thread das ListView als Variable übergeben habe - was natürlich Quark ist.

    Danke, ich vermelde später Erfolge / Misserfolge

    EDIT: so?

    VB.NET-Quellcode

    1. Friend Sub AddToList(LVI As ListViewItem)
    2. TestListView.Invoke(Sub() TestListView.Items.Add(LVI))
    3. End Sub


    Weil da bekomme ich den Fehler "System.InvalidOperationException: "Invoke oder BeginInvoke kann für ein Steuerelement erst aufgerufen werden, wenn das Fensterhandle erstellt wurde.""

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

    pts schrieb:

    wenn das Fensterhandle erstellt wurde
    ? Wann rufst Du diese nebenläufige Befüllungsroutine denn auf? Bei Erstellung des Formulars?
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Häufig von mir verwendete Abkürzungen: CEs = control elements (Labels, Buttons, DGVs, ...) und tDS (typisiertes DataSet)
    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht in den Spekulatiusmodus gehen.

    pts schrieb:

    Danke, ich werde mich mal mit dem Thema Invoke auseinandersetzen.
    [...]
    EDIT: so?

    VB.NET-Quellcode

    1. Friend Sub AddToList(LVI As ListViewItem)
    2. TestListView.Invoke(Sub() TestListView.Items.Add(LVI))
    3. End Sub
    Damit hast du es fast auf meine Ignorier-Liste geschafft.
    Weil meine Posts liest du offsichtlich eh nicht.

    Warum nur fast? Anfänger-Bonus.
    Allerdings in eim anneren Thread biste mit NoFear ja auch nicht anners umgegangen - also du hast in kürzester Zeit doch beträchtlich Minuspunkte angesammelt - vermutlich ohne zu merken, deshalb sag ichs dir.
    @ErfinderDesRades Upps, das war nicht mein Ziel. Ich versuch es nochmal...

    aus einem NebenThread kann man nicht auf Controls zugreifen - das geht aus technischen Gründen nicht.

    ok

    Sondern man muss den Zugriff per Control.BeginInvoke() an den MainThread zurück-delegieren.
    Controls Threadsicher machen
    Lass dir nix anneres einreden - extrem weit verbreitet ist,
    Control.Invoke zu empfehlen, was allerdings gegenüber .BeginInvoke ganz
    unnütz Performance verschenkt.


    .BeginInvoke scheint bei mir keine mögliche Methode zu sein (zumindest zeigt IntelliSense die nicht an) Deswegen habe ich .Invoke genommen

    Nun wäre es bei MassenOperationen (etwa Befüllung eines ListViews) verheerend, für jedes Item einen Threadwechsel vorzunehmen.

    Daher stellt man in NebenThreads MassenDaten bereit - das Einfügen erfolgt aber summarisch wieder im MainThread

    Ein sehr fortschrittliches Konzept ist Async/Await - das kommt ohne Control.BeginInvoke aus


    Das ich hier noch ein einzelnes ListViewItem übergebe ist nur zu Testzwecken. Später möchte ich AddRange nutzen

    Meist ist garkein Threading erforderlich

    Etwa die einfache Anwendung von ListView.BeginUpdate/.EndUpdate kann einen Ladevorgang u.U. um Faktor 1000 beschleunigen.


    Nun ja, ich lade nicht aus einer festen Quelle. Dazu muss ich ausholen:

    In Form1 wird ein Thread1 gestartet, welcher wiederrum mehrere Threads startet, die parallel diverse Aufgaben ausführen. Am Ende sind alle Daten erfolgreich in Variablen in Thread1 - soweit alles gut. Thread 1 habe ich gestartet, damit Form1 während der Aufgaben nicht hängt, es kann gern mal einige Sekunden dauern.

    Nun möchte ich in Thread1 auf das TestListView in Form1 zugreifen - es mit den bereitliegenden Daten befüllen.

    Das Sub AddToList() befindet sich im Form1.vb

    Habe ich einen grundsätzlichen Fehler begangen? Threading ist ein neues Thema für mich... Ich habe mich zwar mit diverser Literatur beschäftigt, aber eben noch nicht alles verstanden.

    @VaporiZed
    Das Form1 samt TestListView sind schon offen und (leer) geladen.

    pts schrieb:

    .BeginInvoke scheint bei mir keine mögliche Methode zu sein (zumindest zeigt IntelliSense die nicht an) Deswegen habe ich .Invoke genommen
    Kann nicht sein. Mit Sicherheit funzt dieses ebensogut, und wird auch Intellisense-Unterstützt:

    VB.NET-Quellcode

    1. Friend Sub AddToList(LVI As ListViewItem)
    2. TestListView.BeginInvoke(Sub() TestListView.Items.Add(LVI))
    3. End Sub
    Ansonsten muss dein VisualStudio iwie verkorkst sein.

    Noch ansonstener freuts mich, dass du dich mit meine Punkte nu erkennbar auseinandersetzt, und ich finde, es bringt auch entsprechend sehr viel: Dir wird mit BeginInvoke geholfen sein, und alle Leser haben ein deutlich klareres Bild der Problemstellung.



    Zu deiner Frage: Nee, ich find, du machst das alles genau korrekt, und sollte so denn auch funzenieren.
    Deine Fehlermeldung aus post#4 bedeutet aber, dass der Thread viel zu schnell fertig ist, nämlich noch ehe das TreeListView richtig initialisiert ist.
    Sowas kann auch passieren, wenn TreeListView auf einem TabControl-Tab sitzt, der noch nie angezeigt wurde.
    Probierma so:

    VB.NET-Quellcode

    1. Friend Sub AddToList(LVI As ListViewItem)
    2. Me.BeginInvoke(Sub() TestListView.Items.Add(LVI)) 'Me ist das Form, und kann ebensogut fürs Invoking herangezogen werden
    3. End Sub
    Wie gesagt: Achte drauf, dass das nicht aufgerufen wird, bevor das Form angezeigt ist (Handle erstellt)

    Oder eben du folgst wirklich deinem Plan, und sammelst die Daten erst, und ganz am Schluss ins Gui damit.
    Weil derzeit siehts so aus, als ob jedes DatenItem sofort geadded wird, und das mag natürlich zu früh sein.

    Testweise kannste auch ein Thread.Sleep einbauen, um diese Verfrühung mal vorläufig auszuschliessen.

    Weil ich vermute, da werden noch weitere, andere Fehler folgen.

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

    Ok, danke für die info, tatsächlich wird .BeginInvoke nur nicht angezeigt.



    Es läuft momentan so:
    MainForm:
    Form1.Show()
    Form1.SubDasDenThread1Startet()

    Dann kommen die Aufgaben in den Threads...

    In meinem Thread1 führe ich nun folgendes aus:

    VB.NET-Quellcode

    1. Thread.Sleep(4000)
    2. MsgBox("Fenster offen?")
    3. Form1.BeginInvoke(Sub() Form1.TestListView.Items.Add(tempLvItem))


    tempLvItem ist mein einzufügendes ListViewItem. Das Fenster (Form1) ist definitiv offen, das ListView wird schon angezeigt. Dennoch erhalte ich die Meldung System.InvalidOperationException: "Invoke oder BeginInvoke kann für ein Steuerelement erst aufgerufen werden, wenn das Fensterhandle erstellt wurde."

    Das ListView sitzt (gedockt) direkt im Form1.


    Wenn alles schief geht, werde ich einfach in Thread 1 das Form und das Listview programmatisch erzeugen... dann hab ich sicher kein Problem mehr mit den Zugriffen.

    nun, du musst mein Snippet genau lesen: Form1.BeginINvoke ist nicht Me.BeginInvoke!

    Form1.BeginINvoke kann nur failen, weil Form1 ist ein Datentyp, ist aber kein Objekt - denk mal über den Unterschied nach.



    pts schrieb:

    Wenn alles schief geht, werde ich einfach in Thread 1 das Form und das Listview programmatisch erzeugen... dann hab ich sicher kein Problem mehr mit den Zugriffen.
    Wirst dich wundern.
    Aber v.a.: Fang nicht mit iwelchen krickel-krückel-Workarounds an, wo keiner mehr durchblickt, nur weil du den graden, logischen Weg noch nicht hinbekommst.
    Bleib dran am Lernen, wies richtig geht, sonst lernst (und gewöhnst dir an) wie's falsch geht.

    Ach - und beachte, dass dein neuer Fehler ein anderer ist als der Vorherige. Anfänger denken immer "Immer noch der Fehler", aber tatsächlich ists ein großer Fortschritt, wenn man den ersten Fehler überwunden hat, und nun auf den 2. stossen kann.
    Okok, bleiben wir erst beim ersten Lösungsansatz.

    Ich habe jetzt also in Form1.vb folgendes stehen:

    VB.NET-Quellcode

    1. Friend Sub AddToList(LVI As ListViewItem)
    2. MsgBox("Test")
    3. Me.BeginInvoke(Sub() TestListView.Items.Add(LVI))
    4. End Sub


    Die MsgBox sorgt nur für Wartezeit... Aber das Fenster ist immer schon vorher da.

    Das Sub oben wird aufgerufen aus einem anderen Sub, aus einem anderen Thread via:

    VB.NET-Quellcode

    1. fA33Vergleich.AddToList(tempLvItem)


    Als Ergebnis bleibt leider:


    (geschwärzt wegen anderem Variablenname)

    Im Sub des Thread kann ich Me.BeginInvoke nicht nutzen, da das Sub in einem Module sitzt und dort gibt es ja kein Me.

    Oder muss da ein Delegate dazwischen???

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

    Das liegt doch bestimmt wieder an der komischen Auto-Instanzierung von VB.Net, die aus VB6-Relikten übrig blieb.

    pts schrieb:

    MainForm:
    Form1.Show()
    instanziert ein neues Objekt der Klasse Form1 und führt dort .Show aus.

    pts schrieb:

    In Thread1:
    Form1.BeginInvoke(Sub() Form1.TestListView.Items.Add(tempLvItem))
    instanziert ein neues Objekt von Form1 und versucht dort .BeginInvoke auszuführen.
    Das ist aber eine andere Instanz als die in MainForm erzeugte.
    Weil die noch nicht angezeigt wurde, gibt es kein Handle dafür.

    Wenn der Thread in derselben Klasse wie Mainform codiert ist, müsste die besagte Adressierung mit Me funktionieren.
    Wenn der Thread in einer anderen Klasse (oder Modul) codiert ist, funktioniert das Verfahren nicht mehr.
    Ich habe das dann immer so gelöst, dass ich dem aufgerufenen Thread einen Pointer zum Aufrufer mitgegeben habe.

    Heute geht das eleganter mit Async.

    Edit: Ich sehe gerade, dass ihr in der Zwischenzeit einen Schritt weiter seid.
    --
    If Not Program.isWorking Then Code.Debug Else Code.DoNotTouch
    --

    ErfinderDesRades schrieb:

    Jo, dassis jetzt der FolgeFehler, den ich bereits in post#8 vermutete: Du kannst in einem NebenThread auch kein ListViewItem erstellen.
    Also auch das Erstellen von ListViewItem muss im MainThread erfolgen.


    Ok =O Warum den das? Ist es nicht egal, wo ich eine Variable erzeuge?

    VB.NET-Quellcode

    1. Dim tempLvItem As New ListViewItem


    steht in meinem Thread drin.
    Hm ok. Ich persönlich verstehe aber eins noch nicht. Mit dem Code aus dem ersten Post habe ich irgendwo ja schon mal meine ListViewItems reingeschoben, denn TestListView.Items.Count.ToString hat ja die korrekte Anzahl ausgegeben. Wo sind die Daten denn dann gelandet, wenn nicht in meinem ListView?
    Wahrscheinlich in dem anderen Formular, von dem Du glaubst, es sei Dein Hauptformular, es aber nicht ist, wie petaod schon schrieb. Schau mal hier vorbei.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Häufig von mir verwendete Abkürzungen: CEs = control elements (Labels, Buttons, DGVs, ...) und tDS (typisiertes DataSet)
    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht in den Spekulatiusmodus gehen.
    in einem anderen ListView, auf einem anderen Form1, in einem anderen Thread.
    Ich sagte ja: Form1 ist ein Datentyp, und kein Objekt.
    Dummerweise generiert VB doch ein Objekt davon, sodass der Fehler nicht aufschlägt.
    Noch dummererweise, wenn im NebenThread aufgerufen, generiert VB dort auch ein Form1-Objekt.
    Dann hast du 2, komlette Objekte des Typs Form1: Das eine siehste auffm Bildschirm, das andere existiert nur im Nebenthread, unsichtbar, denn niemand hat seine .Show-Methode aufgerufen. :P

    Aber guck auch VaporiZeds Link, da steht dasselbe etwas ausführlicher.
    Ich hab spaßenshalber mal ein Form1.Show() aufgerufen. Da kommt wirklich kurz das Fenster nochmal! Und da ist tatsächlich das ListView noch nicht geladen. Kann also nicht gehen.

    Ok - Datentyp vs Objekt. Kapiert. Wie genau führe ich jetzt aber etwas in meinem wirklichen Form1 aus? Nachdem ich meinen Thread1 gestartet hab, müsste ich warten oder sowas, bis die anderen Fertig melden. Aber dann würde ja das Form1 wieder einfrieren... oder bin ich völlig auf dem falschen Dampfer?
    nein - alles gut. Bis auf dass du im NebenThread ListViewItems erstellst. Erstelle dort andere (Daten-)Objekte, um deine Daten aufzunehmen.
    Und gib diese anderen Objekte zurück an den Mainthread, und erstell im Mainthread aus den Datenobjekten ListviewItem.

    Möglicherweise musst du extra für deine Daten eine geeignete Daten-Klasse erschaffen.

    Wie du siehst, bist du eiglich nahe dran, nur die "neben-frage" in post#16 hat dich vonne Linie abgebracht. Niemand hat gesagt, du müsstest auf irgendetwas warten (ausser eben, dass das Form schoma anzeigt, bevor .BeginInvoke aufgerufen werden kann - weil zuvor hats noch keine Handle).

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