Model-View-Controller (MVC) mit JavaFX

MVC mit JavaFXJeder Programmierer kennt das: Man findet im Internet tausende Beispiele für eine neue Programmiersprache/Technologie/Framework, aber wie man eine richtige Anwendung architekturkonform programmiert steht irgendwo-.-„
Mit mehreren Anläufen und viel Erfahrung kann man irgendwann mal von einer guten Architektur reden. Aber Achtung – hier kommt die ultimative Architekturvorlage für JavaFX Anwendungen. Ich zeige in diesem Blogbeitrag, wie man schnell und einfach eine JavaFX Anwendung mit dem MVC Design Pattern programmieren kann. Natürlich kann man diesen Ansatz noch weiter ausbauen, aber die grobe Richtung sollte klar werden.

 

Stichwort: MVC

MVC steht für Model-View-Controller und ist das wohl verbreiteste Design Pattern weltweit. (Bitte nicht mit SOLID-Prinzipen verwechseln!) Das Design Pattern dient zur Strukturierung von Anwendungen und unterteilt diese in 3 Bereiche: dem Datenmodell (engl. model), der Präsentation (engl. view) und der Programmsteuerung (engl. controller).

View
  • ist für die Präsentation von Daten zuständig
  • initialisiert, organisiert und speichert alle Oberflächen Elemente
  • ist für Style und Layout zuständig
1
Controller
  • verbindet View und Model
  • enthält Steuerungslogik
  • delegierte Geschäftslogik, enthält sie aber nicht!
Model
  • speichert Daten
  • besitzt nur Getter- und Setter-Funktionen

Ziel des Model-View-Controller Patterns ist ein flexibler Programmentwurf, der eine spätere Änderung oder Erweiterung erleichtert und eine Wiederverwendbarkeit der einzelnen Komponenten ermöglicht.

 

Beispiel

projektuebersichtDas folgende Beispiel zeigt eine kleine Anwendung, die Namen von AxxG Blog Leser speichert. Auf der linken Seite sieht man eine kleine Übersicht, wie das Java-Projekt aufgebaut ist.

Über die Namensgebung der Packages kann man streiten. Ich bin der Meinung, dass die Sortierung nach Model, View und Controller absolut sinnfrei ist. Der Controller gehört in 95% aller Fälle zu seiner View. Außerdem erleicht diese Art der Sortierung, dass Hin- und Herspringen zwischen den Bereichen.

Wie man sehen kann habe ich einige Namenskonventionen. So enden Controller bei mir mit dem Zusatz „VC„, Views mit dem Zusatz „View“ und Models mit dem Zusatz „Bean„.

 

 

Einstiegspunkt – Start-Klasse

Einstiegspunkt der Anwendung ist die Start-Klasse. Diese initialisiert das Model (aktuell ein Modell für die gesamte Anwendung) und startet den ersten Controller.

public class Start extends Application {
    public static void main(String[] args) {
        launch(args);
    }
     
    @Override
    public void start(Stage primaryStage) {
    	
    	// session scope /application scope Beans initialisieren!
    	// muss von Controller zu Controller weitergegeben werden
    	DataBean dataBean = new DataBean(primaryStage);
    	

    	// Ersten Controller aufrufen
    	EingabeVC eingabeVC = new EingabeVC(dataBean);
    	eingabeVC.show();   	
    }
}

 

Das Model

Das Model speichert wie gesagt, alle Daten der Anwendung. In diesem Fall ist das nur die Stage der Anwendung und eine Map mit den Namen.

public class DataBean {	
	private Stage primaryStage = null;	
	private Map<string , String> namePwMap = null;
	
	public DataBean(Stage primaryStage) {
		this.primaryStage = primaryStage;
		this.namePwMap = new HashMap<>();
	}

	public Map<string, String> getNamePwMap() {
		return namePwMap;
	}

	public Stage getPrimaryStage() {
		return primaryStage;
	}
}

Zur Kontrolle des MVC Patterns sollte man sich die Imports der Klasse ansehen.

Das Model darf keine Referenz auf eine View oder einen Controller haben!

 

Singleton Pattern

