Applying MVC With Javafx

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



Leave a reply



Submit