Multithreading - MVVM - Problem mit Invoke bzw. EnableCollectionSynchronization

  • WPF

Es gibt 13 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

    Multithreading - MVVM - Problem mit Invoke bzw. EnableCollectionSynchronization

    Guten Abend,

    stehe gerade vor dem Problem das ich einen Zeitintensiven Suchvorgang >5sec in einen Thread auslagern will, nebenbei soll ein Busyindicator aus dem WPF Toolkit zum Einsatz kommen:

    Here we go:

    XAML der gebundenen Form:

    XML-Quellcode

    1. <xctk:BusyIndicator IsBusy="{Binding is_busy, Mode=OneWay, NotifyOnTargetUpdated=True, NotifyOnSourceUpdated=True, ValidatesOnNotifyDataErrors=False}" BusyContent="Suche wird durchgeführt..." DisplayAfter="0">
    2. ... hier ist ein Datagrid "verbaut"
    3. </xctk:BusyIndicator>


    Die IsBusy Eigenschaft des Inidcators triggered seinen Sichtbarkeit diese ist gebunden an mein Objekt mit der Boolean Property is_busy => funktioniert einwandfrei.
    Leider blockiert mir meine Suche weiterhin den GUI Thread so das der Indicator nicht angezeigt wird:

    VB.NET-Quellcode

    1. Private Sub SearchExecute(v_search As searchdefinition_v01)
    2. v_search.is_busy = True
    3. v_search.SearchResults = New ListCollectionView(__transactions)
    4. If v_search.SearchResults.CanFilter Then
    5. Dim thr As New Thread(Sub(p) SearchExecuteforThread(v_search))
    6. With thr
    7. .Priority = ThreadPriority.Normal
    8. .SetApartmentState(ApartmentState.STA)
    9. .Start()
    10. End With
    11. If thr.ThreadState = ThreadState.Stopped Then
    12. v_search.is_busy = False
    13. End If
    14. Else
    15. v_search.SearchResults = Nothing
    16. End If
    17. End Sub


    Improvisierte Sub für den Thread:

    VB.NET-Quellcode

    1. Private Sub SearchExecuteforThread(v_search As searchdefinition_v01)
    2. Dim fil_search As New cls_search_filters(v_search)
    3. Application.Current.Dispatcher.Invoke(New Action(Sub()
    4. v_search.SearchResults.Filter = New Predicate(Of Object)(AddressOf fil_search.SearchExecuteFilter)
    5. v_search.SearchResults.SortDescriptions.Add(New SortDescription With {.PropertyName = "datum", .Direction = ListSortDirection.Ascending})
    6. End Sub))
    7. End Sub


    Der Code läuft eigentlich durch lt. meinen Debug Print wird auch die is_busy Property geändert aber leider wird der BusyIndicator nicht angezeigt.

    Ziel der gesamten Geschicht ist es:
    User ruft Suchform auf, definiert die Suche führt Suche aus, BusyIndicator wird eingeblendet, Suche läuft im Hintergrund, Wenn Suche erledigt dann BusyIndicator ausblenden.

    Hat jemand einen Tipp für mich?
    Danke
    mfG.
    Stephan

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

    Der Thread arbeitet er filtert ganz schön die Ergebnisse, sobald der Thread abgeschlossen ist zeigt er mir auch den BusyIndicator an aber nicht vorher.
    (Ich habe absichtlich im Dispatcher die Funktion is_busy nicht auf False gesetzt damit ich sehe wann er den BusyIndicator einblendet).
    mfG.
    Stephan
    Bei solchen Sachen gebe ich ganz gerne Debug-Informationen mit der Thread-ID aus:

    VB.NET-Quellcode

    1. Debug.Print(System.Threading.Thread.CurrentThread.ManagedId & ": Filtering Start")

    (Der Teil CurrentThread.ManagedId ist aus dem Kopf geschrieben)

    Wenn Du das machst, wirst Du feststellen, dass der Code, der filtert, im GUI-Thread ausgeführt wird.
    Der Grund:

    VB.NET-Quellcode

    1. Sub Foo()
    2. 'Dieser Code wird im Hintergrund-Thread ausgeführt.
    3. Dispatcher.Invoke(Sub()
    4. 'Dieser Code wird wieder im GUI-Thread ausgeführt.
    5. Anzeige = Filtern()
    6. End Sub)
    7. End Sub

    Dein Filter-Code steht in der Lambdafunktion drin, und die wird im GUI-Thread ausgeführt.
    Wie Bluespide richtig erkannt hat macht der Hintergrund-Thread nichts anderes, als sofort zu invoken und den GUI-Thread anzuweisen, die komplexen Berechnungen auszuführen.
    Schematisch müsste der Code so aussehen:

    VB.NET-Quellcode

    1. Sub Foo()
    2. 'Dieser Code wird im Hintergrund-Thread ausgeführt.
    3. Dim Ergebnis = Filtern()
    4. Dispatcher.Invoke(Sub()
    5. 'Dieser Code wird wieder im GUI-Thread ausgeführt.
    6. Anzeige = Ergebnis
    7. End Sub)
    8. End Sub

    Dadurch wird im Hintergrund-Thread gefiltert. Das Ergebnis wird dann vom GUI-Thread zur Anzeige gebracht.
    "Luckily luh... luckily it wasn't poi-"
    -- Brady in Wonderland, 23. Februar 2015, 1:56
    Desktop Pinner | ApplicationSettings | OnUtils
    hm ok das leuchtet ein und das Debugging bestätigt es aber die Umsetzung will mir nicht ganz gelingen:

    VB.NET-Quellcode

    1. Private Sub SearchExecuteforThread(v_search As searchdefinition_v01)
    2. Debug.Print(System.Threading.Thread.CurrentThread.ManagedThreadId & ": Filtering Start")
    3. Dim fil_search As New cls_search_filters(v_search)
    4. v_search.SearchResults = New ListCollectionView(__transactions)
    5. Dim v_test = New Predicate(Of Object)(AddressOf fil_search.SearchExecuteFilter)
    6. Application.Current.Dispatcher.Invoke(New Action(Sub()
    7. Debug.Print(System.Threading.Thread.CurrentThread.ManagedThreadId & ": Filtering Start")
    8. v_search.SearchResults.Filter = v_test
    9. v_search.SearchResults.SortDescriptions.Add(New SortDescription With {.PropertyName = "datum", .Direction = ListSortDirection.Ascending})
    10. v_search.is_busy = False
    11. End Sub))
    12. End Sub


    Problem ist ja sobald ich v_search.SearchResults.Filter ausserhalb des Invoke ausführen versuche hängt er sich auf weil er von einem Thread auf den anderen greifen möchte.
    Der obige Code funktioniert zwar löst aber das Problem nicht eine andere Variante wäre mir jetzt nicht einfallen (also Filter ohne das eigentliche Objekt)
    mfG.
    Stephan
    Hm.
    Das ist natürlich doof.

    Man müsste entweder die Filterung aus dem v_search Dingsda rausholen oder das v_search Dingsda selber Multithreading verwenden lassen.
    Könntest Du genauer erklären, was v_search ist und wie die Filterung abläuft? Es erscheint mir nämlich von der Architektur her etwas komisch, dass nur das Zuweisen von Properties das Filtern auslöst. Properties sollen "schnell" sein. Siehe dazu auch: ericlippert.com/2014/05/19/when-should-i-write-a-property/
    "Luckily luh... luckily it wasn't poi-"
    -- Brady in Wonderland, 23. Februar 2015, 1:56
    Desktop Pinner | ApplicationSettings | OnUtils
    Dann lass den Thread halt weg - das ist das sicherste.
    Dein Thread tut ja eh so gut wie gar nichts - er erzeugt nur ein ListCollectionView, übergibt dem das __transaction-Dingens (was immer das sein mag), und setzt dann einen Filter.
    nichts davon sollte eiglich Zeitaufwändig sein.
    Aber kommt auch auf das __transaction-dingens an (was immer das sein mag). Am Ende ist das eine Liste von 1 Mio Elementen, und die anzuzeigen dauert eben eine Weile, und das kann auch nicht gethreaded werden, denn es geht ja ums Anzeigen - muss also im Gui-Thread stattfinden.
    OK ich beginne mal von unten nach Oben. Das __transaction Dingens ist die ObservableCollection einer EntityFramework Entität. Dieses beinhaltet dzt. ca 1800 Datensätze Tendenz relativ stark steigend weil das Programm gedacht ist genau diese Transaktionen zu verwalten.
    Das Problem warum ich einen Thread wollte ist weil das Filtern von dzt. 1800 DS ca. 8 Sekunden dauert und der Benutzer nicht das geringste Feedback erhält, mit dem BusyIndicator will ich das Problem erledigen damit mir der User nicht "dumm" stirbt und Panik bekommt das das Programm abstürzt. Wenn es da eine Lösung gebe die mir den BusyIndicator holt ohne Threading bin ich auch glücklich :)

    So hier mein "Filtermonster" zur Erklärung:
    Mit dieser Filterklasse kann der User sich seine Suche zusammenbasteln, das betrifft verschiedene Entitäten darum ist es relativ komplex geworden (wahrscheinlich auch deshalb langsam)

    VB.NET-Quellcode

    1. ''' <summary>
    2. ''' Klasse die eine Vorbereitung ist für ein Suchergebnis im Konstruktor wird direkt das Suchobjekt mit allen Filtereinstellungen übergeben.
    3. ''' </summary>
    4. Public Class cls_search_filters
    5. Dim v_searchdefinition As searchdefinition_v01
    6. Sub New(Search As searchdefinition_v01)
    7. v_searchdefinition = Search
    8. End Sub
    9. Public Function SearchExecuteFilter(v_obj As Object) As Boolean
    10. 'Filter arbeitet die einzelnen Filtermöglichkeiten ab.
    11. 'Die Filtermöglichkeiten sind vordefiniert mit and verknüpft d.h. es muss jede Bedingung erfüllt sein.
    12. Dim v_state As Boolean = True
    13. If v_obj Is Nothing Then Return False
    14. Dim v_trans As transactions_v01 = DirectCast(v_obj, transactions_v01)
    15. #Region "Zeitraum"
    16. If v_searchdefinition.aktiv_time Then
    17. If v_trans.datum >= v_searchdefinition.search_time_from And v_trans.datum <= v_searchdefinition.search_time_to Then
    18. Else
    19. v_state = False
    20. End If
    21. End If
    22. #End Region
    23. If v_state = False Then Return False
    24. 'Betrag
    25. If v_searchdefinition.aktiv_value Then
    26. If v_trans.totamount >= v_searchdefinition.search_value_from And v_trans.totamount <= v_searchdefinition.search_value_to Then
    27. Else
    28. v_state = False
    29. End If
    30. End If
    31. If v_state = False Then Return False
    32. 'Bemerkung:
    33. If v_searchdefinition.aktiv_note Then
    34. 'Exakte Suche oder nicht:
    35. If v_trans.comment Is Nothing Then
    36. v_state = False
    37. Else
    38. If v_searchdefinition.search_note_exact Then
    39. 'Exakte Übereinstimmung:
    40. If Not v_trans.comment Like v_searchdefinition.search_note Then v_state = False
    41. Else
    42. 'Beinhaltet:
    43. 'V 1.0.0.35 Änderung auf Tolower um Klein Groß Schreibung zu ignorieren
    44. If Not v_trans.comment.ToLower.Contains(v_searchdefinition.search_note.ToLower) Then v_state = False
    45. End If
    46. End If
    47. End If
    48. If v_state = False Then Return False
    49. 'Status:
    50. If v_searchdefinition.nv_search_2_status.Count > 0 Then
    51. If Not v_searchdefinition.nv_search_2_status.Where(Function(x) CBool(x.status_id = v_trans.transstatus_id)).Count > 0 Then v_state = False
    52. End If
    53. If v_state = False Then Return False
    54. 'Typ:
    55. If v_searchdefinition.nv_search_2_typ.Count > 0 Then
    56. If Not v_searchdefinition.nv_search_2_typ.Where(Function(x) CBool(x.typ_id = v_trans.transtyp_id)).Count > 0 Then v_state = False
    57. End If
    58. If v_state = False Then Return False
    59. 'Konto:
    60. If v_searchdefinition.nv_search_2_konto.Count > 0 Then
    61. If Not v_searchdefinition.nv_search_2_konto.Where(Function(x) CBool(x.kto_id = v_trans.konto_id)).Count > 0 Then v_state = False
    62. End If
    63. If v_state = False Then Return False
    64. 'Empfänger:
    65. If v_searchdefinition.nv_search_2_receiver.Count > 0 Then
    66. If v_trans.receiver_id Is Nothing Then
    67. v_state = False
    68. Else
    69. If Not v_searchdefinition.nv_search_2_receiver.Where(Function(x) CBool(x.rec_id = v_trans.receiver_id)).Count > 0 Then v_state = False
    70. End If
    71. End If
    72. If v_state = False Then Return False
    73. 'Person:
    74. If v_searchdefinition.nv_search_2_person.Count > 0 Then
    75. If v_trans.person_id Is Nothing Then
    76. v_state = False
    77. Else
    78. If Not v_searchdefinition.nv_search_2_person.Where(Function(x) CBool(x.pers_id = v_trans.person_id)).Count > 0 Then v_state = False
    79. End If
    80. End If
    81. If v_state = False Then Return False
    82. 'Kategorie:
    83. If v_searchdefinition.nv_search_2_category.Count > 0 Then
    84. If v_trans.nv_trans_t2k.Count = 0 Then
    85. v_state = False
    86. Else
    87. Debug.Print("Durchlauf")
    88. For Each v_searchitem As searchitem_category In v_searchdefinition.nv_search_2_category
    89. If Not v_trans.nv_trans_t2k.Where(Function(x) CBool(x.category_id = v_searchitem.cat_id)).Count > 0 Then
    90. v_state = False
    91. Else
    92. v_state = True
    93. Exit For
    94. End If
    95. Next
    96. End If
    97. End If
    98. Return v_state
    99. End Function
    100. End Class
    mfG.
    Stephan
    Der Code ist zwar sehr umständlich, aber ich kann mir eiglich nicht vorstellen, dass 1800 Datensätze damit zu filtern 8s braucht.

    Kannst du das iwie testen, wenn die Oberfläche abgekoppelt ist?
    Ich denke eher, du hast da vlt. ein sehr komplexes DG dran gebunden, was sich überaus langsam aufbaut.
    Ich habs jetzt mal getestet und hab bemerkt das meine Value Converter gewaltig Leistung ziehen (wird benutzt wenn ein Bild nicht vorhanden ist also Byte() leer ist), die habe ich jetzt in die Klassen ausgelagert und es geht jetzt schneller.

    Trotzdem bleibt für mich die Frage wie ich das mit dem Busyindicator schaffe was ist wenn mein User Hausnummer 10k Datensätze hat? Eine Info vom Programm wäre doch iwi ganz nett.
    mfG.
    Stephan
    Jo, beim Einbinden von Bildern kann man auch jede Menge falsch machen (etwa jedes Bild bei jedem Abruf neu von Platte laden und so Scherze).

    Und ein (Standard-)BusyIndikator ist nur einsetzbar, wenn deine langlaufenden Vorgänge auch wirklich nebenläufig organisierbar sind. Bislang siehts ja nicht so aus.
    Gut das mach ich nicht ich hab ein Mikro 16x16 Bildchen das einmal von *.png aus den Resourcen in ein Byte() umgewandelt wird und fertig.

    Aber mal ne Grundsatzfrage ListCollectionView baut ja in meinem Fall auf die ObservableCollection aus dem EF auf mit der CollectionView steuere ich verschiedene Views z.b. Alle Datensätze, oder inaktive Datensätze (Trigger)
    das mache ich mit filtern und Sortierungen als Hobbybastler stellt sich mir dann die Frage wie würde ein Profi die Sache machen ich kann ja nicht davon ausgehen das ich immer nur kleine Datasets habe wie kann ich das Filtern realisieren ohne das es im GUI Thread läuft? Warum wird .filter überhaupt im GUI aufgerufen eigentlich brauche ich ja nicht das filtern selbst sondern nur den gefilterten Output? Da knackt es bei mir gerade gewaltig....
    mfG.
    Stephan
    Nö - macht man glaub schon so.
    In Wpf ist CollectionView vorgesehen, den Datenfluss vom/zum Viewmodel zu sortieren/filtern - vergleichbar der BindingSource in Winforms.
    Wenns aber zu viele Daten werden, dann filtert man nicht mehr inne CV, sondern dann muss man gleich anne Abfrage filtern - also dass gar nicht mehr Daten abgerufen werden als anzuzeigen sind. Das wird dann aber schon komplizierter, umzusetzen.
    Kenne ich mich auch nicht wirklich aus, evtl. kann man sich da das Lebern erleichtern, wenn man EF-Klassen verwendet, die IQueriable implementieren.
    Aber wirklich wissen tu ich das nicht, evtl. ist das mit IQueryable auch nur ein Hype, der letztlich nicht soo viel bringt.

    Jedenfalls 8s für 2000 Datensätze ist zu lahm, und du solltest unbedingt herausfinden, wo es lahmt: Beim Daten abruf, beim Filtern, beim Aufbau des Views.