Im Kommentarbereich haben wir bereits eine Diskussion zur Speicherung der Stage geführt. Ein weiterer Lösungsansatz, außer das viewspezifische Model, ist die Speicherung der Stage mit dem Singleton Pattern.

 

Die EingabeView

javaFX-EingabeViewDie EingabeView sieht so (linke Seite) aus und beinhaltet alle Elemente der Oberfläche. Im Konstruktor werden alle Elemente initialisiert, gestylt und gelayoutet. Außerdem werden wichtige Elemente, mit dem der Benutzer interagiert, mit Getter-Funktionen erweitert.

Quelltext

public class EingabeView{
	
	private Scene scene;
	
	private GridPane grid;
	private Text scenetitle;
	
	private Label vornameLB;
	private TextField vornameTF;
	
	private Label nachnameLB;
	private TextField nachnameTF;
	
	private Text meldungT;
	
	private Button okBtn;
	private Button addBtn;
	private HBox hbBtn;
	
	public EingabeView() {
		// Layout
		grid = new GridPane();
		grid.setAlignment(Pos.CENTER);
		grid.setHgap(10);
		grid.setVgap(10);
		grid.setPadding(new Insets(25, 25, 25, 25));

		// Ueberschrift
		scenetitle = new Text("Hallo AxxG-Leser");
		scenetitle.setFont(Font.font("Tahoma", FontWeight.NORMAL, 20));
		grid.add(scenetitle, 0, 0, 2, 1);

		// Vorname
		vornameLB = new Label("Vorname:");
		grid.add(vornameLB, 0, 1);

		vornameTF = new TextField();
		grid.add(vornameTF, 1, 1);
		
		// Nachname
		nachnameLB = new Label("Nachname:");
		grid.add(nachnameLB, 0, 2);

		nachnameTF = new TextField();
		grid.add(nachnameTF, 1, 2);

		// Buttons
		addBtn = new Button("eintragen");
		okBtn = new Button("Alle anzeigen");

		// Buttongruppe
		hbBtn = new HBox(10);
		hbBtn.setAlignment(Pos.BOTTOM_RIGHT);
		hbBtn.getChildren().add(addBtn);
		hbBtn.getChildren().add(okBtn);
		grid.add(hbBtn, 1, 4);

		// Meldung
		meldungT = new Text();
		grid.add(meldungT, 1, 6);

		scene = new Scene(grid, 300, 300);
	}

	public void show(Stage stage) {
		stage.setTitle("AxxG - JavaFX MVC Beispiel");
		stage.setScene(scene);
		stage.show();
	}


	// nur Getter von Elementen anlegen, die veraendert werden und/oder dynamisch sind 
	public TextField getVornameTF() {
		return vornameTF;
	}


	public TextField getNachnameTF() {
		return nachnameTF;
	}


	public Text getMeldungT() {
		return meldungT;
	}

	public Button getOkBtn() {
		return okBtn;
	}


	public Button getAddBtn() {
		return addBtn;
	}


}

 

Der EingabeVC

Der Controller verbindet die View und das Model. Das heißt Werte aus dem Model werden in der View gelesen oder gesetzt. Außerdem werden benötigte Eventhandler registriert und Aufgabe an die ggf. vorhandene Geschäftslogik weitergeleitet! Des Weiteren steuert der Controller, wann und auf welche UI-Maske die Anwendung wechseln soll.

Quelltext

public class EingabeVC {
	
	// Model
	private DataBean dataBean;
	
	// View
	private EingabeView view;
	
	
	public EingabeVC(DataBean dataBean) {
		this.dataBean = dataBean;
		this.view = new EingabeView();
		
		// Eventhandler registrieren
		view.getAddBtn().setOnAction(new addBtnEventHandler());	
		view.getOkBtn().setOnAction(new OkBtnEventHandler());	
	}
	
	public void show(){
		view.show(dataBean.getPrimaryStage());
	}
	
	//+++++++++++++++++++++++++++++++++++++++++++++
	// Events
	//+++++++++++++++++++++++++++++++++++++++++++++

	
	class OkBtnEventHandler implements EventHandler<actionevent>{

		@Override
		public void handle(ActionEvent e) {	
		// zur naechsten Seiten springen! 
	    	AusgabeVC ausgabeVC = new AusgabeVC(dataBean);
	    	ausgabeVC.show();   
		}
		
	}
	
	class addBtnEventHandler implements EventHandler</actionevent><actionevent>{

		@Override
		public void handle(ActionEvent e) {	
			// Meldung reseten
			view.getMeldungT().setText("");
			
			// Daten aus den Textfeldern holen
			String vname = view.getVornameTF().getText();
		    String nname = view.getNachnameTF().getText();
		    
			//Textfelder zuruecksetzen
			view.getNachnameTF().setText("");
			view.getVornameTF().setText("");
			
			// Daten testen
			if(vname.isEmpty()){
				view.getMeldungT().setText("Der Vorname fehlt!");
				return;
			}
			if(nname.isEmpty()){
				view.getMeldungT().setText("Der Nachname fehlt!");
				return;
			}
			
			// Daten hinzufuegen 
			String erg = null;
			erg = dataBean.getNamePwMap().put(nname, vname);
			
			// Meldung ausgeben
			if(erg == null){
				view.getMeldungT().setText("Leser hinzugefügt");
			}else{
				view.getMeldungT().setText("Leser bereits vorhanden");
			}
		}
		
	}

}

 

Die AusgabeView

Die AusgabeView enthält nur eine Liste und einen Button.

Quelltext

public class AusgabeView{
	
	private Scene scene;
	
	private GridPane grid;
	private Text scenetitle;
	
	private ListView<string> list;	
	
	private Button backBtn;
	private HBox hbBtn;
	
	public AusgabeView() {
		// Layout
		grid = new GridPane();
		grid.setAlignment(Pos.CENTER);
		grid.setHgap(10);
		grid.setVgap(10);
		grid.setPadding(new Insets(25, 25, 25, 25));

		// Ueberschrift
		scenetitle = new Text("AxxG-Leser Übersicht");
		scenetitle.setFont(Font.font("Tahoma", FontWeight.NORMAL, 20));
		grid.add(scenetitle, 0, 0, 2, 1);

		// Liste
		list = new ListView<>();
		list.setMinWidth(200);
		grid.add(list, 0, 1);
		
		// Button
		backBtn = new Button("zurück");

		// Buttongruppe
		hbBtn = new HBox(10);
		hbBtn.setAlignment(Pos.BOTTOM_RIGHT);
		hbBtn.getChildren().add(backBtn);
		grid.add(hbBtn, 0, 2);

		scene = new Scene(grid, 300, 300);
	}

	public void show(Stage stage) {
		stage.setTitle("AxxG - JavaFX MVC Beispiel");
		stage.setScene(scene);
		stage.show();
	}

	public ListView</string><string> getList() {
		return list;
	}

	public Button getBackBtn() {
		return backBtn;
	}
}

 

Der AusgabeVC

Genauso wie der EingabeVC steuert der AusgabeVC den UI-Maskenwechsel.

Quelltext

public class AusgabeVC {
	
	private DataBean dataBean;
	private AusgabeView view;
	
	
	public AusgabeVC(DataBean dataBean) {
		this.dataBean = dataBean;
		this.view = new AusgabeView();
		
		// Eventhandler registrieren
		view.getBackBtn().setOnAction(new backBtnEventHandler());	
	}
	
	public void show(){
		// View mit Daten befuellen
		int anz = 1;
		for (String key : dataBean.getNamePwMap().keySet()) {
			// 1: Name, Vorname
			view.getList().getItems().add(anz + ": " + key + ", " + dataBean.getNamePwMap().get(key));
			anz++;
		}		
		
		// View anzeigen
		view.show(dataBean.getPrimaryStage());
	}
	
	//+++++++++++++++++++++++++++++++++++++++++++++
	// Events
	//+++++++++++++++++++++++++++++++++++++++++++++

	
	class backBtnEventHandler implements EventHandler<actionevent>{

		@Override
		public void handle(ActionEvent e) {	
			// zur naechsten Seiten springen! 
	    	EingabeVC eingabeVC = new EingabeVC(dataBean);
	    	eingabeVC.show();   
		}
		
	}
}

 

Galerie der Anwendung

 

Download

Für Faule gibt es hier das ganze Projekt…

Java Version JDK 1.7.0_07 Inhalt gepacktes Projekt
JavaFX Version 2.2.1 Größe 14.630 KB (~15 MB)
IDE Eclipse IDE Version 4.2 Endung *.zip
Spende erforderlich Lizenz Creative Commons Lizenzvertrag

dose

Hallo Leser des AxxG Blogs,

ich möchte mich zunächst bei dir bedanken, dass du meinen Beitrag bis zum Ende gelesen hast! Ich hoffe, dass ich dir weiterhelfen konnte^^

Wie du gesehen hast, gebe ich mir sehr viele Mühe bei meinen Beiträgen und beschreibe so gut wie möglich den Sachverhalt. Jedoch kostet das mich sehr viel Zeit und monatlich werden Hosting-Gebühren fällig.

Jetzt aber genug gejammert – Falls dir mein Artikel gefallen hat, kannst du mich ja auf einen Energy-Drink einladen:)
Als Dankeschön erhältst du exklusiv ein zu 100% funktionierendes Projekt und die dazu passende Video-Erklärung!





 

Video-Erklärung

 

Andere sehr gute Ansätze

Lange Zeit sah es sehr sehr sehr dunkel für die Architektur von JavaFX-Anwendungen aus. Niemand hat sich so wirklich für das Thema interessiert. Aber seit der JAX 2013 in Mainz hat sich das Blatt gewendet. Java Größen wie Adam Bien haben sich dem Thema angenommen und entsprechende Lösungen erarbeitet. Hier ist eine kurze Linksammlung:

 

Quellen

 

Copyright © 2013 AxxG – Alexander Gräsel



