Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 14 Min.

MVVM mal ganz einfach

Warum man manchmal einfach von vorn anfangen muss.
Das Entwurfsmuster Model-View-ViewModel, besser bekannt unter seinem Kürzel MVVM, ist inzwischen auch außerhalb der .NET-Welt sehr beliebt. Besonders im Bereich der Cross-Plattform-Entwicklung, aber auch bei vielen Java­Script-Frameworks für Single-Page-Anwendungen, wie zum Beispiel Knockout.js, kann MVVM bei der Entwicklung einer guten Architektur helfen. Selbst für Android und Angular stehen nützliche Implementierungen für das MVVM-Pattern zur Verfügung.Warum fällt die Wahl ausgerechnet auf das MVVM-Pattern? Mit der Implementierung lässt sich der Code deutlich vereinheitlichen und kompakter gestalten. Dadurch lassen sich die Abhängigkeiten zwischen Benutzeroberfläche und Geschäftslogik verringern. MVVM erlaubt eine viel bessere Trennung von User-Interface-Design und User-Interface-Logik.

Frameworks

Neben dem eigentlichen Architekturmuster MVVM erfreuen sich auch etliche MVVM-Frameworks einer steigenden Beliebtheit bei den Entwicklern. Mithilfe eines MVVM-Frameworks lassen sich die typischen Fälle einer MVVM-Applikation wesentlich leichter und effektiver umsetzen als ohne ein solches Framework.Es gibt inzwischen eine Vielzahl von MVVM-Frameworks, die aber auch einen gewissen Overhead an Funktionalität und Umfang mit sich bringen, sodass Sie als Entwickler gezwungen sind, eventuell nicht nur das Entwurfsmuster ­MVVM zu erlernen, sondern auch noch die Besonderheiten des gewählten Frameworks erforschen müssen.So nützlich, praktisch und zeitsparend MVVM-Frameworks auch sind, so schwierig und ineffektiv können sie für Neueinsteiger sein. Das liegt in der Natur der Sache, denn Frameworks bringen eine Vielzahl von Funktionen mit, von denen niemand, wirklich niemand alle benötigt.Daher wird bisweilen durchaus der Anschein erweckt beziehungsweise vielfach beklagt, dass durch die verfügbaren Frameworks die Programmierung unnötig kompliziert würde. Der Umgang mit einem MVVM-Framework ist ohne eine entsprechende Lernkurve nicht zu bewältigen. Vielleicht inspiriert gerade dieser Umstand dazu, ein eigenes, übersichtliches MVVM-Framework zu programmieren und dabei die Zusammenhänge im Detail zu verstehen.Dieser Workshop versteht sich als reines Lernprojekt. Vorgestellt wird die Umsetzung von MVVM unabhängig von der UI-Technologie, die für WPF, UWP und Xamarin, aber auch Windows Forms gleichermaßen geeignet ist. Beeinflusst wurde das Lernprojekt von diversen anderen Frameworks wie ­etwa Caliburn, Cinch, MVVM Light Toolkit und Prism.

Das MVVM-Pattern

In der Praxis geht es darum, Abhängigkeiten zwischen Benutzeroberfläche und Geschäftslogik zu verringern. MVVM reiht sich in die bekannten Architekturmuster MVC (Model-View-Controller), MVP (Model-View-Presenter) und Presentation Model ein. Diese dienen Entwicklern als Vorlage, um grafische Oberflächen standardisiert und strukturiert zu implementieren.Das Grundprinzip besteht darin, die drei Schichten Model, View und ViewModel zu trennen. Da im Gegensatz zu MVC die Referenzierung nicht innerhalb des Controllers zur View, sondern innerhalb der View zum ViewModel erfolgt, kann MVVM als eine Weiterentwicklung bewährter Architekturmuster betrachtet werden. Das MVVM-Pattern erlaubt einfach eine noch viel bessere Trennung von User-Interface-Design und der benötigten User-Interface-Logik.MVVM ermöglicht also eine besonders gute lose Kopplung zwischen View und View-Model. Um diesen Vorteil auch nutzen zu können, benötigt man eine gewisse Infrastruktur für die Umsetzung. Das Pattern besteht aus den folgenden drei Hauptkomponenten:
  • Model: das Datenmodell,
  • View: die Sicht, also die Programmoberfläche (das UI-Design),
  • ViewModel: das Schichtmodell, das die Logik für die Benutzeroberfläche enthält.
Bild 1 zeigt die Abhängigkeiten von MVVM. Die View (Sicht) nutzt das ViewModel und die ViewModel-Schicht das Model. Umgekehrt bestehen keine Abhängigkeiten.
Das heißt, dass nur die übergeordnete Komponente die untergeordnete kennt. Somit kennt das Model weder das ViewModel noch die View.Die View ist für die Darstellung der Informationen zuständig und enthält die entsprechenden grafischen Elemente. Jeder View wird eine ViewModel-Klasse zugeordnet, welche die darzustellenden Daten und die Logik enthält. Somit werden praktisch alle Benutzereingaben mittels Binding an das ViewModel weitergeleitet und dort weiterverarbeitet.Im Model werden die Klassen zusammengefasst, die sich um das Laden und Speichern der Daten kümmern. Es enthält somit auch die Geschäftslogik. Insgesamt ergibt sich über das MVVM-Pattern eine strikte Trennung von Oberfläche und UI-Logik.Die grundsätzliche Struktur einer MVVM-Applikation sollte immer folgende Ansätze besitzen:
  • Pro View wird ein Interface angelegt, in dem die Funktion definiert wird, die das ViewModel benötigt, um die View steuern zu können.
  • Das ViewModel erhält zur Laufzeit ein Objekt, das dieses Interface implementiert.
  • Jede View erhält zur Laufzeit eine Instanz seines ViewModels, um so auf die Daten zugreifen zu können.
