Laden...

Korrekte MVVM implementierung

Erstellt von Rioma vor 9 Jahren Letzter Beitrag vor 9 Jahren 3.683 Views
R
Rioma Themenstarter:in
228 Beiträge seit 2013
vor 9 Jahren
Korrekte MVVM implementierung

Hallo zusammen,

ich habe momentan leider ein Problem mit der Implementierung des MVVM Entwurfsmusters, bzw. bin ich mir nicht sicher ob dies der richtige weg ist.

Ich habe mir jetzt einfach mal ein Beispiel ausgedacht um es zu verdeutlichen:

Das Programm ist ein Vokabeltrainer. Es gibt Vokabelgruppen die eine Liste von Vokabeln enthalten. Die Vokabeln treten niemals ohne Gruppe auf.

Model:


public class Vokabel{
 
 public string Übersetzung {get; set;}
 public string Vokabelwort {get; set; }

 public Vokabel(string übersetzung, string vokabelwort){
    this.Übersetzung = übersetzung;
    this.Vokabelwort  = vokabelwort;
 }

}

public class Vokabelgruppe{
  
  public string Name {get; set; }
  public List<Vokabel> Vokabeln;

  public Vokabelgruppe (string name){
     this.Name = name;
     Vokabeln = new List<Vokabel>();
  }

}

Da die Vokabeln nie ohne Gruppen auftreten, soll das ganze auch über die Vokabelgruppen
gebunden werden.

Für mein Verständnis hätte ich also nur ein VokabelgruppenViewmodel.

Habt ihr dann im ViewModel direkt eine Observablecollection<Vokabelgruppen> (es kann mehrere geben), oder ein 2tes ViewModel und dann eine Observablecollection<VokabelgruppenViewmodel>? Ich habe bereits beide Implementierungen gesehen, sehe aber keine Vor und Nachteile.

Nun gibt es zum Beispiel 2 Listboxen, in einer die Gruppen und in der anderen die dazugehörigen Vokabeln. Wie implementiert ihr nun diese Abhängigkeit richtig?

Ich habe im ViewModel zusätzlich eine Observablecollection<Vokabel> und meine gerade selektierte Gruppe als Eigenschaft. Im setter hole ich mir aus der Gruppe die List<Vokabel> und erzeuge eine neue Observablecollection<Vokabel>. Ich muss hier nur dafür sorgen, dass die Listen bei Änderungen immer Synchron sind. Habt ihr hierzu vielleicht auch einen bewährten Ansatz?

Ich setze übrigens kein Framework ein, da ich erst den Umgang mit dem Entwurfsmuster "beherrschen" möchte. Sollte aber ein Framework ein guten Ansatz für dieses Szenario haben, gucke ich es mir natürlich gerne an.

Danke euch

189 Beiträge seit 2014
vor 9 Jahren

Hallo Rioma,

also wenn man so will könnte man sagen, dass das ViewModel nix anderes ist als deine View in Code-Form.
Eine ObservableCollection von VM ist nur sinnvoll, wenn du viele UserControls hast.
Hast du aber ein UserControl, in dem du eine Liste deiner Gruppen darstellen möchtest, dann hast du eine OC der Gruppen-Klasse, die du dann in z.B. eine ListBox darstellst.

Du hast nun deine 2 Listboxen. Die sind ja Inhalt einer View -> ein VM.
Im Prinzip würde ich das alles so ähnlich wie du machen.
Bezüglich Synchronität: ist es wirklich möglich, dass während der Datennutzung die jemand die Vokabeln ändert?

R
Rioma Themenstarter:in
228 Beiträge seit 2013
vor 9 Jahren

Danke für deine Antwort.

Was genau meinst du mit "während der Datennutzung"?

Es kann auch sein, dass ich in meinem ViewModel die Eigenschaften des Models falsch "publiziere" und ich deswegen auf Probleme stoße.

Ich habe ein kleines Beispiel schnell zusammen geschrieben (Ohne IDE).


public class Vokabel{

public string Übersetzung {get; set;}
public string Vokabelwort {get; set; }

public Vokabel(string übersetzung, string vokabelwort){
    this.Übersetzung = übersetzung;
    this.Vokabelwort  = vokabelwort;
}

}

