Laden...

MVVM: darf Model andere ViewModels kennen?

Erstellt von IamTheBug vor 8 Jahren Letzter Beitrag vor 8 Jahren 2.279 Views
I
IamTheBug Themenstarter:in
401 Beiträge seit 2006
vor 8 Jahren
MVVM: darf Model andere ViewModels kennen?

Hi,

ich habe natürlich schon viel über MVVM gelesen aber einige Sachen sind mir trotzdem noch unklar. Deshalb wollte ich mal hier nachfragen.
Ich habe folgendes (fiktives) Programm:

Model (kein 100% funktionierender Code):


public class House
{
    public string Name {get;set;}
    public Color Color {get;set;}
}

public class Person
{
    public string Name {get;set;}
    public int Age {get;set;}
    public Color FavColor {get;set { ChangeColorOfallHouses();}

    private List<House> Houses {get;set;} = new List<House>();

    public void AddHouse(House house)
   {
      if (Age < 18)
         throw new Exception("Man muss ueber 18 fuer ein Haus sein.")
      Houses.Add(house);
   }
   
   private void ChangeColorOfAllHouses() { .....}
}


Das wären meine zwei Model. Relativ einfach und simpel. Es sollte klar sein was gemeint ist.
Frage 1: Ist es richtig das schon Logik in den Models abgearbeitet wird? Also z.B. Das beim hinzufügen eines Haus schon geprüft wird ob die Person über 18 ist? Sowas muss doch nicht ins ViewModel oder?

Frage 2: Wenn beim Setzen der FavColor einer Person alle Farben der Häuser die er besitzt geändert werden soll (Häuser werden in Lieblingsfarbe gestrichen) kann doch so ein Funktionsaufruf auch im Model bleiben und muss nicht im ViewModel als Logik abgehandelt werden.

Die oben aufgeführten Punkte sind auch keine " das ist in dem Programm so Regel" sondern eine Regel die immer allgemeingültig ist. Wie gesagt das oben ist nur ein an den Haaren herbeigezogenes Beispiel.

Der Stand bis hier hin ist ja das die Models komplett alleine funktionieren und ihre unumstösslichen Regeln auch selber abarbeiten und eventuell Eigenschaften auch auf andere Objekte weiterreichen.

Jetzt habe ich ein View in XAML und ein ViewModel zu dem View erstellt in dem die Commands enthalten sind.
Weiterhin erstelle ich ein ViewModel für die Personklasse zum aufarbeiten der Daten für das View.

Frage 3: jetzt ist ja in der Personenklasse schon direkt eine Liste des HouseModels vorhanden.
Wie bekomme ich aber die darin gespeicherten Häuser in ein HouseViewModel damit diese auch für den View aufgearbetiet werden.
Soll die Personenklasse keine Liste von House sondern von HouseViewModel halten?

Mfg

IamTheBug

2.079 Beiträge seit 2012
vor 8 Jahren

MVVM ist ein Muster, es gibt also kein festes allgemeingültiges Regelwerk, was Du beachten musst.
Bei kleinen Projekten macht das auch nicht unbedingt Sinn.

Prinzipiell würde ich eine Anwendung so aufbauen:
Ich habe meine Models. Sie stellen die Daten bereit und validieren, mehr nicht. Sie kennen nicht die ViewModels. Untereinander dürfen sie sich kennen.
Darüber liegen die ViewModels, sie kennen die Models, bilden deren Daten ab und bieten Funktionalität an. Diese Funktionalität ist in eigenen Command-Klassen und damit ebenfalls getrennt. Die ViewModels validieren auch, hier liegt die Herausforderung, dass sich die Validierung nicht doppelt. Es ist hier aber notwendig, damit der User merkt, dass er was falsch gemacht hat.
Darüber liegt die View, die die ViewModels kennt. Sie darf auch die Models kennen, ich persönlich mache das aber nicht.

Wenn Du es ganz weit treiben willst, dann kann alles Interfaces haben und Du registrierst die Klassen über IoC. So hast Du einen starke Entkopplung.
Ich mag diese Herangehensweise, so lässt sich beinahe alles leicht austauschen ^^

Um auf die Fragen einzugehen:
Frage 1: Dein Beispiel läuft für mich unter Validierung und nicht unter Logik und ist damit völlig ok.
Du kannst aber auch eine weitere Schicht zwischen Model und ViewModel einschieben, die Business-Layer. Das ViewModel kennt die Business-Objects und die kennen die Models. Die Validierung und Logik wandert in dem Business-Layer und ist dort auch für andere Komponenten (z.B. Import, Export) zugänglich. Die ViewModels können dann über RelayCommands auch die Logik aus den Business-Objects anbieten. Wenn Du es klug aufgebaut hast, können die ViewModels auch die Validierung der Business-Objects nutzen und somit wierholst Du dich nicht.

Frage 2: Das ist Situationsabhängig. Manchmal ist das im Model sinnvoll, aber auch nicht immer.
Ich persönlich möchte nichts im Model haben, die sind bei mir nur wegen dem ORM da. Im Business-Layer findet sich dann die Logik die da rein gehört, der Rest wo anders und bekommt die Business-Objects für die weitere Arbeit.
Beachte aber auch: Je mehr Du solche Aktionen automatisch ablaufen lässt, desto mehr Kontrolle gibst Du ab und hast schnell Seiteneffekte, die dann nur schwer zu umgehen sind.

Frage 3: Auch das ist unterschiedlich. Mir fallen spontan zwei Lösungen ein:
Eine ICollection-Implementierung, die an die Model-Liste weiter leitet und INotifyCollectionChanged implementiert.
Eine Speichern-Funktionalität, die dann die Daten von einer Lise in die Andere schaufelt. Das wäre mein Favorit und ist auch sicherer für den User, der kann nämlich auch alles zurück setzen 😉

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

I
IamTheBug Themenstarter:in
401 Beiträge seit 2006
vor 8 Jahren

Vielen Dank für die Informationen und die Schilderung deines Herangehens.

Bis zu deiner Antwort auf Frage 3 konnte ich dem auch gut folgen. 😃 Teilweise war es so wie ich es selber schon mache und ach verstanden habe. Die Sache mit dem Business-Layer ist ein guter Hinweis, da denke ich nochmal tiefer drüber nach 😃.

Allerdings habe ich die Antwort auf Frage 3 nicht so ganz verstanden.

Für mich ist ja die Frage ob ich in Person


private List<House> Houses {get;set;} = new List<House>();

durch


private List<HouseViewModel> Houses {get;set;} = new List<HouseViewModel>();

ersetzen soll

Eventuell könntest du zu deiner Lösung noch etwas sagen damit ich es besser verstehe.
Vielen Dank 😃

Mfg

IamTheBug

2.079 Beiträge seit 2012
vor 8 Jahren

Die Sache mit dem Business-Layer ist ein guter Hinweis, da denke ich nochmal tiefer drüber nach

Beachte aber: Der Mehraufwand das zu machen ist groß. Der zusätzliche Aufwand für MVVM ist schon groß, hier wird der riesig 😄
Überlege dir also, ob Du das brauchst. Wenn nicht, dann reicht denke ich auch jede Logik in den ViewModels

Ob Du ViewModels in Models verwenden kannst/darfst:
Können: Ja, Dürfen: Nein 😄
Wichtig ist, dass Du dir deine Schichten in verschiedenen Höhen vorstellst. Die unteren Schichten (z.B. Model) sind der Grundstein, während die oberen Schichten darauf aufbauen.
Jede Schichte kennt (im idealfall) nur die darunter liegende Schicht. View kennt ViewModel, ViewModel kennt Business-Object, Business-Object kennt Model. View kann auch direkt mit Model arbeiten, das geht, reduziert aber die Flexibilität in beiden Schichten.
Was nie und niemals darf: Model kennt ViewModel, Model kennt Business-Object, etc.

Wenn jede Änderung ans Model weiter gereicht werden soll, dann würde ich eine List-Klasse entwickeln, die die Quell-Liste (vom Model) beinhaltet. Jede Methode dieser Klasse bekommt dann den Typ des ViewModels und muss dann dafür sorgen, dass sie die gültige Instanz des Models dazu bekommt bzw. umgekehrt, damit jeder Methoden-Aufruf an die Model-Liste weiter geleitet werden kann.
Das ist eine Lösung, meiner Meinung nach aber nicht gut, dafür aber relativ schnell und einfach gemacht.

Die meiner Meinung nach bessere Lösung wäre, wenn Du eine Load- und eine Save-Methode hast bzw. passende Commands dazu. In der Load-Methode wird dann die Liste genommen und für jedes Listen-Element das passende ViewModel erstellt und in der eigenen Liste abgelegt. Pro Item muss dann auch nochmal Load aufgerufen werden, damit die auch ihr Daten laden.
In der Save-Methode wird dann diese eigene Liste genommen und für jedes darin enthaltene ViewModel das passende Model geholt, das ViewModel kennt es ja. Entfernte Items müssen aus der Model-Liste entfernt werden, hinzugefügte Items müssen auch für die Model-Liste erzeugt werden. Außerdem muss für jedes Item Save aufgerufen werden.
Der Aufwand ist deutlich größer, allerdings ist dann nicht alles hart verdratet und Du hast die Kontrolle darüber, wann genau die Daten tatsächlich bearbeitet werden.

Ich hab nicht die Zeit, für Beides Code-Beispiele zu schreiben, aber ich hoffe, dass es trotzdem klar geworden ist.
Behalte einfach im Hinterkopf: ViewModel kennt Model, wie nun die Daten hin und her gelangen liegt einzig und allein in der Verantwortung vom ViewModel. Oder einer extra dafür geschrieben Klasse, die vom ViewModel genutzt wird.
Solange Du das nicht über den Haufen wirfst, ist alles gut 😄

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

5.299 Beiträge seit 2008
vor 8 Jahren

Auch auf die Gefahr hin, meinen Vorredner zu wiederholen

Frage 1: Ist es richtig das schon Logik in den Models abgearbeitet wird? Also z.B. Das beim hinzufügen eines Haus schon geprüft wird ob die Person über 18 ist? Sowas muss doch nicht ins ViewModel oder? Wenn es angezeigt werden soll, muss es ins Viewmodel.
Und ein User wird wohl angezeigt bekommen müssen, wenn eine seiner Aktionen scheitert.
Eine Möglichkeit wäre, eine Exception zu werfen. Auf jeden Fall muss das Viewmodel sich ebenfalls mit Fehleingaben auseinandersetzen - so oder so.
Frage 2: Wenn beim Setzen der FavColor einer Person alle Farben der Häuser die er besitzt geändert werden soll (Häuser werden in Lieblingsfarbe gestrichen) kann doch so ein Funktionsaufruf auch im Model bleiben und muss nicht im ViewModel als Logik abgehandelt werden. Selbe Antwort: Wenn es angezeigt werden soll, muss es ins Viewmodel.
Frage 3: jetzt ist ja in der Personenklasse schon direkt eine Liste des HouseModels vorhanden.
Wie bekomme ich aber die darin gespeicherten Häuser in ein HouseViewModel damit diese auch für den View aufgearbetiet werden.
Soll die Personenklasse keine Liste von House sondern von HouseViewModel halten? Die PersonKlasse darf keine Liste von HouseViewmodel halten, da würdest du ja die Ebenen vermischen.
Das mit den Listen ist der ganz leidige Punkt an MVVM: Nimmt man die 3-Teilung "M V VM" ernst, so folgt daraus zwangsläufig, dass sowohl in PersonViewmodel eine Liste von HouseViewmodels gehalten werden muss, als auch in PersonModel eine Liste von HouseModels.
Und es ist ein lausiges Problem, diese doppelte Listenführung konsistent zu halten.

Kannst dir auch mal diesen Thread angucken: Eine Listview verschiedene ViewModel WPF<-> MVVM - Teil/Frage2
Aus meiner Sicht, was dem Frager das Problem macht, und ihn sich ans Forum wenden lässt ist eben dieses doppelte Listenführungs-Problem.
Er bringt dann auch ein tolles MVVM-Code-Sample von einem Professor an - bestimmt 3000 Zeilen Code - es lohnt sich, das downzuloaden!.
Aber wenn mans dann auf die Listen-Haltung hin anguckt stellt man fest, dass der Professor sich um das Problem herummogelt, indem er dem Model gar keine Listen zugesteht, sondern die Listen ausschließlich im Viewmodel hält.

Ich finde das bischen geschummelt, denn so ein Model modelliert nur noch unvollständig, also ohne Hinzuziehung des Viewmodels ist das modellierte Spiel (4-Gewinnt) im Model nicht korrekt abgebildet.

Ich mogel an dem Punkt auch gerne, bei mir gibts oft überhaupt keine Models, sondern nur Viewmodels.
Weil das ist mir meist zu dumm, jegliche Logik 2-fach anzulegen - da kann die Logik im Model dann ja entfallen. Was soll ich im Model groß validieren, wenn ich im Viewmodel dasselbe Problem nochmal anfassen muss? - Das kann kein guter Stil sein.
Und dadurch, dasses entfällt wird das Model dann so dumm (Poco nennt man das glaub), dass ich gar keinen Sinn mehr darin sehe, dafür überhaupt noch Klassen anzulegen.
Womit ich die 3-schichtigkeit nicht grundsätzlich ablehne: In manchen Fällen ist es auch sinnvoll, das Model aus dem Viewmodel auszugliedern.

Und hier widerspreche ich jetzt Paladin007, der MVVM prinzipiell als einen aufwändigen Pattern findet:
Also wenn ich Viewmodels ohne Model darunter anlege, dann ist der Pattern super-einfach.
Aber es ist halt nicht die reine Lehre, wie sie in hunderten von Büchern, Artikeln, und auch beim Professor propagiert wird.

Für die reine Lehre muss wirklich jede Model-Logik, die irgendwie im View sichtbar wird, im Viewmodel ein zwietes Mal behandelt werden.
Und mit den Listen ists besonders arg, und ist eigentlich erstaunlich, dass es diese selbst-synchronisierenden Doppel-Listen, wie Paladin007 sie andeutet, dasses diese nicht schon gibt, auf GitHub oderso.

Der frühe Apfel fängt den Wurm.

2.079 Beiträge seit 2012
vor 8 Jahren

Also wenn ich Viewmodels ohne Model darunter anlege, dann ist der Pattern super-einfach.
Aber es ist halt nicht die reine Lehre, wie sie in hunderten von Büchern, Artikeln, und auch beim Professor propagiert wird.

Im Prinzip hast Du dann aber trotzdem noch das Model - es liegt eben in dem ViewModel.

Aber ich weiß, worauf Du hinaus willst, mich nervt diese Komplexität auch, sobald sich das Programm aber nicht mehr nur auf die UI bezieht und auch andere Features dazu kommen (Kommandozeilen-Schnittstelle, Import, Export, etc.) dann musst Du das ViewModel nutzen, während Du den Großteil davon aber gar nicht brauchst. Wahrscheinlich bekommst Du sogar das Problem, dass das ViewModel versucht, mit dem User zu kommunizieren, obwohl es z.B. beim Import keinen direkten User gibt, der die Dateneingabe steuert.
Auch wenn Du versuchst, das ganze mit UnitTests abzudecken, bekommst Du Probleme.

Mag sein, dass das alles recht viel Aufwand ist, mit etwas kluger Vorbereitung hält sich das aber in Grenzen und ist nicht wirklich schwer, nur etwas zaitaufwendig.

Für die reine Lehre muss wirklich jede Model-Logik, die irgendwie im View sichtbar wird, im Viewmodel ein zwietes Mal behandelt werden.

Das stimmt so meiner Meinung nach nicht ganz.
Die Logik kann auch im Model (oder Business-Layer) liegen und wird vom ViewModel nur noch genutzt. Man kann zwar nicht mit ViewModels arbeiten, aber jedes ViewModel kennt doch das Model und darüber kann man dann auch wieder mit dem ursprünglichen Model arbeiten.
Das gleiche mit der Validierung. Die muss dann allerdings klug genug aufgebaut werden, dass sie vom ViewModel auch genutzt werden kann.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

5.299 Beiträge seit 2008
vor 8 Jahren

Im Prinzip hast Du dann aber trotzdem noch das Model - es liegt eben in dem ViewModel. Nenn es wie du willst - in meiner Begrifflichkeit wäre das Unfug. Ich spreche von Model und Viewmodel nur, wenn es auch 2 verschiedene Klassen sind. Ist es nur eine muss ich ein Wort benutzen, entweder Viewmodel oder Model, oder eiglich ein neues Wort müsste gefunden werden, vlt. "Supermodel"?

Die Logik kann auch im Model (oder Business-Layer) liegen und wird vom ViewModel nur noch genutzt.

Aber dann behandelt das Viewmodel dieselbe Logik doch bereits ein zweites Mal.

Also wenn ich im Model validiere, dass die Person über 18 ist, dann muss ich im Viewmodel validieren, ob im Model validiert wurde, ob die Person über 18 ist.

Und damit ist zweimal sich mit diesem läppischen Thema beschäftigt worden - das meine ich.

Und bitte jetzt nicht in andere Schichten, UnitTests, Sterne und die Unterwelt ausgreifen, das hatte ich schon im anderen Thread.
Da war das Model am Ende auch kein BusinessLayer mehr, sondern war Präsentationsschicht, und blabla und hastenichtgesehen und vollständiger Nebel.

Business-Objekt <-> WCF Service <-> Rep. mit WCF Client -> DTO <-> Model <-> ViewModel <-> View

Darunter tuns die Leute ja nicht.

Der frühe Apfel fängt den Wurm.

I
IamTheBug Themenstarter:in
401 Beiträge seit 2006
vor 8 Jahren

Wenn es angezeigt werden soll, muss es ins Viewmodel.
Und ein User wird wohl angezeigt bekommen müssen, wenn eine seiner Aktionen scheitert.
Eine Möglichkeit wäre, eine Exception zu werfen. Auf jeden Fall muss das Viewmodel sich ebenfalls mit Fehleingaben auseinandersetzen - so oder so.

Das mache ich auch, ich werfe eine Exception. Egal wer auch jemals diese Klassen bzw das Model verwendet, er darf bestimmte Werte in bestimmten Kombinationen nicht belegen, deshalb wird das im Model schon abgeprüft und Exception geworfen. Und natürlich gut dokumentiert 😉

Das mit den Listen ist der ganz leidige Punkt an MVVM: Nimmt man die 3-Teilung "M V VM" ernst, so folgt daraus zwangsläufig, dass sowohl in PersonViewmodel eine Liste von HouseViewmodels gehalten werden muss, als auch in PersonModel eine Liste von HouseModels.
Und es ist ein lausiges Problem, diese doppelte Listenführung konsistent zu halten.

Also ist sozusagen ein "richtiger" aber umständlicher Weg das ich die original Liste der Modelle immer "umpacke" in eine Liste von ViewModellen? Klingt bisschen umständlich für im Endeffekt wenig Ergebnis, wenn das ViewModel z.B. die Daten des Models kaum bis gar nicht verändert und nur durchreicht.

Und mir ging es auch schon wie dir. Bei meinem ersten kleineren Projekt zum testen ist mein Model/Viewmodel auch verschwommen. Mein Model war hinterher so "dumm" (hatte nur string name und int age) und das Viewmodel hatte das einfach weiter durchgereicht. Da habe ich dann auch auf eine Zwischenschicht verzichtet. Es hat alles nur unnötig aufgebläht und war bei meinem kleinen Beispiel ohne Sinn.

Mfg

IamTheBug

5.299 Beiträge seit 2008
vor 8 Jahren

Also ist sozusagen ein "richtiger" aber umständlicher Weg das ich die original Liste der Modelle immer "umpacke" in eine Liste von ViewModellen? Ergibt sich doch logisch zwingend, oder?
Und mit umpacken ist nicht getan, sondern wenn du aus einer ViewmodelListe was löschst, musses auch aus der Model-Liste verschwinden.
Und zufügen nochmal das Theater.
Also wenn der User eine PersonView offen hat, und da ein Haus weghaut, dann muss im Viewmodel die PersonViewmodel-Liste das PersonViewmodel entfernen, und dann auch die PersonModel-Liste aufsuchen, und da die Person auch rauslöschen.

Wie angedeutet - da muss man wohl eine DoubletteObservableCollection<TViewmodel, TModel> erschaffen, evtl. von ObservableCollection<TViewmodel> geerbt, intern einen Verweis auf die Model-Liste enthaltend, und dann muss CollectionChanged abonniert sein, und schön aufpassen, dass die Daten immer synchron bleiben.

Aber tu dich doch mit Bezi aussm annern Thread zusammen, der sollte das doch inzwischen gelöst haben.

Der frühe Apfel fängt den Wurm.

2.079 Beiträge seit 2012
vor 8 Jahren

Es hat alles nur unnötig aufgebläht und war bei meinem kleinen Beispiel ohne Sinn.

Vermutlich ist genau das das Problem.

Ich hab in meiner Ausbildung mit einer ziemlich umfangreichen Software gearbeitet.
Dort ist eine peinlich genaue Beachtung von MVVM notwendig um es nicht noch konplizierter zu machen. Mit der kleinen Command-Ausnahme, jeden noch so kleinen Command in eine eigene Klasse zu packen finde ich idiotisch 😄

In kleinen (Beispiel-) Projekten wirst Du den Vorteil nie sehen, nur die Nachteile.
Wenn das Projekt auf lange Sicht mal größer werden soll, kann es aber dennoch sinnvoll sein, den Mehraufwand in Kauf zu nehmen um sich dann später umfangreiche Umbauten zu ersparen.
Bleibt es klein, dann ist es tatsächlich überflüssig, Model und ViewModel zu trennen.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.