Thomas Kramer

IT-COW | All posts tagged 'Observer'

Weiterentwickelte Version 2.5 meiner verteilten Java-RMI-Anwendung

By Administrator at Oktober 26, 2013 14:02
Filed Under: Projekte, Studium

Für mein Studium hatte ich einen eigenen Anwendungsserver und –Client in Java mittels der RMI-Technik entwickelt und nun hatte ich mich entschlossen das Projekt noch weiterzuentwickeln.

 

Das Programm ist eine hypothetische Projektverwaltung und hat mehr den Zweck einer Technik-Demo. Da für die Projekte nur Meta-Daten erfasst werden können erfüllt es keinen wirklichen Sinn.

 

Nachfolgend ein Überblick der technischen Merkmale der neuen Version der Anwendung.

 

Client:

 

- Model-View-Controller-Architekturmuster

- zentrale Vorhaltung von Datenobjekten im ModelController, keine Daten-Redundanzen im Arbeitsspeicher. Datenobjekte die durch die Beobachter-Logik empfangen wurden werden solange im RAM vorgehalten bis sie gebraucht werden

- Formular- und Dialogobjekte werden nur einmalig instanziiert und danach in Listen im Controller gespeichert, dadurch keine Verzögerungen bei erneutem Aufrufen von Formularen und Dialogen

- dynamisches Instanziieren über Reflection für einfaches Hinzufügen von Formularen und Dialogen, ohne dass diese dem Programm an zentralen Stellen bekannt gemacht werden müssen

- Multithreading, wichtig für die Observer-Logik

- abstrakte Basisklassen und -Methoden wo es sinnvoll ist

- clientseitige Eingabevalidierung

- volle netzwerkweite Mehrbenutzerfähigkeit, echtzeitfähig in dem Sinne dass Datenänderungen aller Benutzer sofort in allen Client-Instanzen sichtbar werden

 

Server:

 

- Versenden/Empfangen von Objekten übers Netzwerk über die Java-Technik Remote Method Invocation (RMI)

- serverseitige Eingabevalidierung mittels Bean Validation durch den Hibernate Validator

- Datenpersistierung über den O/R-Mapper EclipseLink

- Observer/Beobachter-Logik, alle Clients werden sofort über Datenänderungen informiert

- zentrale Registrierung von Objekten welche sich gerade clientseitig in Bearbeitung befinden

- volle netzwerkweite Mehrbenutzerfähigkeit

- Optimistic Locking mit Versionsfeldern

- Transaktionenkonzept

- Datenbank: HSQLDB

- Performance-Messungen mittels JAMon

- Logging durch Apaches Log4j, das Java-integrierte Standard-Logging ist nicht threadsicher

 

Um die serverseitige Datenvalidierung über Bean Validation zu testen stehen JUnit-Testmethoden zur Verfügung, die weiter unten heruntergeladen werden können. Von dem Client- und Server-Projekt sind diesmal nur die ausführbaren JAR-Dateien zum Download verfügbar.

 

Die technischen Änderungen der Version 2.5 sind im Überblick:

 

Clientseitige Neuerungen

 

- Vollständige Mehrbenutzer-Logik

 

In der vorherigen Version hatte ich bereits das kaskadierende Löschen von Datensätzen in der Datenbank abgefangen, in der neuen Version kamen weitere Funktionalitäten hinzu um vollständige Mehrbenutzerfähigkeit zu ermöglichen.

 

Dazu war es zunächst notwendig neben dem Studenten und Dozenten auch eine Administrator-Rolle als Login-Profil hinzuzufügen. Konkrete Passwörter können nicht vergeben werden, aber die Anwendung hat auch mehr den Charakter einer Technik-Demo.

 

Ich beschreibe einmal anhand von ein paar Testfällen was ich unter Mehrbenutzerfähigkeit verstehe:

 

1. Dozent/Student B möchte einen Datensatz bearbeiten den Dozent/Student A bereits bearbeitet.

 

Konsequenz: Benutzer B erhält noch vor dem Öffnen des Ändern-Fensters eine Fehlermeldung dass dieser Datensatz bereits zur Bearbeitung gesperrt ist.

 

In Bezug auf die initialen Ändern-Berechtigungen stehen Dozent und Student zunächst auf derselben Ebene. Ein Dozent kann aber nicht mal eben den Ansprechpartner eines Projektes ändern, das ein Student eingereicht hat. Er kann aber ein Projekt mit einem Kommentar ablehnen und der Student kann es dann mit einem neuen Ansprechpartner (und ergänztem Titel) erneut anlegen und zur Bewilligung einreichen.

 

Es gibt keinen Grund warum ein Dozent bei der Bearbeitung desselben Datensatzes höhere initiale Rechte als die Studenten haben sollte. Der Dozent setzt nur den Status eines Projekts und kommentiert dieses, aber die Studenten verbringen die meiste Zeit mit der Bearbeitung des Projektes.

 

Im Zweifelsfall kann ein Dozent den Administrator veranlassen dass ein zur Bearbeitung markiertes Projekt geschlossen wird, damit er den Status setzen und es kommentieren kann.

 

2. Administrator B möchte einen Datensatz bearbeiten den Dozent/Student A bereits bearbeitet.

 

Konsequenz: Der Administrator kann den Datensatz bearbeiten wohingegen der Dozent/Student A sofort die Mitteilung darüber bekommt und dessen Fenster automatisch geschlossen wird, denn ein Administrator hat natürlich übergeordnete Rechte.

 

3. Zwei Administratoren möchten gleichzeitig denselben Datensatz bearbeiten.

 

Konsequenz: Dem zweiten Administrator wird das Bearbeiten erlaubt und dem ersten Administrator das Fenster unmittelbar mit einer Fehlermeldung geschlossen.

 

Es kann immer nur ein Benutzer gleichzeitig einen Datensatz bearbeiten, ansonsten gibt es datenbankseitig OptimisticLockExceptions. Das proaktive Schließen von Fenstern Anderer ist völlig richtig.

 

4. Eine mehr theoretische Möglichkeit: Zwei Anwender egal welchen Typs bearbeiten gleichzeitig denselben Datensatz und einer veranlasst das Speichern.

 

Praktisch wird das durch die Fälle 1-3 bereits vorher abgefangen, aber die Logik dazu ist in meiner Anwendung ebenfalls vorhanden. Sobald ein Anwender auf speichern klickt werden die gleichen Fenster anderer Benutzer automatisch mit einer Fehlermeldung geschlossen, weil dieser Datensatz nun bereits in einer höheren Version vorhanden ist.

 

Wenn ich die Logik für die Testfälle 1-4 nicht eingebaut hätte würde es datenbankseitig OptimisticLockExceptions geben.

 

5. Ein Datensatz wird von Benutzer B gelöscht, den Benutzer A gerade bearbeitet.

 

Konsequenz: Das Fenster des Benutzers A wird unmittelbar mit einer Fehlermeldung geschlossen, weil der Datensatz nicht mehr existiert.

 

Die Löschen-Button-Rechte werden je nach eingeloggtem Benutzertyp gesetzt. An der Stelle könnte man noch darüber streiten ob nur der Admin oder auch der Dozent Löschen-Rechte haben sollten, bei dieser Anwendung haben es beide Benutzertypen und nur der Student hat diese Rechte nicht.

 

Spätestens wenn ein Admin einen Datensatz bearbeitet den ein anderer Admin gerade löscht greift diese Logik.

 

6. Ein Datensatz wird von Benutzer B gelöscht und ein Benutzer A bearbeitet gerade einen Datensatz der diesen referenziert.

 

Konsequenz: Das Fenster des Benutzers A wird unmittelbar mit einer Fehlermeldung geschlossen, weil der Datensatz nicht mehr existiert – meine Anwendung benutzt das kaskadierende Löschen von Datensätzen-Feature der HSQLDB-Datenbank.

 

Die Löschen-Button-Rechte werden je nach eingeloggtem Benutzertyp gesetzt. An der Stelle könnte man noch darüber streiten ob nur der Admin oder auch der Dozent Löschen-Rechte haben sollten, bei dieser Anwendung haben es beide Benutzertypen und nur der Student hat diese Rechte nicht.

 

Spätestens wenn ein Admin einen Datensatz bearbeitet den ein anderer Admin gerade löscht greift diese Logik.

 

7. Neuanlegen und Umbenennen von Datensätzen, ausgelöst durch andere Benutzer.

 

Konsequenz: Die Ansicht des Programms wird unmittelbar aktualisiert ohne dass der Anwender etwas davon mitbekommt, denn der zuvor ausgewählte Eintrag jedes einzelnen UI-Elements wird wiederhergestellt. Das betrifft sowohl die Listen-Ansichten als auch die Ändern-Fenster von Datensätzen.

 

Es gibt Echtzeit-Aktualisierungen, unmittelbare Reaktionen auf Datenveränderungen die andere Benutzer initiiert haben. Durch diese Funktionen ist meine Anwendung vollständig mehrbenutzerfähig, wie man sich denken kann war dazu auch eine Erweiterung der Server-Logik notwendig.

 

- Zentrale anwendungsweite Vorhaltung der Datenobjekte

 

Die Datenobjekte wurden aus den Formularen und Dialogen herausgezogen und werden nun zentral im ModelController vorgehalten. In meinem Client benutze ich eine Unterscheidung zwischen Controller und ModelController, für eine klare Abgrenzung.

 

Der Vorteil liegt nun darin dass die Daten nicht mehr redundant im Arbeitsspeicher des Clients vorgehalten, sondern bereits vorhandene Datenobjekte wiederverwendet werden. Weiterhin gilt natürlich dass die Daten aus der Datenbank erst bei Notwendigkeit angefordert werden.

 

Ich bevorzuge den Begriff Vorhaltung damit klar wird dass es nicht um die dauerhafte Daten-Persistierung auf einem Datenträger sondern um temporäre Daten im Arbeitsspeicher geht. Wenn die Datenobjekte dagegen in Formularen und Dialogen vorgehalten werden, also in der View-Logik, würde das unweigerlich zu Redundanzen und damit zu einem unnötigen Mehrverbrauch an Arbeitsspeicher führen.

 

Stattdessen bevorzuge ich Referenzen auf zentrale Datenobjekte, in den Views selbst befindet sich nur die notwendigste Logik.

 

- Zentrale Registrierung von UI-Objekten, Zuordnung zu den Datenobjekten

 

Das geschieht über Container-Klassen.

 

Die saubere Trennung der Datenobjekte ist nur möglich weil EclipseLink die referenzierten Datensätze anderer Tabellen automatisch mitauslesen kann (das sogenannte EAGER-Fetching) und über Getter verfügbar macht; in dem Sinne muss man auch keine einfachen Joins mehr selbst über die JPQL-Befehle ausführen, zumindest wenn Foreign-Key-Constraints in der Datenbank gesetzt wurden.

 

Statt mit lokalen Instanzen von View-Objekten wird im View selbst mit Referenzen gearbeitet, in der entsprechenden Methode der Container-Klasse wird das View-Objekt aber immer neu erzeugt. An der Stelle benutze ich dynamisches Instanziieren dieser View-Objekte, denn der Konstruktor wird über Reflection geholt.

 

Das hat auch wichtige Auswirkungen auf die Observer-Logik.

 

- Zentrale Observer-UI-Logik

 

Wenn Daten in der Datenbank geändert oder hinzugefügt/gelöscht wurden müssen andere Clients darüber informiert werden, damit die Anwendung wirklich Mehrbenutzerfähig wird. Das Feature war in V1.0 auch schon vorhanden, findet jetzt aber zentral über die Container-Klassen statt. Der Vorteil liegt darin dass sich nicht mehr jedes Formular- und Dialog-Objekt einzeln um die Aktualisierung seiner View-Elemente kümmern muss.

 

Dabei gab es natürlich ein paar Fallstricke – die Aktualisierung muss in einer bestimmten Reihenfolge geschehen, die Single-Thread-Rule von Swing darf nicht verletzt werden, gewisse Mehrbenutzerregeln müssen beachtet werden usw. Ein modifizierter RepaintManager ist sinnvoll um Verletzungen der Single-Thread-Rule zu finden, weil sie nicht zwangsläufig auf der Konsole geloggt werden.

 

- Verringerung des Einsatzes von Reflection

 

Vorher hatte ich intensiv Reflection eingesetzt um z. B. die ID eines Datensatzes/Entity auszulesen.

 

Die Namensvergabe der Entity-Getter orientiert sich bei automatisch generierten Entities an den jeweiligen Spalten der Datenbank mit der Java-typischen PascalCase-Nomenklatur für Methoden. Das bedeutet dass die getId()-Methode in der Entity nicht vorhanden ist wenn die Primary Key-Spalte in der Datenbank nicht ID heißt, denn die Primary-Key-Kennzeichnung in der Entity geschieht über die @Id-Annotation über der entsprechenden Methode.

 

Konkret habe ich eine abstrakte Basisklasse für die Entities eingeführt um auf Reflection verzichten zu können, das funktioniert übrigens auch serverseitig ohne dass dafür eine Datenbanktabelle existieren muss. Außerdem konnte ich im Client natürlich auch nicht auf Methoden des EntityManagers zugreifen um die Primärschlüssel-ID auszulesen, daher war dieser Weg notwendig.

 

Der Hauptgrund der gegen den Einsatz von Reflection spricht ist der Verzicht auf die durch die Entwicklungsumgebung erzwungene Typsicherheit, denn durch den Einsatz von Methodennamen in String-Literalen kann bei der Kompilierung nicht überprüft werden ob die Methode tatsächlich existiert. Desweiteren verliert man dabei auch die Refactoring-Unterstützung weil die Entwicklungsumgebung die Zuordnungen nicht erkennen kann.

 

Außerdem ist Reflection ziemlich langsam. Es gibt aber auch gute Gründe für Reflection, z. B. den Konstruktor dynamisch zu holen ohne Programmerweiterung oder bei Klassen die man nicht verändern kann oder darf. Wenn man schon Reflection benutzen muss sollte wenn möglich eine Basisklasse mit entsprechenden abstrakten Methoden verwendet werden damit bei abgeleiteten Klassen nicht übersehen wird diese zu implementieren.

 

- Benutzung des nativen Windows-UI-Styles und Änderung des Benutzerinterfaces

 

Auch mit Java ist es möglich die Programme wie native Windows-Anwendungen aussehen zu lassen, etwas was die Benutzer-Akzeptanz deutlich erhöhen dürfte. Nachfolgend ist die neue Optik des Programms zu sehen:

 

 

Wie im Bild bereits zu sehen ist wurde außerdem die Benutzeroberfläche geändert, es werden jetzt Karteireiter mit unteren Buttons genutzt.

 

- Sortierungsmöglichkeit der Tabellen über die Spaltenköpfe

 

Bei den Tabellen kann nun mit Klick auf die Spaltenköpfe sortiert werden. Dabei gab es auch einiges zu beachten denn bei einer Umsortierung wird z. B. das dahinterstehende Datenmodell nicht mitsortiert.

 

- Refactoring

 

Der gesamte Client wurde refactored und besser aufgebaut.

 

- Bugfixing

 

Natürlich habe ich auch einige Fehler gefunden und behoben.

 

- Download

 

RMIClient der Projektverwaltung

 

Der Client lässt sich durch einen einfachen Doppelclick auf die JAR-Datei starten, sofern die Java-Runtime korrekt installiert ist. Der Server sollte dagegen über die Kommandozeile gestartet werden und das natürlich zeitlich vor dem Client.

 

Achtung, RMI verwendet clientseitig zufällige Ports so dass auf allen Clients die Firewall deaktiviert oder alle Ports für die javaw.exe geöffnet werden sollten!

 

Der Client kann per Doppelclick nur dann gestartet werden sofern der Anwendungsserver auf demselben Rechner läuft - ansonsten muss die Eingabeaufforderung geöffnet, zu dem Pfad navigiert werden wo die entpackten Dateien liegen und über java -jar RMIClient.jar $Server-Port $Server-IP gestartet werden. Der Default-Port für RMI ist serverseitig 1099.

 

Serverseitige Neuerungen

 

- Zuordnung von IP-Adressen zu Clients über Hashtables statt umständlich über zwei ArrayListen.

 

- Zentrale Registrierung der sich clientseitig gerade in Bearbeitung befindlichen Datenobjekte.

 

- Serverseitige Eingabevalidierung auf Entity-Ebene, auch als Bean Validation bekannt.

 

Das Feature wird durch den Einsatz des Hibernate Validators erreicht. Dazu habe ich auch ein paar JUnit-Testfälle geschrieben.

 

Auf diese Weise wird vor dem Übernehmen der Datensätze überprüft ob die Pflichtfelder gesetzt sind, der übergebene Text nicht zu lang für eine Datenbankspalte ist oder die übergebene EMail-Adresse dem RegEx-Pattern entspricht.

 