public class Vokabelgruppe{

  public string Name {get; set; }
  public List<Vokabel> Vokabeln;

  public Vokabelgruppe (string name){
     this.Name = name;
     Vokabeln = new List<Vokabel>();
  }

}

public class ViewModel{

public Vokabelgruppe SelectedGruppe 
{
	get{
		return _selectedGruppe;
	}
	set{
		   if (Equals(value, _selectedGruppe)) return;
                _selectedGruppe = value;

				Vokabeln = new ObservableCollection<Vokabel>(_selectedGruppe.Vokabeln);
				
                OnPropertyChanged();
	}	
}

public Vokabel SelectedVokabel
{
	get{
		return _selectedVokabel;
	}
	set{
		   if (Equals(value, _selectedVokabel)) return;
                _selectedVokabel = value;

                OnPropertyChanged();
	}	
}

private ObservableCollection<Vokabelgruppe> _gruppen;
public ObservableCollection<Vokabelgruppe> Gruppen
{
    get { return _gruppen; }
    set{
         if (Equals(value, _gruppen)) return;
            _gruppen = value;
            OnPropertyChanged();
     }
}

private ObservableCollection<Vokabeln> _vokabeln;
public ObservableCollection<Vokabeln> Vokabeln
{
    get { return _vokabeln; }
    set{
         if (Equals(value, _vokabeln)) return;
            _vokabeln = value;
            OnPropertyChanged();
     }
}

//InotifyPropertyChanged ist implementiert --> Eigenschaften der Vokabel
public string Übersetzung {get; set;}
public string Vokabelwort {get; set; }

}

Könntest du mir erklären, wie du vorgehen würdest, wenn du nun die Eigenschaften von SelectedVokabel in der View ändern/anzeigen möchtest?

Direktest Binding auf SelectedVokabel.Übersetzung scheint mir sehr unschön zu sein. Jedes mal im Setter von SelectedVokabel die Eigenschaften raus zu holen und in Eigenschaften des ViewModels zu schreiben halte ich für auch nicht besser.

Ich habe auch Implementierungen gesehen, bei denen die Eigenschaft durchgereicht wird -->


   public string Übersetzung 
        {
            get { return _selectedVokabel.Übersetzung ; }
            set
            {
                if (value == _selectedVokabel.Übersetzung ) return;
                _selectedVokabel.Übersetzung = value;
                OnPropertyChanged();
            }
        }

Allerdings bekommt meine View so natürlich nichts mit, wenn sich _selectedVokabel ändert (Die View aktualisiert nicht)

Ich hoffe es war verständlich und du kannst du weiterhelfen.

Danke

189 Beiträge seit 2014
vor 9 Jahren

Hallo Rioma,

ich meinte mit "während der Datennutzung" den (gleichzeitigen) Mehrfachzugriff auf die Datenbank.

Wenn du nur die Eigenschaften "durchreichen möchtest" kannst du direkt binden. Weshalb findest du das unschön?
Du kannst natürlich auch nochmal Properties im VM definieren:


class VM
{
    public Vokabel SelectedVokabel
    {
        get ...
        set
        {
             selectedVokabel = value;
             OnPropertyChanged("SelectedVokabel");
             OnPropertyChanged("VokabelWort");
             OnPropertyChanged("VokabelUebersetzung");
        }
    }

    public string VokabelWort
    {
        get
        {
            return this.SelectedVokabel.Wort;
        }
    }
...
}

Wenn du es unbedingt so machen möchtest ... . Wenn du aber nicht nur rein durchreichst, sondern modifizierst, wirst du es wiederum so machen müssen.

R
Rioma Themenstarter:in
228 Beiträge seit 2013
vor 9 Jahren

Danke für die verständliche Erklärung. Mehrfachzugriff auf die Datenbank ist ein berechtigter Einwand den ich nicht beachtet habe. Es geht aber auch nicht um ein Produktiv-System sondern eher um die richtige Umsetzung um mir nicht in der Zukunft mal ein Bein zu stellen. Eine kleine Frage hätte ich noch:

Stell dir vor du hast eine Observablecollection<Model>. Die Daten kommen aus einer CSV oder XML-Datei (lediglich ein einziges Model). Die Daten sollen bearbeitet werden können und am Ende alle zurück geschrieben werden.