Mit dieser grundsätzlichen Struktur lässt sich dann sehr einfach die Interaktion im MVVM umsetzen.Der wichtigste Aspekt, der MVVM zu einem wirklich guten Muster macht, ist die Datenbindungsinfrastruktur. Durch das Binden von Eigenschaften einer View an ein ViewModel kommt es zur losen Kopplung zwischen den beiden, wodurch in einem ViewModel kein Code geschrieben werden muss, der eine View direkt aktualisiert.Somit hilft MVVM von vornherein beim Strukturieren der Anwendung und durch Komponententests in den ViewModel-Klassen. Daher kann auch das Verhalten der Benutzeroberfläche durch Regressionstests automatisiert werden. Das spart eine Menge Zeit und Testaufwand. Weitere Informationen zu MVVM finden Sie unter [1].

Aufgabenstellung

Laut Lehrmeinung setzt sich ein Framework typischerweise aus einem Skelett beziehungsweise Rumpf und einer Bibliothek für verschiedene Lösungen eines bestimmten Lösungsbereichs zusammen.Grundsätzlich erfordert es bei der Entwicklung eines Frameworks, die benötigte Schnittmenge verschiedener Lösungen eines Bereichs zu ermitteln und separat zu modellieren. Für das Design eines Frameworks sind Verwendbarkeit, Stabilität und Erweiterbarkeit die maßgebenden Kriterien. Auch erfordert die benötigte Struktur einen gewissen Aufwand, da das ganze Framework später auf dieser Struktur basiert, daher sollte diese auf jeden Fall in der Entwicklungsphase eine sehr hohe Priorität haben. Änderungen und Anpassungen an der Struktur eines Frameworks sind im Nachhinein sehr schwierig und zeitaufwendig.Ein Framework ist immer eine Abstraktion dessen, wie Code organisiert ist und wie das gesamte Programm im Allgemeinen funktionieren sollte.Hierbei ist es unbedingt notwendig, sich mit den Abhängigkeiten innerhalb des Frameworks zu beschäftigen. Diese Abhängigkeiten entstehen, sobald ein Code-Bestandteil einen anderen verwendet. Die Bestandteile bezeichnen Methoden, Klassen und Namensräume, aber auch allgemein benutzte und benötigte Assemblies. Daher ist eine saubere Struktur beim Aufbau eines Frameworks unverzichtbar.Die Aufgabenstellung beziehungsweise das Grundkonzept eines MVVM-Frameworks besteht zunächst auf der Abgrenzung der Komponenten und deren Abhängigkeiten von ­MVVM. Bild 2 zeigt einmal klar die Verteilung der abgegrenzten Komponenten.
Dementsprechend können bei MVVM die Verarbeitungsprozesse getrennt betrachtet werden. Die View präsentiert das User Interface, realisiert das Look-and-feel, stellt Informationen dar und kommuniziert mit dem ViewModel bei Veränderungen.Das ViewModel ist die Brücke zwischen View und Model. Es enthält die Präsentationslogik, informiert die View über Veränderung der Daten und reagiert auf Benutzeraktionen.Das Model wiederum steht für die Datenhaltung. Es repräsentiert die Daten, abstrahiert von der Datenquelle und kann auch Validierungsmechanismen beinhalten.Dies sind also die ersten Grundgedanken zu den drei Teilen Model, View und ViewModel für das Framework. Da es sich um ein Lernprojekt handelt, wird das MVVM-Framework erst einmal nur auf das Wesentliche beschränkt. Es beinhaltet somit auch:
  • keinen Code-behind,
  • keine Event-Übertragungen,
  • keine Data Templates,
  • keine asynchrone Programmierung,
  • keine Custom Controls und
  • keine Libraries von Drittanbietern.
Folglich ist das Framework auf das Einfachste reduziert und es wird auf eine unnötige Komplexitätssteigerung verzichtet. Sie können das Framework aber zu jeder Zeit nach Lust und Laune erweitern.Wie Sie bei der Aufgabenstellung von MVVM ersehen können, eignet sich der Aufbau grundsätzlich für eine starke Modularisierung.Demzufolge können Sie für das Framework das Konstruktionsprinzip Separation of Concerns anwenden.Bei diesem Prinzip wird die Verantwortlichkeit unter Modulen aufgeteilt. Bild 3 zeigt die Aufteilung anhand von Zuständigkeiten. So kann man auf einfache Art und Weise Komponenten, die an der gleichen Aufgabe beteiligt sind, gruppieren und von denen abgrenzen, die für andere Aufgaben zuständig sind.
Damit ist das Prinzip der Modularisierung als Kriterium erfüllt und es werden die verschiedenen Zuständigkeiten voneinander getrennt. Daher können sich diese auch unabhängig voneinander ändern, was die Entwicklung eines Frameworks einfacher gestaltet.Somit ist also die genaue Aufgabenstellung für das Framework gegeben. Es soll eine kompakte und einfache, aber dennoch leistungsfähige Implementierung des MVVM-Patterns entstehen.Bild 4 zeigt eine grobe Strukturübersicht über die Elemente des Frameworks. Hieraus wiederum lässt sich ableiten, dass ein grundlegendes MVVM-Framework im Wesentlichen aus den folgenden zentralen Dingen besteht:
  • Eine Klasse, die entweder ein Dependency Object ist oder INotifyPropertyChanged implementiert, um Data Binding vollständig zu unterstützen.
  • Klassen und Methoden für die Funk­tionsunterstützung des zu realisierenden Frameworks.
