Skip to content

Instantly share code, notes, and snippets.

@james-d
Last active June 27, 2023 19:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save james-d/bae73788a9b881cf9358cbf2d842f554 to your computer and use it in GitHub Desktop.
Save james-d/bae73788a9b881cf9358cbf2d842f554 to your computer and use it in GitHub Desktop.
Demo of a MVC-like approach to managing view switching in JavaFX. Contains three views: a login screen, and two other pages. The model holds a user and a "browse state". A view manager determines the current view based on values in the model. The FXML controllers/presenters have a reference to a shared model whose properties they update.
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Button?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
spacing="20"
alignment="CENTER"
fx:controller="org.jamesd.examples.library.BookController">
<padding><Insets topRightBottomLeft="20"/></padding>
<Label text="Books" style="-fx-font: bold 24pt sans-serif;"/>
<Label fx:id="welcomeLabel"/>
<Label text="Please browse our books..."/>
<HBox spacing="10" alignment="CENTER">
<Button text="Switch to DVDs" onAction="#switchToDvds"/>
<Button text="Logout" onAction="#logout"/>
</HBox>
</VBox>
package org.jamesd.examples.library;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class BookController {
private final Model model;
@FXML
private Label welcomeLabel;
public BookController(Model model) {
this.model = model;
}
@FXML
protected void initialize() {
welcomeLabel.textProperty().bind(
model.userProperty()
.map( u -> String.format("Welcome %s", u.username()))
.orElse("Welcome")
);
}
@FXML
private void switchToDvds() {
model.setBrowseState(BrowseState.DVD);
}
@FXML
private void logout() {
model.logout();
}
}
package org.jamesd.examples.library;
public enum BrowseState { BOOK, DVD }
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
spacing="20"
alignment="CENTER"
fx:controller="org.jamesd.examples.library.DvdController">
<padding><Insets topRightBottomLeft="20"/></padding>
<Label text="DVDs" style="-fx-font: bold 24pt sans-serif;"/>
<Label fx:id="welcomeLabel"/>
<Label text="Please browse our DVDs..."/>
<HBox spacing="10" alignment="CENTER">
<Button text="Switch to Books" onAction="#switchToBooks"/>
<Button text="Logout" onAction="#logout"/>
</HBox>
</VBox>
package org.jamesd.examples.library;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class DvdController {
private final Model model ;
@FXML
private Label welcomeLabel;
public DvdController(Model model) {
this.model = model;
}
@FXML
protected void initialize() {
welcomeLabel.textProperty().bind(
model.userProperty()
.map( u -> String.format("Welcome %s", u.username()))
.orElse("Welcome")
);
}
@FXML
private void switchToBooks() {
model.setBrowseState(BrowseState.BOOK);
}
@FXML
private void logout() {
model.logout();
}
}
package org.jamesd.examples.library;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class HelloApplication extends Application {
@Override
public void start(Stage stage) {
Model model = new Model();
ViewManager viewManager = new ViewManager(model);
Scene scene = new Scene(viewManager.getCurrentView(), 800, 500);
scene.rootProperty().bind(viewManager.currentViewProperty());
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.Tooltip?>
<GridPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
hgap="5" vgap="5"
alignment="CENTER"
fx:controller="org.jamesd.examples.library.LoginController">
<Label fx:id="loginLabel" text="Please enter user name and password"
GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2"/>
<Label text="Username:" GridPane.rowIndex="1" GridPane.columnIndex="0"/>
<Label text="Password:" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
<TextField fx:id="usernameEntry" GridPane.rowIndex="1" GridPane.columnIndex="1" onAction="#login" />
<PasswordField fx:id="passwordEntry" GridPane.rowIndex="2" GridPane.columnIndex="1"
promptText="Password is 'secret'" onAction="#login" >
<tooltip>
<Tooltip text="Password is 'secret'"/>
</tooltip>
</PasswordField>
<HBox alignment="CENTER" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2">
<Button text="Login" onAction="#login" />
</HBox>
</GridPane>
package org.jamesd.examples.library;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
public class LoginController {
private final Model model;
@FXML
private Label loginLabel;
@FXML
private TextField usernameEntry;
@FXML
private PasswordField passwordEntry;
public LoginController(Model model) {
this.model = model;
}
public void login() {
String username = usernameEntry.getText();
String password = passwordEntry.getText();
if (username.isBlank() || password.isBlank()) {
loginLabel.setText("Please enter username and password");
} else {
if (validateLogin(username, password)) {
model.setUser(new User(username));
} else {
loginLabel.setText("Invalid username and/or password");
}
}
}
public boolean validateLogin(String username, String password) {
// dummy implementation
return "secret".equals(password);
}
}
package org.jamesd.examples.library;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
public class Model {
private final ObjectProperty<User> user = new SimpleObjectProperty<>(null);
public User getUser() {
return user.get();
}
public ObjectProperty<User> userProperty() {
return user;
}
public void setUser(User user) {
this.user.set(user);
}
private final ObjectProperty<BrowseState> browseState = new SimpleObjectProperty<>(BrowseState.BOOK);
public BrowseState getBrowseState() {
return browseState.get();
}
public ObjectProperty<BrowseState> browseStateProperty() {
return browseState;
}
public void setBrowseState(BrowseState browseState) {
this.browseState.set(browseState);
}
public void logout() {
setUser(null);
}
}
module org.jamesd.examples.library {
requires javafx.controls;
requires javafx.fxml;
opens org.jamesd.examples.library to javafx.fxml;
exports org.jamesd.examples.library;
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.jamesd.examples</groupId>
<artifactId>library</artifactId>
<version>1.0-SNAPSHOT</version>
<name>library</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>20</java.version>
<javafx.version>20</javafx.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<executions>
<execution>
<!-- Default configuration for running with: mvn clean javafx:run -->
<id>default-cli</id>
<configuration>
<mainClass>org.jamesd.examples.library/org.jamesd.examples.library.HelloApplication
</mainClass>
<launcher>app</launcher>
<jlinkZipName>app</jlinkZipName>
<jlinkImageName>app</jlinkImageName>
<noManPages>true</noManPages>
<stripDebug>true</stripDebug>
<noHeaderFiles>true</noHeaderFiles>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
package org.jamesd.examples.library;
public record User(String username) {}
package org.jamesd.examples.library;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ChangeListener;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.util.Callback;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.URL;
public class ViewManager {
private final Model model ;
private final Callback<Class<?>, Object> controllerFactory ;
private final ReadOnlyObjectWrapper<Parent> currentView = new ReadOnlyObjectWrapper<>();
public Parent getCurrentView() {
return currentView.get();
}
public ReadOnlyObjectProperty<Parent> currentViewProperty() {
return currentView.getReadOnlyProperty();
}
public ViewManager(Model model) {
this.model = model;
// The controller factory is a functional interface that maps Class<?> instances
// to objects. It is used by the FXMLLoader to create controllers for a given
// controller class (specified by fx:controller in the FXML file).
// Since our controllers don't have default constructors, we need a controller
// factory to determine how to instantiate the controllers.
// The implementation here looks for a constructor with a single parameter of type Model,
// and if it finds one invokes that constructor, providing the model instance.
controllerFactory = type -> {
try {
for (Constructor<?> c : type.getConstructors()) {
if (c.getParameterCount() == 1 && c.getParameterTypes()[0].equals(Model.class)) {
return c.newInstance(model);
}
}
return type.getConstructor().newInstance();
} catch (Exception e) {
throw e instanceof RuntimeException re ? re : new RuntimeException(e);
}
};
// update the current view if either the user or the browse state change:
ChangeListener<Object> listener = (obs, oldValue, newValue) -> updateView();
model.userProperty().addListener(listener);
model.browseStateProperty().addListener(listener);
// And initialize the current view based on the current state of the model:
updateView();
}
private void updateView() {
try {
// The FXML file to load:
URL resource;
// If no-one is logged in, provide the login page
if (model.getUser() == null) {
resource = LoginController.class.getResource("Login.fxml");
} else {
// Otherwise, provide the page determined by the "browse state":
resource = switch(model.getBrowseState()) {
case DVD -> DvdController.class.getResource("Dvd.fxml");
case BOOK -> BookController.class.getResource("Book.fxml");
};
}
FXMLLoader loader = new FXMLLoader(resource);
loader.setControllerFactory(controllerFactory);
currentView.set(loader.load());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment