Problem mit Async und ObservableCollection

  • WPF

Es gibt 3 Antworten in diesem Thema. Der letzte Beitrag () ist von kaifreeman.

    Problem mit Async und ObservableCollection

    Hallo Leute,

    ich arbeite mich gerade in das Thema asynchrone Programmierung an (dank dem sehr guten Vorlage Programm von @Nofear23m "WpfNotes2" eigentlich nicht so schwierig).

    Leider scheitere ich gerade an folgendem Szenario:
    Ich habe eine View erzeugt in der alle Einträge einer "Tabelle" in Form eine ICollectionView angezeigt werden. Um einen Eintrag zu editieren wird ein "Edit" Relaycommand ausgeführt, das dann die weiteren Daten für das Objekt selbst lädt.
    Das Objekt selbst (Klasse "User") hat eine n:m Relation zu Usergroups die Join Entity heißt UserUsergroups.

    Code für das Edit Command: (Das Optionale EditItem ist nur eine Möglichkeit der Edit auch ein Item vorzugeben)

    VB.NET-Quellcode

    1. Private Sub EditExecute(Optional EditItem As UserVM = Nothing)
    2. Dim DiaServ = ServiceContainer.GetService(Of IDialogWindowService)
    3. Dim vItemToEdit As UserVM
    4. If EditItem Is Nothing Then
    5. If Users.CurrentItem Is Nothing Then Exit Sub
    6. vItemToEdit = CType(Users.CurrentItem, UserVM)
    7. Else
    8. vItemToEdit = EditItem
    9. End If
    10. 'Load the entire data from Database and after finished show the edit dialog:
    11. If vItemToEdit.LoadData() Then DiaServ.ShowNonModalDialog("edit", New SingleEntityContainerVM(vItemToEdit), Me, False, True)
    12. vItemToEdit.Dispose()
    13. End Sub

    Die Funktion LoadData im UserVM lädt mir dann die restlichen Daten:

    Code für LoadData

    VB.NET-Quellcode

    1. Friend Function LoadData() As Boolean
    2. VMisBusy = True
    3. Dim result = Task.Run(Function() LoadCompleteData()).GetAwaiter.GetResult
    4. UserUsergroups = CollectionViewSource.GetDefaultView(__UserUsergroups)
    5. With UserUsergroups
    6. .MoveCurrentToFirst()
    7. .SortDescriptions.Add(New SortDescription("UsergroupId", ListSortDirection.Ascending))
    8. End With
    9. VMisBusy = False
    10. Return result
    11. End Function


    Die asynchrone Funktion LoadCompleteData:

    VB.NET-Quellcode

    1. Private Async Function LoadCompleteData() As Task(Of Boolean)
    2. Try
    3. If Id = Guid.Empty Then Return False
    4. Dim vResult = Await _BL.GetAllUserDataAsync(_Model, True).ConfigureAwait(True)
    5. If vResult.ErrorStr IsNot Nothing Then Throw New Exception(vResult.ErrorStr)
    6. _Model = vResult._Model
    7. RaisePropertyChanged()
    8. __UserUsergroups?.ToList.ForEach(Sub(x) x.Dispose())
    9. ServiceContainer.GetService(Of IDispatcherService).Invoke(Sub()
    10. If __UserUsergroups Is Nothing Then __UserUsergroups = New ObservableCollection(Of UserUsergroupVM)
    11. _Model.UserUsergroups?.ToList.ForEach(Sub(x) __UserUsergroups.Add(New UserUsergroupVM(x)))
    12. End Sub)
    13. AllAvailableUsergroups.Clear()
    14. Using BL As New UsergroupBL
    15. Dim query = Await BL.GetAllAsync(False).ConfigureAwait(True)
    16. query.ToList.ForEach(Sub(x) AllAvailableUsergroups.Add(New Tuple(Of Guid, String)(x.Id, x.Groupname)))
    17. End Using
    18. AllAvailableTenants.Clear()
    19. Using BL As New TenantBL
    20. Dim query = Await BL.GetAllAsync(False).ConfigureAwait(True)
    21. query.ToList.ForEach(Sub(x) AllAvailableTenants.Add(New Tuple(Of Guid, String)(x.Id, x.CompanyName1)))
    22. End Using
    23. 'Add an empty Tenant:
    24. AllAvailableTenants.Insert(0, New Tuple(Of Guid, String)(Guid.Empty, "---"))
    25. 'Languages:
    26. AllAvailableLanguages.Clear()
    27. AllAvailableLanguages = TranslationManager.Instance.GetAvailableLanguages
    28. Return True
    29. Catch ex As Exception
    30. ServiceContainer.GetService(Of IErrorDialogService).ShowDialog(ex)
    31. Return False
    32. End Try
    33. End Function


    Die Deklaration der Observable Collection und der ICollectionView:

    VB.NET-Quellcode

    1. Private __UserUsergroups As New ObservableCollection(Of UserUsergroupVM)
    2. Dim _UserUsergroups As ICollectionView
    3. Public Property UserUsergroups As ICollectionView
    4. Get
    5. Return _UserUsergroups
    6. End Get
    7. Set(value As ICollectionView)
    8. _UserUsergroups = value
    9. RaisePropertyChanged()
    10. End Set
    11. End Property


    Der Code funktioniert auch im ganzen, die Daten werden geladen und korrekt angezeigt.
    Ich musste relativ lange rumbasteln da ich trotz Dispatcher immer wieder Probleme mit dem GUI Thread bekommen habe.
    Die Lösung war für mich das ich im Constructor die ObservableCollection "vordeklariere"...

    Die ICollectionView ist dann an eine Datagrid gebunden (Screenshot zeigt die Situation in der GUI):


    Will ich jetzt aus dieser ObservableCollection einen Eintrag lösche, fange ich mir mit Interaction die "Del" Taste und übergebe Sie mit EventToCmd an ein Relaycommand meines Viewmodels: (es folgt der Execute Code):

    VB.NET-Quellcode

    1. Private Async Sub DeleteUsergroupExecute()
    2. Dim vIsDeleted As Boolean = Await _BLUserUsergroup.DeleteAsync(CType(UserUsergroups?.CurrentItem, UserUsergroupVM).Id).ConfigureAwait(True)
    3. If vIsDeleted Then
    4. ServiceContainer.GetService(Of IDispatcherService).Invoke(Sub() __UserUsergroups.Remove(CType(UserUsergroups?.CurrentItem, UserUsergroupVM)))
    5. UserUsergroups.MoveCurrentToFirst()
    6. UserUsergroups.Refresh()
    7. End If
    8. End Sub


    und hier beginnt mein Problem, ich erhalte eine:
    System.NullReferenceException: 'Der Objektverweis wurde nicht auf eine Objektinstanz festgelegt.' => die __UserUsergroups ObservableCollection ist "NOTHING" obwohl sie nicht nothing ist und Elemente hält.
    Ich habe versucht auch den Dispatcher wegzulassen, bringt auch nichts.
    Meine Vermutung ist das die __UserUsergroups in einem anderen Thread "wohnt" und ich Sie daher nicht sehe???

    Habe natürlich auch versucht die UserUsergroups ICollectionView einfach nur zu refreshen aber das interessiert die UI auch nicht wirklich.

    Daher die Frage, ist mein asynchrones laden falsch? Mein Asynchrones Löschen??
    Hoffe es kann mir hierbei jemand helfen. Danke :)
    mfG.
    Stephan
    Hallo

    Ich habe den Code jetzt nicht getestet da dies ein wenig viel Aufwand wäre das alles nachzuvollziehen.

    Aber.... bist du sicher das die ObservableCollection das Objekt ist welches Nothing ist?

    Ist das Projekt evtl. auf GitHub oder so damit man das probieren kann?

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Hallo Sascha,

    jup bin mir ziemlich sicher, hab's nochmals getestet:


    Das Project ist über DevOps zugänglich, ich habe mir erlaubt dich dem Viewer Team hinzuzufügen, damit solltest du den kompletten Code sehen (nicht wundern, ich bin beim Projekt eigentlich relativ weit und habe mitten in der Entwicklung entschieden das Async Thema anzugehen...).

    Ich habe jetzt weiter gesucht und bin auf folgenden Umstand gestoßen:
    Innerhalb der Klasse UserVM habe ich eine Dispose Routine eingeführt, diese disposed die Observable Collection, da ich bei einem Neuaufruf der Form sonst einen Fehler bekomme (Änderungen an der UI vom Background Thead nicht zulässig nur mit Dispatcher usw.)
    Dieser Dispose Umstand killt mir defacto die Collection ohne Grund. Um es zu vermeiden habe ich eine zusätzliche HelferListe "vHelper" in meine LoadCompleteData Routine integriert. Damit konnte ich zumindest sicherstellen das ich sowohl das Fenster ohne Fehler aufbekomme als auch die Collection korrekt geladen wird.

    VB.NET-Quellcode

    1. Dim vHelper As New List(Of UserUsergroupVM)
    2. _Model.UserUsergroups?.ToList.ForEach(Sub(x) vHelper.Add(New UserUsergroupVM(x)))
    3. __UserUsergroups = New ObservableCollection(Of UserUsergroupVM)(vHelper)
    4. UserUsergroups = CollectionViewSource.GetDefaultView(__UserUsergroups)
    5. With UserUsergroups
    6. .MoveCurrentToFirst()
    7. .SortDescriptions.Add(New SortDescription("UsergroupId", ListSortDirection.Ascending))
    8. End With


    Leider funktioniert jetzt weder Add (über das Datagrid neue Zeile hinzufügen) noch Delete mit Abfangen des Entf Buttons:
    "Von diesem CollectionView-Typ werden keine Änderungen der "SourceCollection" unterstützt, wenn diese nicht von einem Dispatcher-Thread aus erfolgen.'"

    Die Delete Funktion ruft aber das Remove Event der ObservableCollection über einen Dispatcher auf....

    VB.NET-Quellcode

    1. Private Async Sub DeleteUsergroupExecute()
    2. Dim CurrentItem As UserUsergroupVM = CType(UserUsergroups?.CurrentItem, UserUsergroupVM)
    3. ServiceContainer.GetService(Of IDispatcherService).Invoke(Sub() __UserUsergroups.Remove(CurrentItem))
    4. End Sub



    Edit 2:
    Weiter rumgebastelt an dem Thema, was mir auffällt ist das das Problem immer dann auftritt wenn man das Objekt zuvor mittels einer Asynchronen Sub in eine ObservableCollection lädt und dann das Objekt in eine weitere View gibt.
    Also klassisch => Übersichtsliste async laden => Item auswählen => Edit Command ausführen und Navigation Properties laden => Bearbeitung dieser ObservableCollection löst dann den Fehler aus.

    Kann es sein das man zwingend folgendes tun muss:
    Übersicht async laden => Edit Command ausführen, Objekt komplett neu aus der DB laden (via ID) dann Nav Properties laden => Bearbeitung beenden => Übersicht neu laden??
    mfG.
    Stephan

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

    so ich geh mich jetzt anzünden, habe echt 2 Wochen damit verballert den Fehler zu finden...

    Das Problem soweit ich es verstehen kann:
    "MultiView" Asynchron geladen, hier der ICollectionView innerhalb der Asychronen Function bereits die Werte zugewiesen, das führte zum Fehler.
    geändert auf:
    Multiview asychron laden, ICollectionView Zuweisung im synchronen Thread => zack funktioniert.

    Danke für eure Hilfe.
    mfG.
    Stephan