15 Antworten : “Model-View-Controller (MVC) mit JavaFX”

  1. xehpuk sagt:

    Ich zitiere mal: „Das Model darf keine Referenz auf eine View oder einen Controller haben!“
    Was genau ist dann DataBean#primaryStage, wenn keine Referenz des Models auf eine View? Ich habe jetzt nicht viel Erfahrung mit JavaFX, aber das sieht mir nach einem klaren Verstoß aus.

    • Hallo Xehpuk,
      das ist eine sehr gute Frage, die ich auf Anhieb gar nicht so beantworten kann:-)
      Ich gebe dir Recht, dass die primaryStage ein Teil der GUI ist. Um genau zu sein, ist es das Hauptfenster der Anwendung (ähnlich JFrame). Also der graue Rahmen mit den 3 Buttons, Minimieren, Maximieren und Schließen.
      In diesem Hauptfenster werden die verschiedenen Views eingesetzt und angezeigt.

      Würde man sich nicht die primaryStage im Model merken, würde die Anwendung bei jedem View/Maskenwechsel flakern. Da man bei jedem Wechsel ein neues Fenster erzeugen müsste. Jedoch möchte man dieses Verhalten in der Regel nicht:-)

    • aUser sagt:

      Leider muss ich xehpuk in diesem Punkt recht geben. Hier wird klar im Model auf die View referenziert, was dem Mvc-Pattern und deiner eigenen Aussage über das MVC-Pattern widerspricht. Meiner Meinung nach also leider, wenn auch gut gemeint, kein gutes Tutorial für MVC mit JavaFx.

      • Ok, versuchen wir die Schwächen meines Beitrags zu beheben. Hier die Fakten:
        – Es gibt eine Stage, die das Hauptfenster der Anwendung repräsentiert
        – Man muss sich die Stage irgendwie merken
        – Es gibt ein Model, was keine Elemente der View beinhalten darf

        Meine Lösungsvorschläge:
        – Man verwendet ein zweites viewspezifisches Model und speichert dort die Stage
        – Man verwendet das Singleton-Pattern für die Stage

        Habt ihr andere Vorschläge?

        • Stefan Huber sagt:

          Hy,
          Ich würde das Hauptfenster als Singleton defineren, dann kann man bequem drauf zugreifen und es gibt ja wirklich nur ein Hauptfenster .. also eine Art MainView.getMainStage() ?

          Lg Stefan

          • Hajo Lemcke sagt:

            Hallo,

            ich stimme Stefan hier grundsätzlich zu. Allerdings sollte die Hauptklasse (mit main()) kein Singleton sein, da sie einen public constructor ohne Parameter benötigt, sofern sie eine FXML lädt. Sie kann aber eine static Map enthalten, die die Referenzen auf die anderen Stages enthält. Dann flackert auch nix 🙂

            Hajo

  2. Walter sagt:

    “ // Sessionscope /Applikationscope Beans inizitalisieren!
    // muessen von Controllern weitergegeben werden“

    (abgesehen von Orthographie) verstehe ich das nicht. WAS muss an WEN weitergegeben werden?

    • Fehler korrigiert und nun zu deiner Frage:
      Jeder Controller hat eine Referenz auf das Model und seine View. Das Model enthält die Daten (können von anderen Masken, gelöscht, erstellt oder geändert werden) und die View, die Elemente und das Layout der wirklichen Maske. Willst du nun zu einer anderen Maske wechseln, initialisierst du den Controller mit dem Model und rufst die show-Methode.

      • Walter sagt:

        Danke für die Antwort und das Tutorial allgemein.
        Ich verstehe jetzt was du meinst. Dein Kommentar „von Controller zu Controller“ ist aber ein bisschen missverständlich, man könnte meinen das etwas von einem controller an einen anderen controller weitergegeben wird. Was du aber meinst ist, dass das Datenmodel jeweils an den Controller übergeben wird. (vom Main Program).
        Im übrigen finde ich deinen Ansatz interessant, aber ob es die ultimative Lösung ist???
        Auf der Oracle Website findet man „JavaFX Mastering FXML Release 2.2 E20478-08“ mit einem interessanten Beispiel (meiner Meinung nach auch MVC) wo die View jeweils auf ihren controller verweist.

  3. Felix Roske sagt:

    Hallo Alexander,

    vielen Dank für die Arbeit, die Du Dir gemacht hast!
    Trotzdem zwei Anmerkungen, die vielleicht weniger erfahrenen Programmierern beim Lesen anderer Einführungen weiterhelfen, da Dein MVC-Entwurf doch etwas vom „Standard“ abweicht:

    1. Controller und Model sollten nur über Interfaces angesprochen werden (für OO-Neulinge meistens sehr schwer zu verstehen und anfangs sehr verwirrend, aber das ist eines der wesentlichen Designprinzipien der OOP).

    2. Das Model enthält keineswegs nur Getter- und Setter. Im Regelfall beinhaltet das Model auch die gesamte Geschäftslogik: OO-Programmierung soll ja gerade Daten und die sie verändernden Algorithmen in einer Einheit (sprich: Klasse) vereinen! Der Controller befiehlt dem Model, das es sich zu ändern hat. Wie es das tut, hat den Controller nicht zu interessieren.

    In diesem Sinne, Happy Coding,
    Felix

  4. Mustafa Dündar Celebi sagt:

    Sehr gute Erklärung, Sehr gute Beiträge

    ich hab einen Schreibfehler festgestellt bei der DataBeans.java
    Zeile 10.
    sollte richtig heißen:

    public Map getNamePwMap(){

    Was mich sehr interessieren würde, ob Sie JavaFX mit Scenbuilder, mit Controller fxml und css wie dieses Beispiel vorhaben zu machen, Weil so etwas habe ich nirgendwo gefunden, das wäre für viele Einsteiger sehr interessante Tutorial.

    wieter so. Gruss

    Mit freundlichen Grüssen

  5. Matthias sagt:

    Was mir etwas fehlte ist die Vererbung. Aber diese kann sich jeder selbst dann in den einzelnen EingabeVCs bzw. AusgabeVCs einbauen. Mir gefiel das Beispiel sehr gut.

    Eine Frage, arbeitest Du nicht mit XMLs? Diese ließen sich über XML Builder noch leichter bearbeiten.

    • Hey Matthias,
      danke für das Lob^^

      @XML
      Beide Varianten haben Vor- und Nachteile. Ich bin mehr der Code-Fan, aber durch Android und co komme ich immer mehr auf den XML-Zeig;-)

      Viele Grüße
      Alexander

      • Matthias sagt:

        Finde auch das XML Vorteile hat. Ansonsten liest man ja sehr viel über JavaFX, dass es von Oracle nicht mehr so lange unterstützt wird. Wir werden sehen. Mag JavaFX.

Kommentar verfassen