Es ist wichtig dass diese Überprüfung nicht nur client- sondern auch serverseitig geschieht, denn ansonsten könnte z. B. irgendein Schadprogramm ungültige Datensätze über den Anwendungsserver in die Datenbank schreiben lassen.

 

- Der Server benutzt nun Optmistic Locking mit Versionsfeldern.

 

- Beim Überprüfen ob ein bestimmter Datensatz bereits vorhanden ist wird jetzt auf Wunsch die Groß- und Kleinschreibung ignoriert.

 

- Einführung einer DefaultSortOrder für die Entities, etwas was standardmäßig tatsächlich nicht vorgesehen ist.

 

Es ist tatsächlich möglich dem O/R-Mapper eine abstrakte Basisklasse verfügbar zu machen ohne dass diese datenbankseitig existieren muss, nur leider sind grundsätzlich keine abstrakten statischen Methoden in Java erlaubt.

 

Statt unzähligen JPQL-Befehlen für jede einzelne Entity wurde getDefaultSortOrder() als Methode in die Entities hinzugefügt. Der Aufruf erfolgt über Reflection; genaugenommen geht der Server beim Start alle Entities durch, schaut nach ob diese Methode implementiert wurde und wenn ja merkt er sich das ausgelesene Ergebnis in einer Hashtable. Gab es beim Auslesen dieser Methode eine Exception wird das Starten des Servers mit einer Fehlermeldung abgebrochen.

 

So wird Reflection benutzt, gleichzeitig dessen Fehleranfälligkeit verringert und seine Langsamkeit durch einmalige Anwendung umgangen bzw. reduziert. Ansonsten hätte es ohne eine entsprechende abstrakte statische Methode in der Basisklasse eben eine erhöhte Fehlerwahrscheinlichkeit für das Auslesen der jeweiligen DefaultSortOrder gegeben.

 

- Bei Auslieferungen von Daten über das Observer-Pattern wird der Initiator nun mit übermittelt, welcher PC die Datenänderung veranlasst hat.

 

- Download

 

RMIServer der Projektverwaltung

JUnit-Tests für die Bean Validation

 

Um den Server zu starten muss eine Eingabeaufforderung geöffnet, zu dem Pfad navigiert werden wo die entpackten Dateien liegen und java –jar rmiserver.jar eingegeben und bestätigt werden.

 

Ein Doppelklick auf die JAR-Datei funktioniert zum Starten natürlich auch, aber dann entgehen einem die Konsolenausgaben und da der Server kein Programmfenster besitzt könnte er so nur über den Taskmanager beendet werden, was die Memory-Table-Datenbank zwar nicht in einem invaliden Zustand zurücklassen aber die Lock-Datei stehen lassen würde.

 

Bei dieser Datenbank werden alle Daten zunächst temporär im Arbeitsspeicher des Servers vorgehalten und erst wenn mein Anwendungsserver beendet wird werden sie dauerhaft gespeichert. HSQLDB ist dadurch gut für das Rapid Prototyping geeignet, angenehm weil keine SQL-Tools benötigt werden und auch nichts installiert werden muss.

 

Für den Server auch alle Ports für die javaw.exe freigeben!

 

Weitere Links:

 

Von Oracle:

- Java EE 6 Tutorial: Link

- Java EE 7 Tutorial: Link

 

JPA allgemein:

- JPA Einleitung: Link

- JPA Entity Fields (auch Version Fields): Link

- Java Persistence/Inheritance: Link

- JPA Vererbung SINGLE_TABLE JOINED TABLE_PER_CLASS: Link

- Java Persistence/Locking: Link

- JPA 2.0 Concurrency and locking: Link

- Pessimistic and Optimistic Locking in JPA 2.0: Link

- Optimistic Locking with @Version: Link

- Optimistic Locking with JPA: Link

- Java-EE-Architekturen und JPA: Verwendung des EntityManager im Java-EE-Stack: Link

- JPA Best Practice-Empfehlungen: Link

- Performance Anti-Patterns: Link

- How to get the primary key of any JPA Entity: Link

- Ordered Lists in JPA: Link

 

JPA Bean Validation:

- Annotationen und Bean Validation: Link

- Java 7 – Mehr als eine Insel / Property-Validierung durch Bean Validation: Link

- Hibernate Validation Annotationen für Entity Beans: Link

- JPA2 and Bean Validation in Action: Link

- Tücken der Bean Validation in JPA 2.0: Link

- Probleme bei der Nutzung der Bean Validation in JPA: Link

 