Wie würdest du das zurück schreiben bewerkstelligen? Es geht nicht um den technischen Part sondern eher darum, woher die Daten kommen die geschrieben werden sollen.

Würdest du die Observablecollection<Model> als Parameter durchreichen? Oder auf der Business-Schicht eine List<Model> cachen und mit den Änderungen synchronisieren und diese dann zurück schreiben? Oder ein völlig anderer Weg?

An sich wahrscheinlich eine dämliche Frage aber mir stellen sich irgendwie die Nackenhaare auf wenn ich eine Observablecollection<Model> nach unten durchreiche um die Daten zu schreiben.

Ich hoffe du versteht meinen "komischen" Gedankengang und kannst Licht ins Dunkel bringen.

189 Beiträge seit 2014
vor 9 Jahren

Wenn du schon von "Business-Schicht" sprichst, hast du dir deine Antwort z.T. selbst gegeben, bist nur am falschen Ende herausgekommen. 😉
Nach der [Artikel] Drei-Schichten-Architektur hast du deine Präsentationsschicht, in der deine Oberfläche mit MVVM Pattern liegt; die Business-Schicht, in der du die Daten verarbeitest; und die Datenzugriffsschicht.
Und genau dort hin gehört der Zugriff auf Dateien, Datenbanken usw.
-> Deine Nackenhaare stellen sich zu recht auf, wenn du dem DAL eine OC runterreichst. Was will das DAL mit einer OC? 😉
Da du hier ein nicht-verteiltes System baust, reicht schon die Variante mit der List<>.
Eventuell sieht ein erfahrenerer Entwickler als ich es bin eine bessere Lösung.
Noch ein Hinweis: Falls du die Möglichkeit siehst, die Datenhaltung in absehbarer Zeit zu ändern oder du sowieso mehrere Datenhaltungsverfahren nutzt (z.B. .txt-Datei + DB), ist es extrem wichtig hier ein gutes, allgemeines Interface zu implementieren, um diese Schicht gut austauschen zu können.

R
Rioma Themenstarter:in
228 Beiträge seit 2013
vor 9 Jahren

Danke nochmal. Das heißt also ungefähr so ein Aufbau:

VM --> BL --> DAL

Dazwischen liegen dann natürlich interfaces bzw. wird mit dem Repository-Pattern gearbeitet um die jeweilige Schicht ohne Probleme austauschen zu können.

Nur mal Ein Beispiel:


public class ViewModel{

private ObservableCollection<Vokabel> _vokabeln;
public ObservableCollection<Vokabel> Vokabeln
{
    get { return _vokabeln; }
    set{
         if (Equals(value, _vokabeln)) return;
            _vokabeln = value;
            OnPropertyChanged();
}

private void GetAllVokabeln(){
  Vokabeln = new ObservableCollection<Vokabel>( businessLayer.GetVokabeln());

}

private void SaveAllVokabeln(){
  businessLayer.Save();

}

}


public class BusinessLayer{

IVokabelRepository rep = new VokabelRepository();

public List<Vokabeln> Vokabeln{get; set;}

public List<Vokabeln> GetVokabeln(){
  //Möglicherweise irgendwelche "Business Aufgaben"

  Vokabeln = rep.GetAll();
  return Vokabeln;
}

public void Save(){
  //Möglicherweise irgendwelche "Business Aufgaben"
  rep.Save(Vokabeln);
}


Ich hoffe du verstehst das Beispiel. Hier muss sich natürlich noch um die Synchronisation gekümmert werden, zwischen der Collection im VM und im BL.

Wenn eine Datenbank dahinter liegt, schreibe ich eigentlich jede Änderung an einem Model weg. In diesem Beispiel soll es sich um irgendeine Datei handeln (csv, xml oder ähnliches), die nur am ende mit einem Aktiven Click über ein SpeicherCommand gespeichert werden soll.

Ist es denn dann wirklich Sinnvoll die Collection zusätzlich im BL zu halten? Oder sieht jemand einen "richtigeren" Ansatz?

189 Beiträge seit 2014
vor 9 Jahren

Hallo Rioma,

jo, ich glaube das passt so. 👍
Über Sinn oder Unsinn des BL ohne vorhandene Logik kann man sich streiten. (ist u.a. Thema in dem Beitrag Diskussion zum Artikel: Drei-Schichten-Architektur)
Letztendlich musst du das entscheiden.
Wenn man aber von Anfang an sauber die 3 Schichten umsetzt, hat man's zum einen geübt, zum anderen hält man sich die Option offen das Programm weiter auszubauen ohne gleich wieder alles umstellen zu müssen. 👍

Da bisher keiner der "alten Hasen" eingeschritten ist, dürfte ich nicht allzuviel Quatsch erzählt haben. 😉

R
Rioma Themenstarter:in
228 Beiträge seit 2013
vor 9 Jahren

Danke für die Antworten! Es gibt im Internet leider so viele unterschiedliche Meinungen zu Entwurfsmustern, dass man nur sehr schwer den Einstieg findet.

S
417 Beiträge seit 2008
vor 9 Jahren

Es gibt im Internet leider so viele unterschiedliche Meinungen zu Entwurfsmustern, dass man nur sehr schwer den Einstieg findet.

Ich denke für Einsteiger ist es immer schwerer die Zusammenhänge zu sehen, aber es sollte zu den meisten gängigen Prinzipen keine unterschiedlichen Meinungen geben.
Prinzipien wie Kapselung, Single Responsibility Principle (SRP) oder Dependency Injection sind nunmal sinnvoll.
In deinem gezeigten Code werden leider einige Prinzipien verletzt bzw. nicht angewandt:1.In deiner BusinessLayer Klasse ist die interne Struktur Vokabeln (List<Vokabeln>) für Aufrufer sichtbar. Durch einen Aufruf von Vokabeln.Clear(); löscht ein anderes Objekt dann mal eben alle Vokabeln -> nicht gut 1.Durch IVokabelRepository rep = new VokabelRepository(); gewinnst du leider garnichts. Solange du die Abhängikeit IVokabelRepository nicht von aussen (über den Konstruktor) injezieren lässt, bringt die Verwendung eines Interfaces nicht viel. 1.Die Klasse BusinessLayer war hoffentlich nur beispielhaft so benannt, denn deinen kompletten Business Layer wirst/solltest du nicht in ein Klasse packen um SRP nicht zu verletzen.

Edit:
Noch zu deiner Frage "Ist es denn dann wirklich Sinnvoll die Collection zusätzlich im BL zu halten? Oder sieht jemand einen "richtigeren" Ansatz?"
Ich denke es kommt darauf an was du mit der Collection noch vor hast.
Der Aufruf rep.Save(Vokabeln); gefällt mir z.B. nicht. Hier übergibst du ja alle Vokabeln. Das mag für einen Datei-basierten Ansatz ausreichen, wo du alle Vokabeln auf einmal als Datei wegschreibst.
Aber wenn du z.B. später eine Datenbank verwenden möchtest, ist der Ansatz nicht mehr so toll.
Denn dann muss sich der DAL darum kümmern, welche Einträge in der DB aktualisiert werden soll. Das ist nicht die Aufgabe des DAL -> SRP.
Es wäre besser eine ChangeTracker-Komponente zu bauen, welche die Änderung des Benutzers kennt.
Die geänderten Einträge könntest du dann im Business Layer an die Save-Methode übergeben und dann weiter an den DAL delegieren.

R
Rioma Themenstarter:in
228 Beiträge seit 2013
vor 9 Jahren

Danke auch für deine Antwort.

Wenn ich mit einer Datenbank arbeite, schreibe ich eigentlich alle Änderungen über einfache C(R)UD Methoden im Repository weg. Es geht hier tatsächlich nur um eine Datei, die einmal weggeschrieben werden soll.

Keine sorge auch die Beispiele habe ich hier nur schnell ohne IDE runter geschrieben. Aber die Anmerkungen sind natürlich Sinnvoll, sonst guckt sich das noch jemand ab. So weit hatte ich anfangs nicht gedacht.

Eine Frage hätte ich noch an dich. Wie du sagst werden ja eigentlich DL und BL injected.
Das heißt ich müsste meinem ViewModel schon den BL mitgeben. Wenn ich den Bl erzeuge, muss ich dann aber auch schon den DL mitgeben. Macht ihr dies alles an einer stelle?

S
417 Beiträge seit 2008
vor 9 Jahren

Macht ihr dies alles an einer stelle?

Das übernimmt typischerweise ein DI-Framework, welches die Komponenten erzeugt und Abhängigkeiten injeziert.

Die Frage ist auch, ob das ViewModel den BL kennen sollte. Das ViewModel ist ja nur für die Anzeige der Daten verantwortlich.
Ich würde die Vokabeln dem ViewModel übergeben, und nicht dem ViewModel die Aufgabe überlassen, die Vokabeln aus dem BL zu extrahieren.

R
Rioma Themenstarter:in
228 Beiträge seit 2013
vor 9 Jahren

Danke. Darf ich Fragen, wie du dir das vorstellst mit dem Übergeben?

Die Daten vorher holen und dann dem ViewModel beim erzeugen mit in den Konstruktor geben, oder noch anders?

Ich hatte den Businesslayer eigentlich zwischen VM und DAL und hielt es für richtig. Wie würdest du denn vorgehen, wenn der Benutzer Daten nachladen können soll? Button ist an ein Command im VM gebunden.

S
417 Beiträge seit 2008
vor 9 Jahren

Die Daten vorher holen und dann dem ViewModel beim erzeugen mit in den Konstruktor geben, oder noch anders?

Ja.

Das ViewModel darf nur wissen was es für Daten hat, aber nicht wie es an die Daten kommt.
Stell dir vor, das laden der Vokabeln dauert lange, dann müsstest du innerhalb des ViewModels z.B. eine asynchrone Verarbeitungslogik einbauen, damit die UI nicht einfriert.
Das ist nicht Aufgabe des ViewModels.

Wie würdest du denn vorgehen, wenn der Benutzer Daten nachladen können soll? Button ist an ein Command im VM gebunden.

Das ViewModel hat ja eine ObservableCollection, d.h. ein Command kümmert sich um das Nachladen der Vokabeln (evtl. sogar asynchron) und fügt diese dann der ObservableCollection hinzu. Dadurch wird das ViewModel automatisch aktualisiert.

R
Rioma Themenstarter:in
228 Beiträge seit 2013
vor 9 Jahren

Stell dir vor, das laden der Vokabeln dauert lange, dann müsstest du innerhalb des ViewModels z.B. eine asynchrone Verarbeitungslogik einbauen, damit die UI nicht einfriert. Das ist nicht Aufgabe des ViewModels.

[....]

Das ViewModel hat ja eine ObservableCollection, d.h. ein Command kümmert sich um das Nachladen der Vokabeln (evtl. sogar asynchron) und fügt diese dann der ObservableCollection hinzu. Dadurch wird das ViewModel automatisch aktualisiert.

Ich hatte eine asynchrone ICommand implementierung, die UI würde also nicht einfrieren. Allerdings sagst du oben, dass eine asynchrone Verarbeitungslogik nicht Aufgabe des ViewModels ist und unten sagst du allerdings das Gegenteil, wenn ich dich richtig verstehe.

Nur wie kommen die Daten in die Collection, wenn das ViewModel den BL nicht kennt?
Der DAL hat bei mir meistens "dumme container" erzeugt und der BL musste noch etwas daran machen und dieser hat die Daten dann an das ViewModel zurück gereicht.

S
417 Beiträge seit 2008
vor 9 Jahren

Ich sagt ein Command kümmert sich um das Nachladen, nicht das ViewModel.
Der Command holt sich die Daten aus dem BL und übergibt diese an das ViewModel.
Das ViewModel darf nicht wissen woher die Daten kommen.

R
Rioma Themenstarter:in
228 Beiträge seit 2013
vor 9 Jahren

Entschuldigung, dann habe ich dich falsch verstanden. Darf ich Fragen wie so eine Implementierung aussehen würde?

Ich habe bisher sehr oft solche Ansätze gesehen:


private BL _bl; //Businesslayer
public ICommand einCommand {get; set;}

public VM(){
   einCommand = new DelegateCommand( __ => GetIrgendwas());
   _bl = new BL(); // Bitte ignoriert die fehlende DI
}

public ObservableCollection<Irgendwas> Test {get; set;}

void GetIrgendwas(){
    Test = _bl.GetTeste();
}