Durch diese ersten einfachen, aber wesentlichen Dinge entkoppelt das Framework die Funktionalität des Betriebssystems von der Anwendungslogik.Da Aufgabenstellung sowie Framework als Lernprojekt ausgelegt sind, wird versucht, die Komplexität in Grenzen zu halten, indem die erste Variante für die Windows Presenta­tion Foundation (WPF) ausgelegt wird.Eine Erweiterung auf eine andere UI-Technologie, wie zum Beispiel die Universal Windows Platform (UWP), kann jederzeit durchgeführt werden. Sie können Ihr Framework pro­blemlos auf weitere Bedürfnisse anpassen.Für das MVVM-Framework ergeben sich somit erst einmal folgende Annahmen:
  • Das ViewModel kann wiederverwendet werden.
  • Das ViewModel ist unabhängig vom User Interface.
  • Die Code-behind-Datei für die Datenbindung stellt kein ViewModel dar.
Des Weiteren ergeben sich für die Klassen des ViewModels folgende Eigenschaften:
  • Sie implementieren INotifyPropertyChanged.
  • Gegebenenfalls werden sie von Dependency Object abgeleitet.
  • Sie implementieren eventuell Dependency Properties.
  • Sie verwenden Observable Collections.
  • Sie lösen im Setter PropertyChanged-Events aus.
  • Sie laufen ausschließlich im User-Interface-Thread.
  • Sie verarbeiten Events und ermöglichen das Validieren des User Interface.
  • Sie lesen und schreiben im Model.
Die Klassen des Models können über folgende Eigenschaften verfügen:
  • Modellieren der Funktionalität der Anwendung unabhängig von einem konkreten User Interface.
  • Sie verwenden keine User-Interface-Funktionalität.
  • Die Model-Klassen lesen und schreiben Daten anhand eines Data Providers.
  • Die Model-Klassen laufen je nach Aufgabenstellung eventuell asynchron.
  • Sie können automatisiert getestet werden.
  • Sie validieren die Daten im Gesamtkontext.
Weiterhin soll das Model im Framework wiederverwendbar sein und keinen User-Interface- oder Datenbank-Code verwenden.Des Weiteren wäre es auch möglich, Tests für das Model gleich zu Projektbeginn zu schreiben.

Inversion of Control (IoC)

Da das hier vorgestellte MVVM-Framework in einzelne Module aufgeteilt ist, lässt sich das Konzept von Inversion of Control, kurz IoC, für das Erstellen einer losen gekoppelten Architektur einsetzen.IoC bezeichnet ein Muster in der objektorientierten Programmierung, bei dem mehrfach verwendete Klassen einer Anwendung eine spezifische Klasse aufrufen. Durch dieses Konzept ergibt sich eine Umkehrung des Befehlsflusses. Das heißt, beim MVVM-Framework wartet der Code des Entwicklers darauf, aktiv zu werden.Inversion of Control stellt nur das Konzept bereit. Um die lose Kopplung zu erreichen, kann man auf Dependency Injection (DI) zurückgreifen. Dependency Injection (DI) ist eine Methode, um Abhängigkeiten, die ein Objekt besitzt, erst zur Laufzeit bereitzustellen. Dependency Injection bezieht sich auf das Übergeben der Abhängigkeiten einer Klasse oder Methode als Parameter, anstatt diese Beziehungen über Aufrufe von new oder static hart zu codieren. Demzufolge nutzt das MVVM-Framework folgende Entwurfsmuster:
  • MVVM,
  • Command,
  • Dependency Injection (DI),
  • Inversion of Control (IoC),
  • Separation of Concerns (SoC).
Das ist erst einmal genug an Theorie. Nachdem durch die Aufgabenstellung klargeworden ist, was für die Architektur des MVVM-Frameworks zu tun ist, lässt sich jetzt die Beispielanwendung definieren.

Die Anwendung

Neben dem Aufbau der Framework-Struktur soll die Anwendung unter WPF auch die Ansicht einer einfachen Datenstruktur ermöglichen. Um die Komplexität des Beispiels gering zu halten, bestehen die Daten aus einer Fahrzeugliste mit Marke, Typ und einer Kilowatt-Angabe. Des Weiteren soll die Ansicht folgende Bedingungen erfüllen.
  • Das Beispiel besteht aus einer Hauptansicht.
  • In der Ansicht sollen die Daten in einer Tabelle (DataGrid) angezeigt werden.
  • Des Weiteren sollen die Marke in einer ListBox und einer Combobox auswählbar beziehungsweise darstellbar sein. Der Eintrag beziehungsweise die Auswahl soll dann jeweils markiert werden.
  • Über eine einfache Textbox und einen Button kann neuer Text hinzugefügt werden.
Somit ist die erste kleine Aufgabenstellung für das Framework definiert. Daher können Sie jetzt mit der Erstellung beziehungsweise Implementierung des MVVM-Frameworks beginnen.

MVVM-Implementierung

Doch wie erstellt man überhaupt ein Framework für ­MVVM mit Visual Studio? Auch hierfür gibt es mehrere richtige Lösungen. Sie können mit einer komplett leeren Solu­tion (Blank Solution) beginnen und alles von Hand erstellen, oder – hier habe ich mich ein wenig an MVVM Light orientiert – mit
einer ersten einfachen Projektvorlage für die entsprechende UI-Technologie beginnen.Daher wird für das Lernprojekt als Erstes eine WPF-Vorlage erstellt, in der Sie das MVVM-Muster implementieren.Erstellen Sie deshalb eine neue Solution (Grundlage WPF-App) mit der in Bild 5 gezeigten Projektstruktur. Somit besteht die Solution aus einem Projekt MVVMFramework.Core, das die gemeinsame Codebasis für alle späteren Projekte enthält. Als Vereinfachung bei der Erstellung wird es als ausführbare WPF-App erstellt und enthält somit auch schon die Angabe der View auf Basis der Windows Presentation Foundation (WPF).
Als Erstes erstellen Sie das benötigte MVVM-Model. Das Model ist für das Laden und Speichern der Daten zuständig. Es enthält die Entitätsklasse PocoVehicle.cs aus Listing 1. Dieses einfache Plain Old CLR Object (POCO) dient dabei als Datenklasse.
Listing 1: Die Klasse PocoVehicle
<span class="hljs-keyword">namespace</span> <span class="hljs-title">MvvMFramework.Model</span> <br/>{ <br/>  <span class="hljs-keyword">class</span> <span class="hljs-title">PocoVehicle</span> <br/>  { <br/>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Marke { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } <br/>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Typ { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } <br/>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> Kilowatt { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } <br/>  } <br/>}  
Da die Klassen- und Eigenschaftsbezeichnungen denen der Entitäten und Eigenschaften entsprechen, ist die Klasse auf das Interagieren mit der simulierten Datenbankschicht ausgelegt.Weiterhin verfügt das Model über die Klasse Vehicle.cs. Diese implementiert das Interface INotifyPropertyChanged sowie die Getter- und Setter-Methoden für Marke, Typ und Kilowatt. Listing 2 zeigt die vollständige Implementierung der Klasse Vehicle.
Listing 2: Die Klasse Vehicle
<span class="hljs-keyword">using</span> System.ComponentModel; <br/><br/><span class="hljs-keyword">namespace</span> <span class="hljs-title">MvvMFramework.Model</span> <br/>{ <br/>  <span class="hljs-keyword">class</span> <span class="hljs-title">Vehicle</span> : <span class="hljs-title">INotifyPropertyChanged</span> <br/>  { <br/>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">event</span> PropertyChangedEventHandler <br/>      PropertyChanged; <br/><br/>    <span class="hljs-keyword">string</span> _Marke; <br/>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Marke <br/>    { <br/>      <span class="hljs-keyword">get</span> <br/>      { <br/>        <span class="hljs-keyword">return</span> _Marke; <br/>      } <br/>      <span class="hljs-keyword">set</span> <br/>      { <br/>        <span class="hljs-keyword">if</span> (_Marke != <span class="hljs-keyword">value</span>) <br/>        { <br/>          _Marke = <span class="hljs-keyword">value</span>; <br/>          RaisePropertyChanged(<span class="hljs-string">"Marke"</span>); <br/>        } <br/>      } <br/>    } <br/><br/>    <span class="hljs-keyword">string</span> _Typ; <br/>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Typ <br/>    { <br/>      <span class="hljs-keyword">get</span> <br/>      { <br/>        <span class="hljs-keyword">return</span> _Typ; <br/>      } <br/>      <span class="hljs-keyword">set</span> <br/>      { <br/>        <span class="hljs-keyword">if</span> (_Typ != <span class="hljs-keyword">value</span>) <br/>        { <br/>          _Typ = <span class="hljs-keyword">value</span>; <br/>          RaisePropertyChanged(<span class="hljs-string">"Typ"</span>); <br/>        } <br/>      } <br/>    } <br/><br/>    <span class="hljs-keyword">int</span> _Kilowatt; <br/>    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> Kilowatt <br/>    { <br/>      <span class="hljs-keyword">get</span> <br/>      { <br/>        <span class="hljs-keyword">return</span> _Kilowatt; <br/>      } <br/>      <span class="hljs-keyword">set</span> <br/>      { <br/>        <span class="hljs-keyword">if</span> (_Kilowatt != <span class="hljs-keyword">value</span>) <br/>        { <br/>          _Kilowatt = <span class="hljs-keyword">value</span>; <br/>          RaisePropertyChanged(<span class="hljs-string">"Kilowatt"</span>); <br/>        } <br/>      } <br/>    } <br/><br/>    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">RaisePropertyChanged</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> prop</span>) </span><br/><span class="hljs-function">    </span>{ <br/>      PropertyChanged?.Invoke(<span class="hljs-keyword">this</span>, <br/>        <span class="hljs-keyword">new</span> PropertyChangedEventArgs(prop)); <br/>    } <br/>  } <br/>}  
Als nächsten Zwischenschritt können Sie im Ordner Data­Access die Datenbankzugriffe für das Framework implementieren. In diesem Bereich modellieren Sie beispielsweise die benötigten Datenbank-Entitäten sowie den Datenzugriff auf eventuelle OR-Mapper. Bild 6 zeigt den grundsätzlichen Aufbau der Datenbankschicht.
Für das Beispiel wird eine simulierte Auflistung erstellt, die von der Klasse ObservableCollection<T> abgeleitet wird.Es handelt sich in diesem Fall um eine einfache Datenauflistung. Des Weiteren erstellen Sie eine stark typisierte Liste von Objekten, in diesem Fall PocoVehicle, auf die über einen Index zugegriffen werden kann. So erhalten Sie Methoden zum Durchsuchen, Sortieren und Bearbeiten der Liste.Listing 3 zeigt die Implementierung von ObservableCollection <Vehicle> und List<PocoVehicle> in der Klasse Data­base­Layer. Über diese Klasse wird im Fallbeispiel der Datenbankzugriff simuliert.
Listing 3: Die Klasse DatabaseLayer
&lt;span class="hljs-keyword"&gt;using&lt;/span&gt; System.Collections.ObjectModel; &lt;br/&gt;&lt;span class="hljs-keyword"&gt;using&lt;/span&gt; MvvMFramework.Model; &lt;br/&gt;&lt;br/&gt;&lt;span class="hljs-keyword"&gt;namespace&lt;/span&gt; &lt;span class="hljs-title"&gt;MvvMFramework.DataAccess&lt;/span&gt; &lt;br/&gt;{ &lt;br/&gt;  class DatabaseLayer &lt;br/&gt;  { &lt;br/&gt;    public static ObservableCollection&amp;lt;Vehicle&amp;gt; &lt;br/&gt;      GetVehicleFromDatabase() &lt;br/&gt;    { &lt;br/&gt;      return new ObservableCollection&amp;lt;Vehicle&amp;gt; &lt;br/&gt;      { &lt;br/&gt;        new Vehicle { Marke="Audi", Typ="A 5", &lt;br/&gt;                      Kilowatt=140 }, &lt;br/&gt;        new Vehicle { Marke="BMW", Typ="M 240i", &lt;br/&gt;                      Kilowatt=294 }, &lt;br/&gt;        new Vehicle { Marke="Mercedes", Typ="C 350e", &lt;br/&gt;                      Kilowatt=155 }, &lt;br/&gt;      }; &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    public static List&amp;lt;PocoVehicle&amp;gt; &lt;br/&gt;      GetPocoVehicleFromDatabase() &lt;br/&gt;    { &lt;br/&gt;      return new List&amp;lt;PocoVehicle&amp;gt; &lt;br/&gt;      { &lt;br/&gt;        new PocoVehicle { Marke="Audi", Typ="A 5",&lt;br/&gt;                          Kilowatt=140 }, &lt;br/&gt;        new PocoVehicle { Marke="BMW", Typ="M 240i", &lt;br/&gt;                          Kilowatt=294 }, &lt;br/&gt;        new PocoVehicle { Marke="Mercedes", &lt;br/&gt;                          Typ="C 350e", Kilowatt=155}, &lt;br/&gt;      }; &lt;br/&gt;    } &lt;br/&gt;  } &lt;br/&gt;}  

Helpers

Im Ordner Helpers werden die für die Umsetzung von ­MVVM erforderlichen Hilfsprogramme zusammengefasst. Im ersten Schritt benötigen Sie nur die Klasse RelayCommand. Diese kapselt die für ein Command-Objekt erforderlichen Infor­mationen und stellt die Verbindung zwischen der Command-Eigenschaft eines Steuerelements und einer auszuführenden Methode her. Listing 4 zeigt die Implementierung der Klasse RelayCommand.
Listing 4: Die Klasse RelayCommand
using System; &lt;br/&gt;using System.Windows.Input; &lt;br/&gt;&lt;br/&gt;namespace MvvMFramework.Helpers &lt;br/&gt;{ &lt;br/&gt;  public class RelayCommand : ICommand &lt;br/&gt;  { &lt;br/&gt;    #region Fields &lt;br/&gt;    readonly Action&amp;lt;object&amp;gt; _execute; &lt;br/&gt;    readonly Predicate&amp;lt;object&amp;gt; _canExecute; &lt;br/&gt;    #endregion &lt;br/&gt;&lt;br/&gt;    #region Constructors &lt;br/&gt;    public RelayCommand(Action&amp;lt;object&amp;gt; execute) &lt;br/&gt;      : this(execute, null) &lt;br/&gt;    { &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    public RelayCommand(Action&amp;lt;object&amp;gt; execute, &lt;br/&gt;      Predicate&amp;lt;object&amp;gt; canExecute) &lt;br/&gt;    { &lt;br/&gt;      if (execute == null) &lt;br/&gt;        throw new ArgumentNullException(„execute"); &lt;br/&gt;&lt;br/&gt;      _execute = execute; &lt;br/&gt;      _canExecute = canExecute; &lt;br/&gt;    } &lt;br/&gt;    #endregion &lt;br/&gt;&lt;br/&gt;    #region ICommand Members &lt;br/&gt;    public bool CanExecute(object parameter) &lt;br/&gt;    { &lt;br/&gt;      return _canExecute == null ? true : &lt;br/&gt;            _canExecute(parameter); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    public event EventHandler CanExecuteChanged &lt;br/&gt;    { &lt;br/&gt;      add { CommandManager.RequerySuggested += value; } &lt;br/&gt;      remove { CommandManager.RequerySuggested -= &lt;br/&gt;              value; } &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    public void Execute(object parameter) &lt;br/&gt;    { &lt;br/&gt;      _execute(parameter); &lt;br/&gt;    } &lt;br/&gt;    #endregion &lt;br/&gt;  } &lt;br/&gt;}  
Das heißt, Commands können an diverse Steuerelemente gebunden werden, im Beispiel an die Controls Button, Combobox, ListBox und DataGrid, die bei bestimmten Ereignissen ein Command auslösen. Dafür müssen alle Command-Klassen das Interface ICommand implementieren.Das Interface definiert eine Execute- und eine CanExecute-Methode. Zusätzlich wird noch ein CanExecuteChanged-Event definiert. Der Übergabeparameter vom Typ Object kann dann über das Binding definiert werden.Im Beispiel nutzen Sie den CommandManager von WPF, an den der CanExecuteChanged-Event delegiert wird. Bitte beachten Sie, dass der hier aufgezeigte Delegierungsbefehl WPF-spezifisch ist. Für andere UI-Technologien muss die Klasse entsprechend erweitert werden, oder Sie erstellen eine DelegateCommandBase-Klasse, die unterschiedliche User Interfaces unterstützt. Dazu lohnt sich auch ein Blick auf das MVVM-Framework Prism [2] und die dort enthaltenen Command-Klassen.

ViewModel

Das MVVM-ViewModel ist das Bindeglied zwischen View und Model. Es enthält eine Basisklasse ViewModelBase, die das Interface INotifyPropertyChanged implementiert und so die Verbindung für das .NET Data Binding schafft.In dieser Klasse lassen sich die Gemeinsamkeiten der verschiedenen ViewModels zusammenfassen. Die konkreten ViewModels, wie im Beispiel die Hauptansicht, erben von dieser Klasse. Listing 5 zeigt den für die Klasse ViewModel­Base notwendigen Code.
Listing 5: Die Klasse ViewModelBase
using System; &lt;br/&gt;using System.ComponentModel; &lt;br/&gt;using System.Windows; &lt;br/&gt;using System.Windows.Threading; &lt;br/&gt;&lt;br/&gt;namespace MvvMFramework.ViewModel &lt;br/&gt;{ &lt;br/&gt;  class ViewModelBase : INotifyPropertyChanged &lt;br/&gt;  { &lt;br/&gt;    public event PropertyChangedEventHandler &lt;br/&gt;      PropertyChanged; &lt;br/&gt;    internal void RaisePropertyChanged(string prop) &lt;br/&gt;    { &lt;br/&gt;      PropertyChanged?.Invoke(this, &lt;br/&gt;        new PropertyChangedEventArgs(prop)); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    bool? _CloseWindowFlag; &lt;br/&gt;    public bool? CloseWindowFlag &lt;br/&gt;    { &lt;br/&gt;      get { return _CloseWindowFlag; } &lt;br/&gt;      set &lt;br/&gt;      { &lt;br/&gt;        _CloseWindowFlag = value; &lt;br/&gt;        RaisePropertyChanged("CloseWindowFlag"); &lt;br/&gt;      } &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    public virtual void CloseWindow(&lt;br/&gt;      bool? result = true) &lt;br/&gt;    { &lt;br/&gt;      Application.Current.Dispatcher.BeginInvoke(&lt;br/&gt;        DispatcherPriority.Background, &lt;br/&gt;        new Action(() =&amp;gt; &lt;br/&gt;      { &lt;br/&gt;        CloseWindowFlag = CloseWindowFlag == null &lt;br/&gt;          ? true &lt;br/&gt;          : !CloseWindowFlag; &lt;br/&gt;      })); &lt;br/&gt;    } &lt;br/&gt;  } &lt;br/&gt;}  
Wie Sie im Listing erkennen können, nutzt der PropertyChanged-Event den Null-conditional-Operator. Der Event lässt sich auf diese Weise ganz einfach auf not null prüfen, indem ein Fragezeichen an den Event gehängt wird. Die In­voke-Methode wird somit nur dann ausgeführt, wenn der Event nicht null ist.Das verwendete CloseWindowFlag ist als Option implementiert und ermöglicht das Reagieren auf das Schließen einer View.Die Klasse MainViewModel führt eine Liste der aktiven ViewModels und Methoden und Properties, die zur Laufzeit an die Controls gebunden werden. Listing 6 zeigt den benötigten Code der Klasse für das Anwendungsbeispiel. Alle Informationen, der in der View angezeigt werden sollen, werden in der Klasse MainViewModel über öffentliche Eigenschaften bereitgestellt.
Listing 6: Die Klasse MainViewModel
using System; &lt;br/&gt;using System.Collections.ObjectModel; &lt;br/&gt;using MvvMFramework.Model; &lt;br/&gt;using MvvMFramework.Helpers; &lt;br/&gt;using MvvMFramework.DataAccess; &lt;br/&gt;&lt;br/&gt;namespace MvvMFramework.ViewModel &lt;br/&gt;{ &lt;br/&gt;  class ViewModelMain : ViewModelBase &lt;br/&gt;  { &lt;br/&gt;    public ObservableCollection&amp;lt;Vehicle&amp;gt; &lt;br/&gt;      Fahrzeug { get; set; } &lt;br/&gt;    public RelayCommand &lt;br/&gt;      AddVehicleCommand { get; set; }    &lt;br/&gt;&lt;br/&gt;    object _SelectedFahrzeug; &lt;br/&gt;    public object SelectedFahrzeug &lt;br/&gt;    { &lt;br/&gt;      get &lt;br/&gt;      { &lt;br/&gt;        return _SelectedFahrzeug; &lt;br/&gt;      } &lt;br/&gt;      set &lt;br/&gt;      { &lt;br/&gt;        if (_SelectedFahrzeug != value) &lt;br/&gt;        { &lt;br/&gt;          _SelectedFahrzeug = value; &lt;br/&gt;          RaisePropertyChanged("SelectedFahrzeug"); &lt;br/&gt;        } &lt;br/&gt;      } &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    string _TextProperty1; &lt;br/&gt;    public string TextProperty1 &lt;br/&gt;    { &lt;br/&gt;      get &lt;br/&gt;      { &lt;br/&gt;        return _TextProperty1; &lt;br/&gt;      } &lt;br/&gt;      set &lt;br/&gt;      { &lt;br/&gt;        if (_TextProperty1 != value) &lt;br/&gt;        { &lt;br/&gt;          _TextProperty1 = value; &lt;br/&gt;          RaisePropertyChanged("TextProperty1"); &lt;br/&gt;        } &lt;br/&gt;      } &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    public ViewModelMain() &lt;br/&gt;    { &lt;br/&gt;      Fahrzeug = &lt;br/&gt;        DatabaseLayer.GetVehicleFromDatabase(); &lt;br/&gt;      AddVehicleCommand = &lt;br/&gt;        new RelayCommand(AddVehicle); &lt;br/&gt;    } &lt;br/&gt;&lt;br/&gt;    void AddVehicle(object parameter) &lt;br/&gt;    { &lt;br/&gt;      if (parameter == null) return; &lt;br/&gt;      Random kw = new Random(); &lt;br/&gt;      Fahrzeug.Add(new Vehicle { &lt;br/&gt;        Marke = parameter.ToString(), &lt;br/&gt;        Typ = parameter.ToString(), &lt;br/&gt;        Kilowatt = kw.Next(40,600) }); &lt;br/&gt;    } &lt;br/&gt;  } &lt;br/&gt;}  
Die Properties sind in der Klasse MainViewModel so aufgebaut, dass sie bei jeder Änderung den Event PropertyChanged auslösen.

View

Zum Schluss benötigen Sie nur noch eine passende View für die gewünschte Ansicht der Steuerelemente. Dazu erstellen Sie im Ordner View die Datei MainWindow.xaml (Listing 7). Da für das Beispiel eine WPF-Vorlage verwendet wurde, können Sie die Beispielanwendung auch über [F5] starten. Die App sollte dann so aussehen wie in Bild 7 dargestellt.
Listing 7: Die Datei MainWindow.xaml
&amp;lt;Window x:Class="MvvMFramework.View.MainWindow" &lt;br/&gt;  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/&lt;br/&gt;        presentation" &lt;br/&gt;  ...&lt;br/&gt;  xmlns:local="clr-namespace:MvvMFramework.View" &lt;br/&gt;  xmlns:vm="clr-namespace:MvvMFramework.ViewModel" &lt;br/&gt;  mc:Ignorable="d" &lt;br/&gt;  Title="WPF Beispiel MVVM" Height="400" Width="650" &lt;br/&gt;  DataContext="{DynamicResource ViewModelMain}"&amp;gt; &lt;br/&gt; &amp;lt;Window.Resources&amp;gt; &lt;br/&gt;  &amp;lt;vm:ViewModelMain x:Key="ViewModelMain"/&amp;gt; &lt;br/&gt; &amp;lt;/Window.Resources&amp;gt; &lt;br/&gt; &amp;lt;Grid&amp;gt; &lt;br/&gt;  &amp;lt;Grid.RowDefinitions&amp;gt; &lt;br/&gt;  &amp;lt;RowDefinition Height="0.939*"/&amp;gt; &lt;br/&gt;  &amp;lt;RowDefinition Height="Auto"/&amp;gt; &lt;br/&gt;  &amp;lt;/Grid.RowDefinitions&amp;gt; &lt;br/&gt;  &amp;lt;GroupBox Header="Darstellung in WPF" &lt;br/&gt;    HorizontalAlignment="Center" &lt;br/&gt;    VerticalAlignment="Center" Margin="145,66,161,67" &lt;br/&gt;    Width="336"&amp;gt; &lt;br/&gt;  &amp;lt;Grid&amp;gt; &lt;br/&gt;    &amp;lt;Grid.RowDefinitions&amp;gt; &lt;br/&gt;    &amp;lt;RowDefinition /&amp;gt; &lt;br/&gt;    &amp;lt;RowDefinition Height="Auto"/&amp;gt; &lt;br/&gt;    &amp;lt;/Grid.RowDefinitions&amp;gt; &lt;br/&gt;    &amp;lt;ScrollViewer VerticalScrollBarVisibility="Auto"&amp;gt; &lt;br/&gt;    &amp;lt;StackPanel&amp;gt; &lt;br/&gt;      &amp;lt;StackPanel Orientation="Horizontal"&amp;gt; &lt;br/&gt;      &amp;lt;ListBox ItemsSource="{Binding Fahrzeug}" &lt;br/&gt;        SelectedItem="{Binding SelectedFahrzeug}" &lt;br/&gt;        DisplayMemberPath="Marke" &lt;br/&gt;        HorizontalAlignment="Left"/&amp;gt; &lt;br/&gt;      &amp;lt;DataGrid ItemsSource="{Binding Fahrzeug}" &lt;br/&gt;        SelectedItem="{Binding SelectedFahrzeug}" &lt;br/&gt;        HorizontalAlignment="Left" &lt;br/&gt;        Margin="5,0,0,0"/&amp;gt; &lt;br/&gt;      &amp;lt;/StackPanel&amp;gt; &lt;br/&gt;&lt;br/&gt;      &amp;lt;ComboBox ItemsSource="{Binding Fahrzeug}" &lt;br/&gt;        SelectedItem="{Binding SelectedFahrzeug}" &lt;br/&gt;        DisplayMemberPath="Marke" Margin="5,0,0,5"&lt;br/&gt;        VerticalAlignment="Top"/&amp;gt; &lt;br/&gt;      &amp;lt;TextBox x:Name="tb1" Text=&lt;br/&gt;        "{Binding TextProperty1, UpdateSourceTrigger=&lt;br/&gt;        PropertyChanged}" Margin="5"/&amp;gt; &lt;br/&gt;      &amp;lt;TextBlock FontWeight="Bold" Margin="5" &lt;br/&gt;        Text="Eingabe in die Textbox: "&amp;gt;&lt;br/&gt;      &amp;lt;Run Text="{Binding TextProperty1}"/&amp;gt;&lt;br/&gt;      &amp;lt;Run Text=""/&amp;gt;&lt;br/&gt;      &amp;lt;/TextBlock&amp;gt; &lt;br/&gt;    &amp;lt;/StackPanel&amp;gt; &lt;br/&gt;    &amp;lt;/ScrollViewer&amp;gt; &lt;br/&gt;    &amp;lt;Button Grid.Row="1" Content="Marke hinzufügen" &lt;br/&gt;      Command="{Binding AddVehicleCommand}" &lt;br/&gt;      CommandParameter="{Binding Text, &lt;br/&gt;      ElementName=tb1}" Margin="5" &lt;br/&gt;      focusable="False"/&amp;gt; &lt;br/&gt;  &amp;lt;/Grid&amp;gt; &lt;br/&gt;  &amp;lt;/GroupBox&amp;gt; &lt;br/&gt; &amp;lt;/Grid&amp;gt; &lt;br/&gt;&amp;lt;/Window&amp;gt;  

Das Framework erstellen

In diesem Lernprojekt wurde zunächst lediglich vorgeführt, wie Sie das Konzept Model-View-ViewModel als Muster in einer App verwenden können. Jetzt können Sie beginnen, Ihr eigenes Framework zu abstrahieren.Die Beispielanwendung beinhaltet die wichtigsten Bestandteile für ein Framework. Die einzelnen Ordner haben dabei unterschiedliche Zuständigkeiten.Das heißt, um den Kern (Core) des Frameworks abzubilden, muss die Abstraktion auf dieser Schicht entsprechend hoch sein. So ergibt sich für das Core-Projekt als eigenständige Library der in Bild 8 gezeigte Aufbau. Darunter fallen alle gemeinsamen Dateien und Ressourcen, die zum Kern des Frameworks gehören und keinerlei Referenzen auf das Framework selbst benötigen.
Im Bereich Service werden die Grundlagen für das Framework definiert und verwaltet. Infolgedessen können dann der Datenbankzugriff als MvvMFramework.DataAccess sowie die einzelnen UI-Technologien für das Framework ausgearbeitet werden.Wie Sie an der Abstraktion erkennen können, wächst das Framework mit jedem weiteren Entwicklungsschritt, und die Komplexität steigt. Bitte beachten Sie, dass es sich beim hier vorgestellten Fallbeispiel nur um ein Lernprojekt handelt. Sie können gerne auch Ihre eigene Projektstruktur favorisieren, da jetzt ja bekannt ist, wie das MVVM-Muster in einem Framework funktionieren muss.Das MVVM-Framework soll die Funktionalität des Betriebssystems von der Anwendungslogik entkoppeln, indem es ein leistungsfähiges Servicesystem bereitstellt, um auch Code von der Anwendungslogik zu abstrahieren. Des Wei­teren sollte es für das Framework nur eine untergeordnete Rolle spielen, welche Art von Inversion-of-Control-Container (IoC) Sie verwenden.Sie können auch das Testen der UI-Logik mittels Unit-Tests durchführen. Automatisierte Tests können in einem großen Projekt den benötigten Testaufwand erheblich reduzieren.Um das ViewModel in einem Unit-Test testen zu können, müssen Sie Mock-Objekte verwenden, welche die View-Interfaces implementieren.

Fazit

Dieser kleine Workshop will Sie animieren, ein eigenes Framework zu entwickeln – und sei es nur zu Lernzwecken.Die Umsetzung gelingt hier schon allein mit .NET-Bordmitteln, zeigt gleichzeitig aber auch, dass die Komplexität des Projekts sehr schnell ansteigen kann. Dafür steht ein Framework auf der anderen Seite für  Vorzüge wie Wiederverwendbarkeit, Erwei­terbarkeit, Flexibi­lität, Fehlertoleranz und Modularisierung.Allerdings kann der Workshop nicht alle Besonderheiten oder speziellen Implementierungen berücksichtigen, es wurde lediglich versucht, Ihnen Appetit auf den Entwurf und das Entwickeln eines eigenen MVVM-Frameworks zu machen. Dazu wünsche ich Ihnen viel Spaß und Erfolg.
Projektdateien herunterladen

Fussnoten

  1. Wikipedia, Model View ViewModel, http://www.dotnetpro.de/SL1804MVVMSelbstbau1
  2. Prism, https://github.com/PrismLibrary

Neueste Beiträge

DWX hakt nach: Wie stellt man Daten besonders lesbar dar?
Dass das Design von Websites maßgeblich für die Lesbarkeit der Inhalte verantwortlich ist, ist klar. Das gleiche gilt aber auch für die Aufbereitung von Daten für Berichte. Worauf besonders zu achten ist, erklären Dr. Ina Humpert und Dr. Julia Norget.
3 Minuten
27. Jun 2025
DWX hakt nach: Wie gestaltet man intuitive User Experiences?
DWX hakt nach: Wie gestaltet man intuitive User Experiences? Intuitive Bedienbarkeit klingt gut – doch wie gelingt sie in der Praxis? UX-Expertin Vicky Pirker verrät auf der Developer Week, worauf es wirklich ankommt. Hier gibt sie vorab einen Einblick in ihre Session.
4 Minuten
27. Jun 2025
„Sieh die KI als Juniorentwickler“
CTO Christian Weyer fühlt sich jung wie schon lange nicht mehr. Woran das liegt und warum er keine Angst um seinen Job hat, erzählt er im dotnetpro-Interview.
15 Minuten
27. Jun 2025
Miscellaneous

Das könnte Dich auch interessieren

UIs für Linux - Bedienoberflächen entwickeln mithilfe von C#, .NET und Avalonia
Es gibt viele UI-Frameworks für .NET, doch nur sehr wenige davon unterstützen Linux. Avalonia schafft als etabliertes Open-Source-Projekt Abhilfe.
16 Minuten
16. Jun 2025
Mythos Motivation - Teamentwicklung
Entwickler bringen Arbeitsfreude und Engagement meist schon von Haus aus mit. Diesen inneren Antrieb zu erhalten sollte für Führungskräfte im Fokus stehen.
13 Minuten
19. Jan 2017
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige