Last active
June 27, 2023 19:40
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.jamesd.examples.library; | |
public enum BrowseState { BOOK, DVD } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
module org.jamesd.examples.library { | |
requires javafx.controls; | |
requires javafx.fxml; | |
opens org.jamesd.examples.library to javafx.fxml; | |
exports org.jamesd.examples.library; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.jamesd.examples.library; | |
public record User(String username) {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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