Unit of Work und Repository - GetAll Methode mit generischer Klasse und Filter

  • VB.NET

Es gibt 61 Antworten in diesem Thema. Der letzte Beitrag () ist von Nofear23m.

    Unit of Work und Repository - GetAll Methode mit generischer Klasse und Filter

    Abend,

    ich spiele mich gerade mit dem UnitofWork und Repository Pattern die Implementierung funktioniert eigentlich ganz gut und tut was es soll.
    Aktuell habe ich eine "GetAll" Methode welche Daten aus der DB liest und als ObservableCollection zurückgibt:

    Hier mal das Interface vom Repository (nur mit der Relevanten Function):
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Imports System
    2. Imports System.Collections.Generic
    3. Imports System.Collections.ObjectModel
    4. Imports System.Linq.Expressions
    5. Namespace Core.Repositories
    6. Public Interface IRepository(Of TEntity As Class)
    7. Function GetAll() As ObservableCollection(Of TEntity)
    8. End Interface
    9. End Namespace



    Dann noch das eigentliche Repository (nur mit der relevanten Function):
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Imports System
    2. Imports System.Collections.Generic
    3. Imports System.Collections.ObjectModel
    4. Imports System.Data.Entity
    5. Imports System.Linq
    6. Imports System.Linq.Expressions
    7. Imports MyERP.Core.Repositories
    8. Imports System.Reflection
    9. Namespace Persistence.Repositories
    10. Public Class Repository(Of TEntity As Class)
    11. Implements IRepository(Of TEntity)
    12. Protected ReadOnly Context As DbContext
    13. Public Sub New(ByVal _context As DbContext)
    14. Context = _context
    15. End Sub
    16. Public Function GetAll() As ObservableCollection(Of TEntity) Implements IRepository(Of TEntity).GetAll
    17. Return New ObservableCollection(Of TEntity)(Context.[Set](Of TEntity)())
    18. End Function
    19. End Class
    20. End Namespace



    In meinem Modell verwende ich angelehnt an das "Homestorage" Projekt ein IDelete Interface welches ein logisches löschen (setzen einer Property IsDeleted = True) übernimmt.

    Jetzt hätte ich gerne meine GetAll Methode erweitern damit ich auswählen kann ob "nur aktive", "nur gelöschte" oder "alle" Items einer Entität geholt werden sollen.

    Mein Versuch (hier nur mit einer Boolean Variable für IsDeleted Änderung im Repository ist durchgeführt aber nicht abgebildet um den Text kurz zu halten:

    VB.NET-Quellcode

    1. Public Function GetAll(Optional IsDeleted as Boolean = False) As ObservableCollection(Of TEntity) Implements IRepository(Of TEntity).GetAll
    2. if IsDeleted then
    3. Return New ObservableCollection(Of TEntity)(Context.[Set](Of TEntity)(function(x) x.Deleted = False))
    4. else
    5. Return New ObservableCollection(Of TEntity)(Context.[Set](Of TEntity)())
    6. end if
    7. End Function


    Natürlich klappt das nicht weil die Property Deleted nicht in meiner generischen Klasse TEntity zu finden sein kann.
    Normalerweise würde ich jetzt prüfen ob die Entität das IDelete Interface implementiert hat dann danach Casten und meine Where Clause zusammenbauen.
    Aber TEntity hat kein GetType, ich kann es auch nicht direkt als Type benutzen und somit stehe ich gerade ziemlich an....

    Daher zu meiner Frage, gibt es eine Möglichkeit das auf Repository Ebene zu schaffen oder muss ich mein, ich nenne es mal, typisiertes GetAll in alle typisierten Repositories einbauen? (Da klappt es ja auch weil TEntity dort eben schon das Objekt ist).

    *Topic verschoben*
    mfG.
    Stephan

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „Marcus Gräfe“ ()

    ähm - das letzte Snipped dürfte garnet kompilieren (oder?).
    Weil die Signatur der Implementation von .GetAll stimmt nicht mit der vom Interface vorgegebenen überein.

    Was du angibst, warum es nicht klappt, kannich daher nicht nachvollziehen.

    Eine annere Frage wäre auch, was du mit dem Interface bezweckst.
    Und (falls der Zweck einen Sinn ergibt) ob man dasselbe nicht eleganter mit einer Basisklasse umsetzen kann.
    Hallo @kaifreeman

    Ja, das ist nicht so einfach innerhalb der Generischen Klasse.
    Was mir aber in Auge sticht:

    Warum gibt die GetAll Methode eigendlich eine ObservableCollection zurück und kein IQueryable(Of T)??
    Damit nimmst du dir alle möglichkeiten die Daten zu Filtern, zu sotieren, paging und noch vieles mehr.
    Du würdest also IMMER ALLE DATENSÄTZE laden, was sehr schlecht ist.

    Weiters würdest du mit deiner implementierung immer entweder die gelöschten holen oder immer nur die nicht gelöschten.
    Normalerweise ruft man eigendlich per Default immer alle nicht gelöschten ab und im bedarfsfall ALLE Datensötze inkl. der gelöschten.

    Angelehnt an den Code im HomeStorage Projekt habe ich folgendes versucht mit meiner Logik des abrufens inkl. gelöschten wenn notwendig.
    Das Compiliert auch. ICh habe jetzt keine Abfrage abgeschickt, aber warum sollte das nicht funzen.

    VB.NET-Quellcode

    1. Public Overridable Function GetAll(Optional tracking As Boolean = False, Optional IncludeDeleted As Boolean = False) As IQueryable(Of T) Implements IGenericRepository(Of T).GetAll
    2. Dim query As IQueryable(Of T) = CType(_entities, StorageContext).[Set](Of T)()
    3. If Not IncludeDeleted Then
    4. If GetType(Model.Interfaces.ILogicalDelete).IsAssignableFrom(GetType(T)) Then
    5. query = query.Where(Function(x) DirectCast(x, Model.Interfaces.ILogicalDelete).DeletedFlag = False)
    6. End If
    7. End If
    8. If tracking Then query = query.AsTracking
    9. Return query
    10. End Function


    Hoffe das Hilt dir. In C# darfst du es selbst übersetzen ^^
    Grüße
    Sascha

    Edit:

    ErfinderDesRades schrieb:

    Eine annere Frage wäre auch, was du mit dem Interface bezweckst.
    Und (falls der Zweck einen Sinn ergibt) ob man dasselbe nicht eleganter mit einer Basisklasse umsetzen kann.


    Damit der DBContext in der SaveChanges Methode weis ob die Entität gelöscht werden darf oder diese nur als gelöscht markiert werden kann.
    Das ergibt also schon sinn.
    Ginge vermutlich mit einer Basisklasse auch, in der Regel nimmt man für sowas aber ein Interface oder irre ich.
    Ich finde es angenehmer, ich kann so in jeder Modelklasse von meiner Basisklasse erben und dann gewisse Funktionalitäten hereinholen und muss mir nicht merken welche Basisklasse jetzt welche Properties enthält da man ja immer nur von einer Basisklasse erben kann. Will ich die möglichkeit haben das löschen verboten werden soll implementiere ich ILogicalDelete. Soll eine Entität PRotokollierbar sein implementiere ich IProtocolable. usw.
    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. ##

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

    Hallo Zusammen,

    erstmal danke für die Antworten.
    @ErfinderDesRades
    Nein kompiliert auch nicht, war eigentlich nur gedacht um das Interface darzustellen. Nachdem die Lösung aber sowieso nicht kompilierte war mir das jetzt nicht so wichtig.
    Nja das mit dem Interface ist so eine Geschichte, ich hab mir einen Udemy Kurs angesehen und dort wurde das so erklärt. Die Implementierung mit den Interfaces erscheint mir auch etwas kompliziert immerhin muss man für jede Entität nebst einem eigenen Repository noch ein Interface anlegen...
    Da es mir aktuell mehr um das Gesamtverständnis geht habe ich es so gelassen sollte aber definitiv besser zu lösen sein.

    @Nofear23m
    Danke für deine Antwort das hat mein Problem gelöst.
    Das die GetAll kein IQueryable zurückgibt ist eigentlich wirklich nicht schlau.... Wie gesagt ich beginne gerade UnitofWork und Repositories zu verstehen, ich werd es auf IQueryable umsetzen das sollte dann auch mein Filterproblem lösen. Danke für den Tipp.

    Anbei meine Implementierung im Endeffekt 1:1 das von dir übernommen und angepasst.

    VB.NET-Quellcode

    1. Public Function GetAll(Optional IncludeDeleted As Boolean = False) As IQueryable(Of TEntity) Implements IRepository(Of TEntity).GetAll
    2. Dim Query As IQueryable(Of TEntity) = Context.Set(Of TEntity)
    3. If Not IncludeDeleted Then
    4. If GetType(IDelete).IsAssignableFrom(GetType(TEntity)) Then
    5. Query = Query.Where(Function(x) DirectCast(x, IDelete).Deleted = False)
    6. End If
    7. End If
    8. Return Query
    9. End Function


    C# kann ich mir sparen, ich arbeite nur in vb.net hoffentlich schaut mein Code nicht so grottig aus das er mit C# verwechselt wird. :P

    Nachtrag:
    Blöde Frage aber ich habe es jetzt so umgebaut das ein IQueryable zurückgegeben wird aber in eine ObservableCollection oder in eine Listcollection krieg ich das nicht konvertiert?

    Unable to cast the type 'MyERP.Currency' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.'


    Nachtrag 2:
    Wenn ich die Query ohne die Where erstelle (also IncludeDeleted auf True) dann funktioniert es...
    Es ist auch egal ob ich die Query schon im Context.Set(of TEntity) mit .Where erweitere....
    Anscheinend ist das "DirectCast" für die Interface abfrage das Problem... wenn ich nur "Function(x) x isnot nothing" als Where übergebe ist es ok....
    mfG.
    Stephan

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

    kaifreeman schrieb:

    ich hab mir einen Udemy Kurs angesehen und dort wurde das so erklärt.
    wie erklärt?
    Evtl. wurde es dort nur so vorgetanzt, ohne Erklärung?

    kaifreeman schrieb:

    mir aktuell mehr um das Gesamtverständnis geht
    Tipp: Achte sorgfältig darauf, was erklärt wurde, (also so, dass du, dem es ums Verständnis geht, es auch verstehst), und was nicht, also was dir (noch?) unklar ist, auch wenn du es schon nachtanzen kannst.



    Nofear23m schrieb:

    Damit der DBContext in der SaveChanges Methode weis ob die Entität gelöscht werden darf oder diese nur als gelöscht markiert werden kann.
    Hey - das verstehe ich sogar ansatzweise.
    Also als Erklärung eines IDelete-Interfaces ergäbe es mir Sinn - auch wenn ich persönlich bislang nie solche Bedürfnisse gehabt hab.
    Ich würde wohl einfach nicht Deleten, wenn ich nicht deleten will. Ein Delete aufrufen, was dann in wirklichkeit nur ein "gelöscht"-Flag setzt - hmm, hmm - kann evtl. auch Verwirrung stiften.
    Und Frage malwieder, ob mans denn konkret braucht.

    Allerdings bezog sich meine Eingangsfrage aufs IRepository(of T)-Interface.
    Ich bastel ja auch so das eine oder annere Repository, aber denen lasse ich jeweils die Methoden einfach angedeihen, die ich brauche, ohne da ein Interface zu implementieren.

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

    Hallo

    kaifreeman schrieb:

    Die Implementierung mit den Interfaces erscheint mir auch etwas kompliziert immerhin muss man für jede Entität nebst einem eigenen Repository noch ein Interface anlegen...

    Habe ich eh geschrieben warum ich es mit Interfaces gemacht habe. Und stimmt, das ist so üblich da man ja sonst zig Basisklassen hätte und die Benamsung dieser ist das auch meisst nicht die beste.

    kaifreeman schrieb:

    C# kann ich mir sparen, ich arbeite nur in vb.net hoffentlich schaut mein Code nicht so grottig aus das er mit C# verwechselt wird.

    Oh, sorry, habe ich dich verwechselt, habe beim Antworten nciht mehr auf den Code geguckt.

    kaifreeman schrieb:

    Blöde Frage aber ich habe es jetzt so umgebaut das ein IQueryable zurückgegeben wird aber in eine ObservableCollection oder in eine Listcollection krieg ich das nicht konvertiert?

    Und wo tritt der Fehler auf - bei welchem Code.

    Du bekommst nun immer ein IQueryable zurück. Das Bedeutet das du nun dort wo du dein Repository verwendest auch entscheiden kann WANN die Abfrage an die DB geht.
    Beispiel:

    VB.NET-Quellcode

    1. Using rep As New DAL.GenericRepository(Of Article)
    2. Dim query = rep.GetAll() 'Alle selektieren - Es findet kein DB Routrip statt
    3. query = query.Where(Function(x) x.Code.StartsWith("123")) 'Nur die Artikel wo der Barcode mit 123 beginnt - Es findet kein Routrip zur DB statt
    4. query = query.Where(Function(x) x.CanExpire) 'Nur die Artikel welche ablaufen können - Es findet kein Routrip zur DB statt
    5. query = query.OrderBy(Function(x) x.Stock) 'Nun sortieren nach Lagermenge - Es findet kein Routrip zur DB statt
    6. query = query.Select(Function(x) New Article() With {.ArticleId = x.ArticleId, .Title = x.Title}) 'Nur die Spalten ID und Title abrufen (Performancethema) - Es findet kein Routrip zur DB statt
    7. query = query.Take(10) 'Wir wollen nur die ersten 10 Ergebnisse - Es findet kein Routrip zur DB statt
    8. Dim result As List(Of Article) = query.ToList 'HIER findet nun der Routrip zur DB statt
    9. 'Alle gefundenen Artikel (Nur Titel) ausgeben
    10. For Each a In result
    11. Console.Write(a.Title)
    12. Next
    13. End Using


    Das würde nun alle Artikel abrufen wo der Barcode mit '123' beginnt UND der Artikel ablaufen kann. Der SQL würde nur die Spalten ID und Title der ersten 10 Artikel sortiert nach Lagermenge zurückgeben.

    Das zeigt auch das an den SQL Server abgesette Query:

    SQL-Abfrage

    1. SELECT TOP(10 /* @__p_0 */) [x].[ArticleId],
    2. [x].[Title]
    3. FROM [Articles] AS [x]
    4. WHERE (([x].[DeletedFlag] = 0)
    5. AND ([x].[Code] LIKE N'123' + N'%'
    6. AND (LEFT([x].[Code], LEN(N'123')) = N'123')))
    7. AND ([x].[CanExpire] = 1)
    8. ORDER BY [x].[Stock]


    Hoffe damit wurde es klarer.
    Grüße
    Sascha

    Edit:
    @ErfinderDesRades
    Kannst du mir erklären wie DU es denn machen würdest? Du bist auf meine Erklärung nicht eingegangen und gehst trotzdem auf @kaifreeman los. Warum? Was hast du dagegen das er es so macht? Das möchte ich bitte schon begründet haben. Hast ja editiert
    Meine Antwort darauf:

    ErfinderDesRades schrieb:

    Ich würde wohl einfach nicht Deleten, wenn ich nicht deleten will. Ein Delete aufrufen, was dann in wirklichkeit nur ein "gelöscht"-Flag setzt

    Ist aber nun mal so üblich. Man soll ja löschen können. Es ist für den User in dem Moment ja auch weg. Nur soll es immer möglich sein dies rückgängig zu machen. Was ist wenn es keine Ansicht war oder der falsche Kunde gelöscht wurde?
    Es gibt zig einsatzmöglichkeiten und Gründe. Eine anständige Applikation unterstützt einfach nicht nur das Rückgägigmachen von "Löschvorgängen" sonder auch das Protokollieren von änderungen. Stichwort Auditierung.

    ErfinderDesRades schrieb:

    Ich bastel ja auch so das eine oder annere Repository, aber denen lasse ich jeweils die Methoden einfach angedeihen, die ich brauche, ohne da ein Interface zu implementieren.

    Naja, da du (wie ich von dir selbst erfahren habe) keine Unittests schreibst ist das auch OK. Mit dem ersten Test welchen du (korrekt) schreiben möchtest, stolpert man dann heftigst. :S

    Grüße
    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. ##

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

    Hallo Sascha,
    Danke für deine Erklärung das ganze ist damit eigentlich verständlich. Ich sehe gerade das ich durch diese Implementierung einiges im Projekt ändern muss.
    Bei der GetAll habe ich das Problem jetzt auf die Zeile eingrenzen können:

    VB.NET-Quellcode

    1. Query = Query.Where(Function(x) DirectCast(x, IDelete).Deleted = False)

    Wenn ich mit dem Debugger das ganze mitverfolge wird direkt in derZeile ein Error ausgelöst:
    'DirectCast(Query, System.Data.Entity.Infrastructure.DbQuery(Of MyERP.Currency)).Sql' threw an exception of type 'System.NotSupportedException'
    Anscheinend ist das DirectCast(x,IDelete) das Problem. Mein Interface ist aber gleich wie bei deinem Homestorage Projekt aufgebaut nur die Properties habe ich anders benannt.
    DasObjekt "Currency" implementiert aber auch IDelete und in der DB sind die Werte auch korrekt verfügbar.
    mfG.
    Stephan
    Hallo @kaifreeman

    Ich kenne das, oft denkt man ZU kompliziert.

    Mach einfach:

    VB.NET-Quellcode

    1. Query = Query.Where(Function(x) x.Deleted = False)


    Hier meine Methode angepasst (und funzt):

    VB.NET-Quellcode

    1. Using rep As New DAL.GenericRepository(Of Article)
    2. Dim query = rep.GetAll() 'Alle selektieren - Es findet kein DB Routrip statt
    3. query = query.Where(Function(x) x.Code.StartsWith("123")) 'Nur die Artikel wo der Barcode mit 123 beginnt - Es findet kein Routrip zur DB statt
    4. query = query.Where(Function(x) x.DeletedFlag = False)
    5. query = query.Where(Function(x) x.CanExpire) 'Nur die Artikel welche ablaufen können - Es findet kein Routrip zur DB statt
    6. query = query.OrderBy(Function(x) x.Stock) 'Nun sortieren nach Lagermenge - Es findet kein Routrip zur DB statt
    7. query = query.Select(Function(x) New Article() With {.ArticleId = x.ArticleId, .Title = x.Title}) 'Nur die Spalten ID und Title abrufen (Performancethema) - Es findet kein Routrip zur DB statt
    8. query = query.Take(10) 'Wir wollen nur die ersten 10 Ergebnisse - Es findet kein Routrip zur DB statt
    9. Dim result As List(Of Article) = query.ToList 'HIER findet nun der Routrip zur DB statt
    10. 'Alle gefundenen Artikel (Nur Titel) ausgeben
    11. For Each a In result
    12. Console.Write(a.Title)
    13. Next
    14. End Using


    Wie man sehen kann:

    SQL-Abfrage

    1. SELECT TOP(10 /* @__p_0 */) [x].[ArticleId],
    2. [x].[Title]
    3. FROM [Articles] AS [x]
    4. WHERE ((([x].[DeletedFlag] = 0)
    5. AND ([x].[Code] LIKE N'123' + N'%'
    6. AND (LEFT([x].[Code], LEN(N'123')) = N'123')))
    7. AND ([x].[DeletedFlag] = 0))
    8. AND ([x].[CanExpire] = 1)
    9. ORDER BY [x].[Stock]


    Grüße
    Sascha

    EDIT:
    Nur so am Rande. Was hat das eigendlcih mit WPF zu tun???
    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,

    Nofear23m schrieb:

    Hallo @kaifreeman

    Ich kenne das, oft denkt man ZU kompliziert.

    Mach einfach:

    VB.NET-Quellcode

    1. Query = Query.Where(Function(x) x.Deleted = False)



    Das funktioniert natürlich weil ich ja schon defacto "typisiert" unterwegs bin.
    Aber in der generischen Klasse

    VB.NET-Quellcode

    1. Public Function GetAll(Optional IncludeDeleted As Boolean = False) As IQueryable(Of TEntity) Implements IRepository(Of TEntity).GetAll
    2. Dim Query As IQueryable(Of TEntity) = Context.Set(Of TEntity)
    3. If Not IncludeDeleted Then
    4. If GetType(IDelete).IsAssignableFrom(GetType(TEntity)) Then
    5. Query = Query.Where(Function(x) DirectCast(x, IDelete).Deleted = False)
    6. End If
    7. End If
    8. Return Query
    9. End Function


    da funktioniert es nicht.
    Prinzipiell kann ich die Entscheidung gelöschte Items mitzuladen ja durchaus im ViewModel erledigen aber meine Befürchtung ist halt wenn ich einmal vergessen die Where Clause für Deleted=False zu setzen das ich dann meine gelöschten Items sehe ohne das zu wollen.
    mfG.
    Stephan
    Hallo

    Was soll ich sagen. Funzt bei mir. Da muss der Hund wo anders begraben liegen. Evtl. kannst du ne Zip holagen und dann schau ich mal drüber.
    Vieleicht finde ich da dann ja was.

    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. ##

    Morgen Sascha,

    ich wollte gestern nicht die komplette Solution hochladen das wäre für dich zu aufwendig.
    Ich habe daher ein neues Projekt erzeugt und nur die Entität Currencies mitgenommen und alles so implementiert wie ich es auch in meinem "Hauptprojekt" getan habe. Der Fehler ist reproduzierbar.
    Im Anhang das abgespeckte Projekt, sehr freundlich von dir das du dir die Zeit nimmst da drüber zu schauen.

    lg
    Stephan

    UoWTestApp.zip
    mfG.
    Stephan
    Hallo

    OK, also ich muss sagen das hier glaube ich EF schuld ist da der Fehler erst bei der ausführung des Querys auftritt. Ich habe gesehen das du das ältere EF 6 verwendest.
    Gibt es einen Grund nicht EF Core zu verwenden? Ich weis nicht genau ob hier EF 6 das problem ist und hier den Cast evtl. nicht korrekt unterstützt.

    Weiters muss ich leider auf ein paar Dinge eingehen. Soll jetzt keine Kritik sein, nur Hinweise von mir.
    • Warum implementiert die Model-Basisklasse ALLE Interfaces? Da muss ich @ErfinderDesRades recht geben, das macht keinen Sinn. Da kannst du wirklich gleich alle Properties in die Basisklasse packen und muss auch nicht Casten. Dann läufts auch ohne umwege. Der Sinn der Interfaces und der Basisklasse ist ja eben das ALLE Models einen gemeinsame Basis haben (Basisklasse). Also bekommt JEDE Entität die Funktionalität dieser Basisklasse vererbt. So. Bei gewissen Tabellen wie z.b. Artikel will ich das man nicht löschen kann, also implementiert die Entität 'Article' dieses Interface. Aber eine Entität welche dieses Interface NICHT implementiert unterstützt ganz normal das löschen.
    • Warum hast du eine zweite Methode für das Speichern des DBContexts? SaveChangesRealDelete()
      Auch dies geht am Sinn vorbei. Warum macht man sowas wie ein IDelete-Interface... um sich vor sich selbst zu schützen. Genausogut könnte man ja immer beim Code daran danken und einfach das Deleted-Flag setzen und schon ist ein Datensatz als gelöscht markiert. Der Sinn an dem Interface und dem automatischen setzen des "Flags" ist ja das man sich nicht darum kümmern muss. Ich weis beim Programmieren das ich einfach löschen kann wenn ich das will, alles andere passiert automatisch. Ist es eine Entität wo nicht gelöscht werden darf wird auch nicht gelöscht, ich muss mir keine Gedanken darum machen. Hoffe da ich das verständlich geschrieben habe.
    • In deiner UnitOfWork ist sowohl das Property _context als auch der Konstruktor welche einen Context übernimmt Public.
      Das ist schlecht. Der sinn eines generischem Repositories ist unter anderem dieser ausgetauscht werden kann. Möchte ich oder der Kunde später auf ein Webservice wechseln muss lediglich die Repositoryklasse umgeschrieben werden, der Rest des Programms inkl. dem VM oder einer Businesslogik kann bleiben wie sie ist, es interessiert sie nicht.
    • Warum ein generisches Repository wenn du dann trotzdem eine CurrenciesRepository hast. brauchst du ja garnicht.
    • Deine Repositoryklasse implementiert kein IDisposable. Ein DBContext sollte aber immer Disposed werden da es sonst gerade mit dem EF zu unangenehmen nebeneffekten kommt. Gerade bei EF 6. EF Core würde Pooling auch unterstützen. Du müsstest also immer im VM daran denken und den Context Disposen. (Was gar nicht ginge wenn der Context nicht nach aussen gereicht werden würde - siehe oben)
    • Warum implementiert deine Model-Basisklasse INotifyPropertyChanged und INotifyCollectionChanged? Ein Model sollte eigendlich so primitiv wie möglich sein. Man kann es schon machen wenn man quasi zu "faul" ist später eigene VMs zu erstellen. Aber ich habe das schon mehrmals "versucht". Das geht nicht lange gut. Man handelt sich damit mehr probleme ein als man an Vorteilen gewinnt.

    So, ich hoffe ich habe dich nun nicht entmutigt. Du bist auf den richtigen Weg, solltest aber zusehen das du nicht den Fade verlierst und dich "verläufst". Ist ist ein kompliziertes Thema und schwer an alles zu denken.

    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. ##

    Mahlzeit,

    so es war wirklich ein Problem mit EF6 ich habe jetzt alles auf Core umgeschrieben und es funktioniert. Danke für deine Hilfe. Hab die aktualisierte App nochmals hochgeladen falls es jemand benötigt. UoWTestApp2.zip
    Einen Grund für EF6 hat es eigentlich nicht gegeben, ich hatte mein letztes Projekt damit gemacht und habe daher viel Code in das neue Projekt importiert (das war bevor ich mit UnitofWork und Repositories in Kontakt gekommen bin :D )

    Danke für deine Hinweise bin immer dankbar wenn ein Profi das ganze bewertet und mir Feedback gibt.

    Nofear23m schrieb:

    Warum implementiert die Model-Basisklasse ALLE Interfaces? Da muss ich @ErfinderDesRades recht geben, das macht keinen Sinn. Da kannst du wirklich gleich alle Properties in die Basisklasse packen und muss auch nicht Casten. Dann läufts auch ohne umwege. Der Sinn der Interfaces und der Basisklasse ist ja eben das ALLE Models einen gemeinsame Basis haben (Basisklasse). Also bekommt JEDE Entität die Funktionalität dieser Basisklasse vererbt. So. Bei gewissen Tabellen wie z.b. Artikel will ich das man nicht löschen kann, also implementiert die Entität 'Article' dieses Interface. Aber eine Entität welche dieses Interface NICHT implementiert unterstützt ganz normal das löschen.


    Das alle Interfaces in der Basisklasse eingebaut sind basiert auf einem Design Fehler, ich bin ursprünglich davon ausgegangen das jede Entität IDelete enthält, bin aber dann darauf gekommen das ich sehr wohl Objekte direkt löschen möchte, zusätzlich wollte ich mir den Boilerplate Code ersparen das ich bei jeder Klasse DeleteTimeStamp und Delete Property einkopieren muss.

    Nofear23m schrieb:

    Warum hast du eine zweite Methode für das Speichern des DBContexts? SaveChangesRealDelete()
    Auch dies geht am Sinn vorbei. Warum macht man sowas wie ein IDelete-Interface... um sich vor sich selbst zu schützen. Genausogut könnte man ja immer beim Code daran danken und einfach das Deleted-Flag setzen und schon ist ein Datensatz als gelöscht markiert. Der Sinn an dem Interface und dem automatischen setzen des "Flags" ist ja das man sich nicht darum kümmern muss. Ich weis beim Programmieren das ich einfach löschen kann wenn ich das will, alles andere passiert automatisch. Ist es eine Entität wo nicht gelöscht werden darf wird auch nicht gelöscht, ich muss mir keine Gedanken darum machen. Hoffe da ich das verständlich geschrieben habe.


    Schlicht und ergreifend gesagt es fehlte mir die Idee es anders zu lösen, das ich nach der Implementierung des Interfaces "fragen" kann habe ich erst in deinem HomeStorage geshen.

    Nofear23m schrieb:

    In deiner UnitOfWork ist sowohl das Property _context als auch der Konstruktor welche einen Context übernimmt Public.
    Das ist schlecht. Der sinn eines generischem Repositories ist unter anderem dieser ausgetauscht werden kann. Möchte ich oder der Kunde später auf ein Webservice wechseln muss lediglich die Repositoryklasse umgeschrieben werden, der Rest des Programms inkl. dem VM oder einer Businesslogik kann bleiben wie sie ist, es interessiert sie nicht.


    Hatte ich so von dem besagten Kurs übernommen, mit der neuen Implementierung von dir ist es definitv besser, soweit ich UnitOfWork verstanden habe ist es ja eine Kapselung der Datenzugriffsvorgänge um ein einfaches austauschen der Datenzugriffstechnologie zu ermöglichen.

    Nofear23m schrieb:

    Warum ein generisches Repository wenn du dann trotzdem eine CurrenciesRepository hast. brauchst du ja garnicht.


    Weil ich im CurrenciesRepository die Add Methode überschreibe, in der Testapplikation habe ich einiges an Code gekickt somit erschein das Repo als sinnlos. Es enthält auch einige weitere Methoden die nicht im generischen Repo vorhanden sind.

    Nofear23m schrieb:

    Deine Repositoryklasse implementiert kein IDisposable. Ein DBContext sollte aber immer Disposed werden da es sonst gerade mit dem EF zu unangenehmen nebeneffekten kommt. Gerade bei EF 6. EF Core würde Pooling auch unterstützen. Du müsstest also immer im VM daran denken und den Context Disposen. (Was gar nicht ginge wenn der Context nicht nach aussen gereicht werden würde - siehe oben)


    Danke wusste ich nicht habe ich korrigiert.

    Nofear23m schrieb:

    Warum implementiert deine Model-Basisklasse INotifyPropertyChanged und INotifyCollectionChanged? Ein Model sollte eigendlich so primitiv wie möglich sein. Man kann es schon machen wenn man quasi zu "faul" ist später eigene VMs zu erstellen. Aber ich habe das schon mehrmals "versucht". Das geht nicht lange gut. Man handelt sich damit mehr probleme ein als man an Vorteilen gewinnt.


    Ganz ehrlich in meinen ersten geh versuchen mit MVVM habe ich das MVVM falsch verstanden und hatte ein ViewModel für alles. Damit war es notwendig das ich NotifyPropertyChanged implementiere um der View zu sagen wenn sich Änderungen an einer Property ergeben haben.
    Wie sollte ich sonst Änderungen mitbekommen.
    Wenn ich jetzt genau darüber nachdenke macht es wahrscheinlich Sinn auf NotifypropertyChanged im Model zu verzichten. Jede View hat ja mittlerweile ihr ViewModel... Ich muss mal testen wie sich das verhält wenn ich dann eine Currency adde ob die Übersicht dann entsprechend aktualisiert wird.
    Ergibt dann auch Sinn warum beim EntityFramework ModelFirst mit DB Designer in den erstellten Klassen nie NotifyProperyChanged implementiert war...

    Um ein kleines grünes Männchen zu zitieren: Noch viel zu lernen ich habe.

    Danke nochmal für dein Feedback und deine Bemühungen, ich bin definitiv nicht entmutigt sondern eher motiviert. Ich werde zwar mein Projekt nochmal komplett umarbeiten müssen aber ich mach es ja weil ich was lernen möchte.

    lg
    Stephan
    mfG.
    Stephan
    Hallo Stephan

    Freut mich das es nun funzt. JA, in EF Core ist einiges anders, das meißte jedoch viel besser. Vorallem in Sachen performance und "kaltstart" wie du sicher bemerkt hast. EF Core lohnt sich.
    Übrigens - wenn du EF lernen möchtest und richtig damit arbeiten willst, empfehle ich immer gerne das Buch von Holger Schwichtenberg. Ich habe es gelesen und kann es empfehlen. Generel die Bücher von Holger. Ich habe mehrere von ihm.

    kaifreeman schrieb:

    Danke für deine Hinweise bin immer dankbar wenn ein Profi das ganze bewertet und mir Feedback gibt.

    Ich bin auch kein Profi - auch ich bin nur Hobby Programmierer habe mich mit diesen Theme jedoch viel beschäftigt, bin aber noch lange nicht am ende.

    kaifreeman schrieb:

    zusätzlich wollte ich mir den Boilerplate Code ersparen das ich bei jeder Klasse DeleteTimeStamp und Delete Property einkopieren muss.

    Musst nicht einkopieren. Einfach das Interface implementieren und VS erstellt dir den Code - also die Properties. Das einzig unschöne, die Properties werden mit Getter und Setter erstellt, welche man einfach weglöschen kann. - Vorausgesetzt man hat eben Modelklassen ohne INotifyPropertyChanged 8|

    kaifreeman schrieb:

    Wie sollte ich sonst Änderungen mitbekommen.
    Wenn ich jetzt genau darüber nachdenke macht es wahrscheinlich Sinn auf NotifypropertyChanged im Model zu verzichten. Jede View hat ja mittlerweile ihr ViewModel... Ich muss mal testen wie sich das verhält wenn ich dann eine Currency adde ob die Übersicht dann entsprechend aktualisiert wird.

    Richtig, jede View hat ein ViewModel. Aber wirklich JEDE. Also auch Currency!!! Genaugenommen ist es innerhalb z.b. einer ListBox ja auch ein View oder? Also gibt es ein CurrencyVm.
    Dieses VM macht nichts anderes als eine Instanz von Currency zu Syncronisieren.

    Beispiel:

    VB.NET-Quellcode

    1. Public Class CurrencyVm
    2. Inherits ViewModelBase
    3. Private _currencyModel as Currency
    4. Public PRoperty Prop1 as String
    5. Get
    6. REturn _currencyModel.Prop1
    7. End Get
    8. Set
    9. _currencyModel.Prop1 = value
    10. RaisePropertyChanged()
    11. End Set
    12. End Property
    13. End Class


    Auf den ersten Blick wirkt das Umständlich und unnötig, ist es aber nicht. Ein Vorteil hier. Die View muss nichts vom Model wissen. Du kannst also im Model etwas ändern ohne Angst zu haben das Plötzlich ein Binding nicht mehr funzt. Im ViewModel würde eh der Compiler meckern. Ausserdem bist du Flexibel was die Anzeige betrifft. Beispielsweise kannst du im Model ein Datumsproperty haben aber willst im View für das morgige Datum einfach "Morgen" in einem Label anzeigen. Ganz easy im VM ohne die Struktur vom Model ändern zu müssen. So, jetzt wurde es wieder ein Roman den keiner braucht. :whistling:

    kaifreeman schrieb:

    Danke nochmal für dein Feedback und deine Bemühungen, ich bin definitiv nicht entmutigt sondern eher motiviert. Ich werde zwar mein Projekt nochmal komplett umarbeiten müssen aber ich mach es ja weil ich was lernen möchte.

    Das ist schön und lobenswert, nur weiter so. Wenns mal passt macht es richtig spaß das Repository dann zu verwenden, arbeitet sich echt gut damit.

    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,

    ich hab das jetzt mit dem ViewModel für die Entität Currency mal durchgespielt:

    Entität Currency:

    VB.NET-Quellcode

    1. Imports System.ComponentModel.DataAnnotations
    2. Imports UoWTestApp.Interfaces
    3. Partial Public Class Currency
    4. Inherits model_base
    5. Implements IDelete
    6. #Region "Mapped"
    7. Public Property Id As Integer
    8. <Display(Name:="Währungssymbol", Description:="Symbol der Währung", Order:=1)>
    9. <Required(AllowEmptyStrings:=False, ErrorMessage:="Währungsymbol muss angegeben sein")>
    10. <StringLength(10)>
    11. Public Property CurrSymbol As String
    12. <Display(Name:="Währungsbezeichnung", Description:="Währungsbezeichnung in Langform", Order:=2)>
    13. <Required(ErrorMessage:="Währungsbezeichnung muss angegeben sein")>
    14. Public Property CurrName As String
    15. <Display(Name:="Umrechnungsrate", Description:="Umrechnungsrate im Vergleich zur Basiswährung", Order:=3)>
    16. <Required>
    17. <Range(Decimal.MinValue, Decimal.MaxValue, ErrorMessage:="Umrechnungsrate muss angegeben sein")>
    18. Public Property CurrConversionRate As Decimal
    19. <Display(Name:="Umrechnungsratendatum", Description:="Datum der letzten Aktualisierung der Umrechnungsrate", Order:=4)>
    20. <Required(AllowEmptyStrings:=False, ErrorMessage:="Umrechnungsratendatum muss angegben sein")>
    21. Public Property CurrConversionRateDate As Date
    22. Public Property Deleted As Boolean Implements IDelete.Deleted
    23. Public Property DeletedTimeStamp As Date? Implements IDelete.DeletedTimeStamp
    24. #End Region
    25. End Class

    Alles ohne die Get/Sets weil die braucht es ja nicht mehr.
    hier gleich meine 1. Frage ich nutze DataAnnotations zum erzeugen von dynamischen DataGrid Autogenerated Columns ist die Implementierung besser im Model oder im Viewmodel aufgehoben?

    Danach das ViewModel für die Entität Currency:

    VB.NET-Quellcode

    1. Imports GalaSoft.MvvmLight
    2. Public Class VMCurrency
    3. Inherits ViewModelBase
    4. Dim _CurrencyModel As Currency
    5. Public Property Id As Integer
    6. Get
    7. Return _CurrencyModel.Id
    8. End Get
    9. Set(value As Integer)
    10. _CurrencyModel.Id = value
    11. RaisePropertyChanged("Id")
    12. End Set
    13. End Property
    14. Public Property CurrSymbol As String
    15. Get
    16. Return _CurrencyModel.CurrSymbol
    17. End Get
    18. Set(value As String)
    19. _CurrencyModel.CurrSymbol = value
    20. RaisePropertyChanged("CurrSymbol")
    21. End Set
    22. End Property
    23. Public Property CurrName As String
    24. Get
    25. Return _CurrencyModel.CurrName
    26. End Get
    27. Set(value As String)
    28. _CurrencyModel.CurrName = value
    29. RaisePropertyChanged("CurrName")
    30. End Set
    31. End Property
    32. Public Property CurrConversionRate As Decimal
    33. Get
    34. Return _CurrencyModel.CurrConversionRate
    35. End Get
    36. Set(value As Decimal)
    37. _CurrencyModel.CurrConversionRate = value
    38. RaisePropertyChanged("CurrConversionRate")
    39. End Set
    40. End Property
    41. Public Property CurrConversionRateDate As Date
    42. Get
    43. Return _CurrencyModel.CurrConversionRateDate
    44. End Get
    45. Set(value As Date)
    46. _CurrencyModel.CurrConversionRateDate = value
    47. RaisePropertyChanged("CurrConversionRateDate")
    48. End Set
    49. End Property
    50. End Class


    Soweit ja so gut aber dann bin ich wieder bei meinem Ursprungsproblem warum ich eigentlich wieder davon abgekommen bin.
    Der Datacontext oder in dem aktuellen Fall die UnitofWork gibt bei GetAll Objekte vom Typ Currency zurück was ja eigentlich korrekt ist.

    VB.NET-Quellcode

    1. Private Function GetAllCurrencies() As Boolean
    2. Dim QCurrencies = _UnitofWork.Currencies.GetAll
    3. QCurrencies = QCurrencies.Where(Function(x) x.Deleted = False)
    4. Currencies = New ListCollectionView(QCurrencies.ToList)
    5. For Each element In Currencies
    6. Debug.Print(element.GetType.ToString) '=> Output = UoWTestApp.Currency
    7. Next
    8. Return True
    9. End Function

    Schreibe ich jetzt das Repository so um das ein Object vom Type VMCurrency erwartet wird meckert der Compiler "Cannot create a DbSet for 'VMCurrency' because this type is not included in the model for the context.'" was ja auch wieder logisch ist.
    Muss ich das ganze beim Laden von der DB in das Model dann ins VMCurrency "rüber" casten?

    Kann es sein das ich auf dem Holzpfad bin und folgende Überlegung korrekter wäre:
    VMCurrency wird benutzt wenn es um CRUD Operationen für das Object (Entität) geht
    Wenn ich aber alle Currencies haben möchte benutze ich weiterhin eine List(of Currency) oder ähnliches??

    Edit: Sinnlose Variablen aus Model gelöscht.
    mfG.
    Stephan

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

    Hallo

    Gut das du nachfragst, ich weis - da kann man sich oft lange spielen.

    kaifreeman schrieb:

    Alles ohne die Get/Sets weil die braucht es ja nicht mehr.

    Aber was machen die Variablen da noch?
    So?

    VB.NET-Quellcode

    1. Imports System.ComponentModel.DataAnnotations
    2. Imports UoWTestApp.Interfaces
    3. Public Class Currency
    4. Inherits model_base
    5. Implements IDelete
    6. Public Property Id As Integer
    7. <Required(AllowEmptyStrings:=False, ErrorMessage:="Währungsymbol muss angegeben sein")>
    8. <StringLength(10)>
    9. Public Property CurrSymbol As String
    10. <Required(ErrorMessage:="Währungsbezeichnung muss angegeben sein")>
    11. Public Property CurrName As String
    12. <Required>
    13. <Range(Decimal.MinValue, Decimal.MaxValue, ErrorMessage:="Umrechnungsrate muss angegeben sein")>
    14. Public Property CurrConversionRate As Decimal = 1
    15. <Required(AllowEmptyStrings:=False, ErrorMessage:="Umrechnungsratendatum muss angegben sein")>
    16. Public Property CurrConversionRateDate As Date = Date.Now
    17. Public Property Deleted As Boolean Implements IDelete.Deleted
    18. Public Property DeletedTimeStamp As Date? Implements IDelete.DeletedTimeStamp
    19. End Class

    Einfacher und übersichtlicher oder?
    In der kurzen schreibweise eines Properties wir vom Compiler automatisch ein Backingfield im Hintergrund im Shema "_%PropertieName" erzeugt. Aber für dich nicht sichtbar.
    Kannst da ja mal Probieren nun in dem Model von mir z.b. im Konstruktor der Variable _Deleted einen Wert zuzuweisen. Du hast zwar keine Intellisense aber es geht. =O

    kaifreeman schrieb:

    hier gleich meine 1. Frage ich nutze DataAnnotations zum erzeugen von dynamischen DataGrid Autogenerated Columns ist die Implementierung besser im Model oder im Viewmodel aufgehoben?

    Lies mal nochmal meine letzte Antwort. Die View braucht das Model nicht zu kennen. Wie soll es dann die DataAnnotations überhaupt sehen?
    Du übergibst dem View (Also auch dem DataGrid) ja kein Currency sondern ein CurrencyVm. Also muss die DataAnnotation für die Spaltenbeschreibung wenn dann dort hin.
    Habe ich persönlich aber noch nicht verwendet, ich gebe die Spalten stets manuell an.

    kaifreeman schrieb:

    Schreibe ich jetzt das Repository so um das ein Object vom Type VMCurrency erwartet wird meckert der Compiler "Cannot create a DbSet for 'VMCurrency' because this type is not included in the model for the context.'" was ja auch wieder logisch ist.
    Muss ich das ganze beim Laden von der DB in das Model dann ins VMCurrency "rüber" casten?

    Ne, nix casten. Geht ja nicht. Wie willst du Currency in CurrencyVm Casten?

    Willst du eine Liste von CurrencyVMs anhand einer Liste von Currency's welche du von der DB bekommst "füllen" machst du das einfach wie folgt.

    VB.NET-Quellcode

    1. Public Class TestVm
    2. Public Sub New()
    3. CurrencyList = New ObservableCollection(Of CurrencyVm)
    4. Dim allCurrencies As List(Of Currency) = DB.GetCurrencys 'Laden von Currencyliste z.b. durch GetAll()
    5. allCurrencies.ForEach(Sub(x) CurrencyList.Add(New CurrencyVm(x))) 'In ForEach einfach nun für jedes Currency ein CurrencyVm erstellen!!!
    6. End Sub
    7. Private _currencyList As ObservableCollection(Of CurrencyVm)
    8. Public Property CurrencyList() As ObservableCollection(Of CurrencyVm)
    9. Get
    10. Return _currencyList
    11. End Get
    12. Set(ByVal value As ObservableCollection(Of CurrencyVm))
    13. _currencyList = value
    14. NotifyPropertyChanged()
    15. End Set
    16. End Property
    17. End Class
    18. Public Class CurrencyVm
    19. Private ReadOnly _currentModel As Currency
    20. Public Sub New()
    21. End Sub
    22. Friend Sub New(currencyModel As Currency)
    23. _currentModel = currencyModel
    24. End Sub
    25. Public Property CurrSymbol As String
    26. Get
    27. Return _currentModel.CurrSymbol
    28. End Get
    29. Set(value As String)
    30. _currentModel.CurrSymbol = value
    31. NotifyPropertyChanged()
    32. End Set
    33. End Property
    34. End Class


    Hoffe es wird nun klarer. Ansonsten sag einfach bescheid, dann können wir das Anhand eines Praxisbeispiels durchgehen.

    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. ##

    Sorry, vergessen. Zwei Dinge vieleicht noch.
    • Achte darauf das ich den zweiten Konstruktor der CurrencyVm-Klasse als Friend deklariert habe. Das ist wichtig damit eben das Model NICHT nach außen gereicht wird!
    • Warum schreibst du immer RaisePropertyChanged("CurrName"). Da das MVVM Light wie hier in ViewModelBase zu sehen ist verwendet das Toolkit <CallerMemberName>.
    Du kannst also einfach RaisePropertyChanged() schreiben. So kannst du dich nicht vertippen und wenn du das Property änderst musst du hier nicht daran danken das auch zu ändern.

    Soltest du aber doch mal ein Argument mitgeben wollen wie z.b. wenn du dem View sagen willst das sich ein ReadOnly-Property geändert hat dann mach das bitte wie folgt damit du sowohl Compilerprüfung hast als auch die Refactoringmöglichkeiten von VS nutzen kannst.

    VB.NET-Quellcode

    1. Private _firstName As String
    2. Public Property FirstName() As String
    3. Get
    4. Return _firstName
    5. End Get
    6. Set(ByVal value As String
    7. )
    8. _firstName = value
    9. RaisePropertyChanged()
    10. RaisePropertyChanged(NameOf(FullName))
    11. End Set
    12. End Property
    13. Private _lastName As String
    14. Public Property LastName() As String
    15. Get
    16. Return _lastName
    17. End Get
    18. Set(ByVal value As String)
    19. _lastName = value
    20. RaisePropertyChanged()
    21. RaisePropertyChanged(NameOf(FullName))
    22. End Set
    23. End Property
    24. Public ReadOnly Property FullName As String
    25. Get
    26. Return $"{FirstName} {LastName}"
    27. End Get
    28. End Property


    Wobei ich aber sagen muss das es von dem Framework irgendwie folgende zeile doof ist:

    C#-Quellcode

    1. if (string.IsNullOrEmpty(propertyName))
    2. {
    3. throw new ArgumentException("This method cannot be called with an empty string", "propertyName");
    4. }


    MS hat extra ein Feature implementiert damit man die "ganze View" auf wunsch quasi "Refreshen" kann indem man RaisePropertyChanged(Nothing) schreibt und die nehmen das einfach weg? Komisch. Aber OK.

    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. ##

    Abend,

    ganz ehrlich das Thema mit einem eigenen ViewModel für die Entität ist ziemlich komplex.... irgendwie habe ich das Gefühl ich tipp mir nen Ast und bastel Klasse und Klasse und habe aktuell keine Mehrwert aber egal da muss er durch der Lurch...

    Hab jetzt versucht meine View um CRUD zu erweitern:
    Spoiler anzeigen

    VB.NET-Quellcode

    1. Imports System.Collections.ObjectModel
    2. Imports GalaSoft.MvvmLight
    3. Imports GalaSoft.MvvmLight.CommandWpf
    4. Imports UoWTestApp.Persistence
    5. Public Class VMUclCurrencies
    6. Inherits ViewModelBase
    7. Dim _UnitofWork As New UnitOfWork(New UoWTestAppModelCore)
    8. Sub New()
    9. __Currencies = New ObservableCollection(Of VMCurrency)
    10. Dim AllCurrencies As List(Of Currency) = _UnitofWork.Currencies.GetAll.ToList
    11. AllCurrencies.ForEach(Sub(x) __Currencies.Add(New VMCurrency(x)))
    12. Currencies = New ListCollectionView(__Currencies)
    13. End Sub
    14. Dim __Currencies As ObservableCollection(Of VMCurrency)
    15. Public Property Currencies As ListCollectionView
    16. #Region "CRUD"
    17. Public Property CmdAdd As New RelayCommand(AddressOf AddItem)
    18. Private Sub AddItem()
    19. Dim vNewCurr As New Currency With {.CurrName = "Test", .CurrSymbol = "T", .CurrConversionRate = 1, .CurrConversionRateDate = Date.Now}
    20. _UnitofWork.Currencies.Add(vNewCurr)
    21. _UnitofWork.Complete()
    22. Currencies.AddNewItem(New VMCurrency(vNewCurr))
    23. Currencies.CommitNew()
    24. End Sub
    25. Public Property CmdEdit As New RelayCommand(AddressOf EditItem)
    26. Private Sub EditItem()
    27. Currencies.EditItem(Currencies.CurrentItem)
    28. Dim vItem As VMCurrency = Currencies.CurrentEditItem
    29. vItem.CurrName &= "Edit"
    30. Currencies.CommitEdit()
    31. _UnitofWork.Complete()
    32. End Sub
    33. Public Property CmdDelete As New RelayCommand(AddressOf DeleteItem)
    34. Private Sub DeleteItem()
    35. Dim vCurr As Currency = _UnitofWork.Currencies.Get(DirectCast(Currencies.CurrentItem, VMCurrency).Id)
    36. _UnitofWork.Currencies.Remove(vCurr)
    37. Currencies.Remove(Currencies.CurrentItem)
    38. Currencies.MoveCurrentToFirst()
    39. _UnitofWork.Complete()
    40. End Sub
    41. #End Region
    42. End Class


    Das Create funktioniert ganz gut die UnitOfWork tut was sie soll, Delete gefällt mir zwar nicht aber funktioniert, bei Edit steh ich allerdings schon wieder am Schlauch.

    Das Edit:

    VB.NET-Quellcode

    1. Public Property CmdEdit As New RelayCommand(AddressOf EditItem)
    2. Private Sub EditItem()
    3. Currencies.EditItem(Currencies.CurrentItem)
    4. Dim vItem As VMCurrency = Currencies.CurrentEditItem
    5. vItem.CurrName &= "Edit"
    6. Currencies.CommitEdit()
    7. _UnitofWork.Complete()
    8. End Sub

    Das Edit als solches funktioniert perfekt die View wird aktualisiert alles grün. Nur die UnitOfWork interessiert es aktuell nicht die Bohne die Datenbank zu aktualisieren....
    Das einzige was mir einfällt wäre es mit der UnitOfWork durch Get die bearbeitete Currency zu holen und dann mit der VMCurrency alle Properties abzugleichen aber das klingt so brutal das will ich gar nicht erst versuchen....

    oder Alternativ:
    Ich implementiere eine Function Update im ViewModel die mit einer neuen UnitOfWork das Update in die Datenbank spielt... aber das ist doch auch irgendwie nicht schön?
    mfG.
    Stephan
    Hallo

    Na du kommst ja ganz gut voran. Gefällt mir :thumbsup:

    kaifreeman schrieb:

    ganz ehrlich das Thema mit einem eigenen ViewModel für die Entität ist ziemlich komplex.... irgendwie habe ich das Gefühl ich tipp mir nen Ast und bastel Klasse und Klasse und habe aktuell keine Mehrwert aber egal da muss er durch der Lurch...

    Ja, denkt man am Anfang. Ganz so schlimm ist es aber dann nicht. Ich habe mir hierfür Vorlagen und T4 Templates geschaffen welche das für mich erledigen aber....
    Gehe einfach in die Modelklasse. Copy&Paste in die VM-Klasse und dann einfach bei jedem Property am Ende ein ENTER gefolgt von Get tippen und ENTER.
    Der Rest wird dir nun von VS erstellt. Nun bruchst du nur noch deinen Getter und Setter ausfüllen. Und das ist gleube ich vertretbar.

    Aber überlege mal. spätestens beim ersten Command den du in einem Model benötigen würdest (und das kommt, glabe mir) müsstest du sowieso ein VM um das Model machen. So, und nun hast du eine Mischung aus Models und ViewModels. Echt unschön. So hast du eine gerade Linie. Mach dir Vorlagen für Copy&Paste und/oder VisualStudio CodeSnippets die das für dich erledigen. Niemand tippt den ganzen rotz runter wenn einem die IDE das erledigen kann.

    kaifreeman schrieb:

    Delete gefällt mir zwar nicht aber funktioniert
    Was gefällt dir nicht?

    kaifreeman schrieb:

    Das Edit als solches funktioniert perfekt die View wird aktualisiert alles grün. Nur die UnitOfWork interessiert es aktuell nicht die Bohne die Datenbank zu aktualisieren....

    Solange das Tracking von EF eingeschaltet ist und der selbe DBContext verwendet wird muss ja lediglich SaveChanges aufgerufen werden. Ich nehme an das macht deine _UnitofWork.Complete() richtig?
    Der Changetracker bekommt es selbstständig mit wenn sich Properties ändern. Und das tun sie ja auch weil du ja im VMCurrency in jedem Setter das Property des Modelobjekts nachträgst so wie ich das gezeigt habe. Das hoffe ich zumindest, du hast die Klasse nicht gepostet.

    Sobald der ChangeTracker beim machen der änderung "anwesend" ist (wenn Tracking eingeschaltet - was der Default-Wert bei EF ist) bekommt er die änderung mit und schreibt die geänderten Werte beim SaveChanges zurück in die DB. Und zwar NUR DIE GEÄNDERTEN. Nicht alle Spalten des Datensatzes, so interlligent ist es schon. SaveChanges gibt dann zurück (Integer) wieviele Datensätze von der änderung betroffen waren. Werte diesen Wert also immer aus!!!!! Das ist eines der häufigsten Fehler den die Leute beim umgang mit EF machen und mir stellen sich immer die Haare auf. ;(

    Ansonsten Poste nun mal dein aktuelles Projekt damit ich mir ansehen kann warum nicht gespeichert wird.

    Grüße
    Sascha

    PS: Ich habe jetzt vermutlich mehr getippt als wenn du 10 VM Klassen machen würdest. :whistling:
    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. ##

    Abend,

    muss die letzten Tage vom Urlaub ausnutzen drum bin ich motiviert am arbeiten :thumbup:

    Am Delete gefällt mir nicht das ich mir das eigentlich bereits vorhandene Object nochmals aus der DB holen muss, wäre es hier nicht einfacher das bereits in der Viewmodel Klasse vorhandene Object vom Typ Entität doch zugänglich zu machen?
    Ist wahrscheinlich mehr eine Optik geschickte es würde halt eine Query ersparen...

    Zum Edit:
    Tja ich hatte per Default den ChangeTracker deaktiviert... (Optional trackings as boolean war false)

    VB.NET-Quellcode

    1. Protected Friend Sub New(Optional _Context As UoWTestAppModelCore = Nothing, Optional tracking As Boolean = True)
    2. If _Context IsNot Nothing Then
    3. Context = _Context
    4. _disposeContext = False
    5. Else
    6. Context = New UoWTestAppModelCore()
    7. End If
    8. ContextInternal.ChangeTracker.AutoDetectChangesEnabled = tracking
    9. If tracking Then
    10. ContextInternal.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll
    11. Else
    12. ContextInternal.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking
    13. End If
    14. End Sub


    und jetzt speichert meine UnitofWork brav wie es sein soll (Ja Complete ruft eigentlich nur .savechanges auf).

    So mit dem Wissen werde ich jetzt mal meine Anwendung umbauen muss mir auch noch das Thema DBContext und Migration von EF Core geben, schade das EF Core Migration VB nicht unterstützt und man eine C# Klasse basteln muss.

    Danke dir vielmals für deine Hilfe.

    lg
    Stephan
    mfG.
    Stephan