Applying MVC With JavaFx
There are many different variations of this pattern. In particular, "MVC" in the context of a web application is interpreted somewhat differently to "MVC" in the context of a thick client (e.g. desktop) application (because a web application has to sit atop the request-response cycle). This is just one approach to implementing MVC in the context of a thick client application, using JavaFX.
Your Person
class is not really the model, unless you have a very simple application: this is typically what we call a domain object, and the model will contain references to it, along with other data. In a narrow context, such as when you are just thinking about the ListView
, you can think of the Person
as your data model (it models the data in each element of the ListView
), but in the wider context of the application, there is more data and state to consider.
If you are displaying a ListView<Person>
the data you need, as a minimum, is an ObservableList<Person>
. You might also want a property such as currentPerson
, that might represent the selected item in the list.
If the only view you have is the ListView
, then creating a separate class to store this would be overkill, but any real application will usually end up with multiple views. At this point, having the data shared in a model becomes a very useful way for different controllers to communicate with each other.
So, for example, you might have something like this:
public class DataModel {
private final ObservableList<Person> personList = FXCollections.observableArrayList();
private final ObjectProperty<Person> currentPerson = new SimpleObjectPropery<>(null);
public ObjectProperty<Person> currentPersonProperty() {
return currentPerson ;
}
public final Person getCurrentPerson() {
return currentPerson().get();
}
public final void setCurrentPerson(Person person) {
currentPerson().set(person);
}
public ObservableList<Person> getPersonList() {
return personList ;
}
}
Now you might have a controller for the ListView
display that looks like this:
public class ListController {
@FXML
private ListView<Person> listView ;
private DataModel model ;
public void initModel(DataModel model) {
// ensure model is only set once:
if (this.model != null) {
throw new IllegalStateException("Model can only be initialized once");
}
this.model = model ;
listView.setItems(model.getPersonList());
listView.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) ->
model.setCurrentPerson(newSelection));
model.currentPersonProperty().addListener((obs, oldPerson, newPerson) -> {
if (newPerson == null) {
listView.getSelectionModel().clearSelection();
} else {
listView.getSelectionModel().select(newPerson);
}
});
}
}
This controller essentially just binds the data displayed in the list to the data in the model, and ensures the model's currentPerson
is always the selected item in the list view.
Now you might have another view, say an editor, with three text fields for the firstName
, lastName
, and email
properties of a person. It's controller might look like:
public class EditorController {
@FXML
private TextField firstNameField ;
@FXML
private TextField lastNameField ;
@FXML
private TextField emailField ;
private DataModel model ;
public void initModel(DataModel model) {
if (this.model != null) {
throw new IllegalStateException("Model can only be initialized once");
}
this.model = model ;
model.currentPersonProperty().addListener((obs, oldPerson, newPerson) -> {
if (oldPerson != null) {
firstNameField.textProperty().unbindBidirectional(oldPerson.firstNameProperty());
lastNameField.textProperty().unbindBidirectional(oldPerson.lastNameProperty());
emailField.textProperty().unbindBidirectional(oldPerson.emailProperty());
}
if (newPerson == null) {
firstNameField.setText("");
lastNameField.setText("");
emailField.setText("");
} else {
firstNameField.textProperty().bindBidirectional(newPerson.firstNameProperty());
lastNameField.textProperty().bindBidirectional(newPerson.lastNameProperty());
emailField.textProperty().bindBidirectional(newPerson.emailProperty());
}
});
}
}
Now if you set things up so both these controllers are sharing the same model, the editor will edit the currently selected item in the list.
Loading and saving data should be done via the model. Sometimes you will even factor this out into a separate class to which the model has a reference (allowing you to easily switch between a file-based data loader and a database data loader, or an implementation that accesses a web service, for example). In the simple case you might do
public class DataModel {
// other code as before...
public void loadData(File file) throws IOException {
// load data from file and store in personList...
}
public void saveData(File file) throws IOException {
// save contents of personList to file ...
}
}
Then you might have a controller that provides access to this functionality:
public class MenuController {
private DataModel model ;
@FXML
private MenuBar menuBar ;
public void initModel(DataModel model) {
if (this.model != null) {
throw new IllegalStateException("Model can only be initialized once");
}
this.model = model ;
}
@FXML
public void load() {
FileChooser chooser = new FileChooser();
File file = chooser.showOpenDialog(menuBar.getScene().getWindow());
if (file != null) {
try {
model.loadData(file);
} catch (IOException exc) {
// handle exception...
}
}
}
@FXML
public void save() {
// similar to load...
}
}
Now you can easily assemble an application:
public class ContactApp extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
BorderPane root = new BorderPane();
FXMLLoader listLoader = new FXMLLoader(getClass().getResource("list.fxml"));
root.setCenter(listLoader.load());
ListController listController = listLoader.getController();
FXMLLoader editorLoader = new FXMLLoader(getClass().getResource("editor.fxml"));
root.setRight(editorLoader.load());
EditorController editorController = editorLoader.getController();
FXMLLoader menuLoader = new FXMLLoader(getClass().getResource("menu.fxml"));
root.setTop(menuLoader.load());
MenuController menuController = menuLoader.getController();
DataModel model = new DataModel();
listController.initModel(model);
editorController.initModel(model);
menuController.initModel(model);
Scene scene = new Scene(root, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
}
As I said, there are many variations of this pattern (and this is probably more a model-view-presenter, or "passive view" variation), but that's one approach (one I basically favor). It's a bit more natural to provide the model to the controllers via their constructor, but then it's a lot harder to define the controller class with a fx:controller
attribute. This pattern also lends itself strongly to dependency injection frameworks.
Update: full code for this example is here.
If you are interested in a tutorial on MVC in JavaFX, see:
- The Eden Coding tutorial: How to apply MVC in JavaFX
How to make a proper MVC Pattern with Javafx and Scene builder
All UI elements should be in the view.
A model should have only information and logic that the view and controller use.
public class ModelLogin {
private final String userName;
private final String password;
ModelLogin(String userName, String password) {
this.userName = userName;
this.password = password;
}
boolean isCorrectCredentials(String userName, String password){
return this.userName.equals(userName)&&this.password.equals(password);
}
}
The controller "wires" the view and the model: it handles credential verification and
change of scene.
Note that it is modified to accept a reference of Main
so it can change scene:
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
public class ControllerLogin {
@FXML TextField userNameField;
@FXML PasswordField passwordField;
private ModelLogin model;
private Main main;
@FXML
void initialize() {
model = new ModelLogin("test", "1234");
}
public void login(ActionEvent event) {
if (model.isCorrectCredentials(userNameField.getText(), passwordField.getText() )) {
try {
main.startApp();
} catch (Exception e) {
e.printStackTrace();
}
}else {
System.out.println("Try again");
}
}
void setMain(Main main) {
this.main = main;
}
}
The text field onAction
is not used, so it was removed from the fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.text.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="290.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/8"
xmlns:fx="http://javafx.com/fxml/1" fx:controller="login.ControllerLogin">
<children>
<AnchorPane prefHeight="290.0" prefWidth="400.0">
<children>
<Label alignment="CENTER" layoutX="150.0" layoutY="38.0" prefHeight="30.0" prefWidth="100.0" text="Login">
<font>
<Font name="System Bold" size="20.0" />
</font>
</Label>
<Label layoutX="159.0" layoutY="108.0" text="Benutzername">
<font>
<Font name="System Bold" size="12.0" />
</font>
</Label>
<TextField fx:id="userNameField" layoutX="126.0" layoutY="125.0"/>
<Label layoutX="175.0" layoutY="165.0" text="Passwort">
<font>
<Font name="System Bold" size="12.0" />
</font>
</Label>
<PasswordField fx:id="passwordField" layoutX="126.0" layoutY="182.0" />
<Button fx:id="loginButton" layoutX="175.0" layoutY="233.0" mnemonicParsing="false" onAction="#login" text="Login" />
</children>
</AnchorPane>
</children>
</VBox>
The Main
was modified to get a reference to the controller, and to change scene:
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application{
private Stage primaryStage;
private Parent root;
@Override
public void start(Stage primaryStage) throws Exception{
try {
this.primaryStage = primaryStage;
FXMLLoader loader = new FXMLLoader(getClass().getResource("/login/LoginUI.fxml"));
root = loader.load();
ControllerLogin controller = loader.getController();
controller.setMain(this);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
} catch (Exception e) {
e.printStackTrace();
}
}
public void startApp() throws Exception{
try {
root = FXMLLoader.load(getClass().getResource("/financeApp/UI.fxml"));
Scene scene = new Scene(root, 1022, 593);
primaryStage.setScene(scene);
primaryStage.show();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
MVC Design Pattern and Controllers in JavaFX
It's not possible to do what you are saying, you can't create something from some other thing that's not even existing. If you don't instantiate the PersonOverview
in Main, you can't make it do anything.
Also, notice in this case that the view of PersonOverview
is attached to the RootLayout
which is created in Main
. So you can consider these as one Main View where each controller is managing one part of the view.
In the case of PersonEditDialog
, You are starting a stage through the main view to edit some information. That's why it's created in Main. The stage is attached to the MainStage.
And if you have several stages to create, You don't have to do that in Main
. It depends on your needs. You can basically run a stage which uses some controller from another controller by clicking on a button for example. So it depends on : after what event you want to see that stage.
Example : You can add a button in PersonEditDialog controller like More...
and you define its setOnAction
event to open a new view (stage) where you show the picture, the Twitter link...
JavaFX - MVC Application best practices with database
Here is an example implementation
The DAO
class takes care of connecting to the database (may use a pool or something else). In this case, it makes a simple connection.
public class DAO {
public Connection getConnection() throws SQLException {
return DriverManager.getConnection("jdbc:mysql://192.168.40.5:3306/test", "root", "");
}
}
The ToDoListModel
class takes care of working with the database by using an instance of DAO
to get a valid connection.
public class ToDoListModel {
private DAO dao;
public static ToDoListModel getInstance() {
ToDoListModel model = new ToDoListModel();
model.dao = new DAO();
return model;
}
private ToDoListModel() {
}
public void addTask(Task task) throws SQLException {
try(Connection connection = dao.getConnection()) {
String q = "insert into todo (name) values (?)";
try(PreparedStatement statement = connection.prepareStatement(q, Statement.RETURN_GENERATED_KEYS)) {
statement.setString(1, task.getName());
statement.executeUpdate();
try(ResultSet rs = statement.getGeneratedKeys()) {
if(rs.next()) {
task.setId(rs.getInt(1));
}
}
}
}
}
public void deleteTask(Task task) throws SQLException {
try(Connection connection = dao.getConnection()) {
String q = "delete from todo where id = ?";
try(PreparedStatement statement = connection.prepareStatement(q)) {
statement.setInt(1, task.getId());
statement.executeUpdate();
}
}
}
public ObservableList<Task> getTaskList() throws SQLException {
try(Connection connection = dao.getConnection()) {
String q = "select * from todo";
try(Statement statement = connection.createStatement()) {
try(ResultSet rs = statement.executeQuery(q)) {
ObservableList<Task> tasks = FXCollections.observableArrayList();
while (rs.next()) {
Task task = new Task();
task.setId(rs.getInt("id"));
task.setName(rs.getString("name"));
tasks.add(task);
}
return tasks;
}
}
}
}
}
The controller uses ToDoListModel
to initialize TableView
controls and add operations (editing and reading - I did not implement them because I stick to your code)
public class Controller {
@FXML
private TextField textField;
@FXML
private TableView<Task> tableView;
@FXML
private TableColumn<Task, String> nameTableColumn;
@FXML
private Button addButton;
@FXML
private void initialize() {
nameTableColumn.setCellValueFactory(cdf -> cdf.getValue().nameProperty());
addButton.disableProperty().bind(Bindings.isEmpty(textField.textProperty()));
CompletableFuture.supplyAsync(this::loadAll)
.thenAccept(list -> Platform.runLater(() -> tableView.getItems().setAll(list)))
.exceptionally(this::errorHandle);
}
@FXML
private void handleAddButton(ActionEvent event) {
CompletableFuture.supplyAsync(this::addTask)
.thenAccept(task -> Platform.runLater(() -> {
tableView.getItems().add(task);
textField.clear();
textField.requestFocus();
}))
.exceptionally(this::errorHandle);
}
private Task addTask() {
try {
Task task = new Task(textField.getText());
ToDoListModel.getInstance().addTask(task);
return task;
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
private ObservableList<Task> loadAll() {
try {
return ToDoListModel.getInstance().getTaskList();
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
private Void errorHandle(Throwable throwable) {
throwable.printStackTrace();
return null;
}
}
Any database operations are asynchronous with CompletableFuture
but you can use whatever you prefer. The important thing is to remember that UI threads can only be made uniquely by it.
Related Topics
Calculating the Difference Between Two Java Date Instances
Difference Between Chromedriver and Webdriver in Selenium
"Implements Runnable" VS "Extends Thread" in Java
Assigning Variables With Dynamic Names in Java
How to Create a Java String from the Contents of a File
Error Java.Lang.Outofmemoryerror: Gc Overhead Limit Exceeded
How to Load Jar Files Dynamically At Runtime
Cannot Refer to a Non-Final Variable Inside an Inner Class Defined in a Different Method
Java Url Encoding of Query String Parameters
Can You Find All Classes in a Package Using Reflection
Stringbuilder VS String Concatenation in Tostring() in Java
How to Deal With "Java.Lang.Outofmemoryerror: Java Heap Space" Error
How to Fix a Nosuchmethoderror
Validating Input Using Java.Util.Scanner
When Do You Use Java'S @Override Annotation and Why
What Is the Point of the Diamond Operator (≪≫) in Java