JUnit:

- JUnit: Link

- JUnit 4 Tutorial: Link

 

HSQLDB:

- HSQLDB Data Types: Link

 

Java allgemein:

- Auslesen von Annotationen: Link

- PropertyChangeEvent: Link

 

Nachtrag

 

Gemäß unserem Dozenten unterstützt die Oberflächentechnik Swing von Java standardmäßig kein vollständiges eventgetriggertes DataBinding mit UI-Elementen.

 

Mit DataBinding werden UI-Elemente an Datenobjekte gebunden, so dass eine Änderung z. B. in einem Textfeld eine Änderung im dazugehörigen Datenobjekt hervorruft und umgekehrt. Die grundsätzlichen PropertyChangeEvents und –Listener gibt es bereits, aber scheinbar muss die Anbindung an konkrete UI-Elemente immer noch händisch durch eigenen Quellcode geschehen.

 

Es gibt ergänzende Bibliotheken die dies für Swing ermöglichen, z. B. JGoodies, welche jedoch erfordern die JPA-Klassen händisch um PropertyChange-Unterstützung zu erweitern. Alternativ dazu gibt es auch noch das Eclipse DataBinding und JFace Data Binding, welche aber nicht mit Swing funktionieren.

 

Zu einem solchen Framework zählen dann natürlich auch Validierungsregeln, wann eingegebene Daten übernommen werden dürfen. Der Hibernate Validator ist demnach eher für serverseitige Eingabevalidierung gedacht weil er erst dann greift wenn das Datenobjekt bereits existiert; die Validierungsregeln dieser Frameworks greifen aber bereits vorher bei der Eingabe.

 

Dennoch muss eine fortgeschrittene verteilte Netzwerkanwendung die Dateneingaben sowohl client- als auch serverseitig auf Gültigkeit überprüfen – schließlich könnte der eigene Client übergangen werden. Spätestens bei öffentlichen Webanwendungen dürfte das sehr wichtig sein.

 

Soweit gingen unsere Java-Vorlesungen und Praktika gar nicht dass DataBinding überhaupt vorgekommen wäre, in Swing ist es auch noch nicht vollständig implementiert. Letztenendes muss das aber auch über eine Beobachter/Observer-Logik funktionieren.

 

In meiner Anwendung funktioniert das noch nicht eventgetriggert sondern über eine eigene Beobachter-Logik. Vermutlich könnte clientseitig durch die Nutzung von JGoodies oder dem JFace Data Binding noch ein paar Zeilen Quellcode eingespart werden, aber spätestens bei eigenen Validierungsregeln scheint mir der Vorteil nicht mehr so groß zu sein und dann müssten natürlich auch die JPA-Klassen händisch angepasst werden.

 

Für den Anwender ergibt sich jedenfalls kein Unterschied, auch die Oberfläche meiner Anwendung aktualisiert sich automatisch sobald Daten geändert wurden. Bevor ich mir ein erweiterndes Framework für Swing anschaue wäre es vielleicht auch sinnvoller sich direkt mit dem Swing-Nachfolger JavaFX zu beschäftigen, nachfolgend ein Link von heise Developer dazu.

 

Eventuell werde ich später trotzdem noch auf DataBinding umstellen, aber eigentlich wäre für mich zunächst ein alternatives Weblogin wichtiger.

 

Das MVVM-Entwurfsmuster aus der .NET-Welt setzt bereits auf ein vollständiges DataBinding. Das .NET-Framework ist in ein paar Teilbereichen bereits etwas weiter (da neuer); z. B. Lamda-Expressions gibt es dort schon länger und kommen erst demnächst in Java 8 hinein. Umgekehrt hat sich MS aber auch einiges von Java abgeschaut und durfte sich auch schon Kritik von Entwicklern anhören dass sie zu häufig ihre Oberflächen-APIs wechseln.

 

Zum MVVM-Design Pattern (aus der .NET-Welt):

- Model View ViewModel: Link

- heise Developer: Model View ViewModel mit Knockout.js: Link

- Visual C+ 2012 – Das MVVM-Pattern: Link

- MVVM Tutorial: Link

- Probleme und Lösungen mit Model-View-ViewModel (MSDN): Link

- WPF-MVVM: Best Practices in der Validierung von Benutzereingaben: Link

 

Tag-Wolke

Monats-Liste