To start off with a client app for building anything serious with JavaFX, I had to go through a bunch of scattered tutorials and sometimes the aim of the tutorials were too microscopic in nature when you are looking for a seed project which exhibits most of the common functionality to get started off with. Some of the JavaFX samples partially covers what I am going to show in this tutorial. FXML views and controllers with scene switching is covered in FXML LoginDemo, Worker/Tasks with Property Binding is covered in the Henley Car Sales sample, Model-View separation is covered in the resources for the book Pro JavaFX 2. I would also like to point out the documentation for JavaFX Architecture and Best Practices for JavaFX 2.
Main
See the replaceSceneContent function to find out how FXML views are dynamically loaded and the controller attached to the view is returned.
View
Use the fully qualified class name for the controller. Runtime errors can be caused due to several reasons :- if the FXML file cannot be parsed properly, the controller can’t be found or the function provided for some action is not found in the controller.
Controller
Avoid any heavy processing on the JavaFX application thread. Delegate work to Worker threads using Tasks or Services. To interact in any way with the worker threads, use bindings or listeners on the properties of the worker threads. Notice the value property return the type of object associated with the worker.
Model
Main
import java.io.InputStream;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.fxml.JavaFXBuilderFactory;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
public class BotApp extends Application {
private Stage stage;
public static void main(String[] args) {
Application.launch(BotMain.class, (java.lang.String[])null);
}
@Override
public void start(Stage stage) throws Exception {
this.stage = stage;
stage.setTitle("Sample App");
gotoLogin();
stage.show();
}
private void gotoLogin() {
try {
LoginController login = (LoginController) replaceSceneContent("Login.fxml");
login.setApp(this);
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
}
public void gotoSecond() {
try {
//SecondController second = (SecondController) replaceSceneContent("Second.fxml");
//second.setApp(this);
} catch (Exception ex) {
System.out.println(ex.getMessage());
}
}
private Initializable replaceSceneContent(String fxml) throws Exception {
FXMLLoader loader = new FXMLLoader();
InputStream in = BotMain.class.getResourceAsStream(fxml);
loader.setBuilderFactory(new JavaFXBuilderFactory());
loader.setLocation(BotMain.class.getResource(fxml));
AnchorPane page;
try {
page = (AnchorPane) loader.load(in);
} finally {
in.close();
}
Scene scene = new Scene(page, 600, 350);
stage.setScene(scene);
stage.sizeToScene();
return (Initializable) loader.getController();
}
public void setClient(Client client) {
this.client = client;
}
}
See the replaceSceneContent function to find out how FXML views are dynamically loaded and the controller attached to the view is returned.
View
<?xml version="1.0" encoding="UTF-8"?>
<?import java.lang.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.paint.*?>
<AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="350.0" prefWidth="500.0" styleClass="background" xmlns:fx="http://javafx.com/fxml" fx:controller="LoginController">
<children>
<HBox id="hBox1" AnchorPane.leftAnchor="20.0" AnchorPane.rightAnchor="20.0" spacing="10" style="-fx-padding: 10">
<children>
<VBox id="vBox1" alignment="BASELINE_LEFT" spacing="10" style="-fx-padding: 10" prefWidth="350.0" >
<children>
<Label id="label1" text="Provide Login Details" />
<Label id="label1" text="Username" />
<TextField id="textField1" fx:id="userId" prefWidth="200.0" />
<Label id="label2" text="Password" />
<PasswordField id="passwordField1" fx:id="password" prefWidth="200.0" />
<Button id="button1" fx:id="login" defaultButton="true" onAction="#processLogin" prefHeight="50.0" prefWidth="200.0" text="Login" />
</children>
</VBox>
<VBox id="vBox2" spacing="10" style="-fx-padding: 10">
<children>
<Label id="label3" text="Console" />
<TextArea id="console" fx:id="console" />
</children>
</VBox>
</children>
</HBox>
</children>
</AnchorPane>
Use the fully qualified class name for the controller. Runtime errors can be caused due to several reasons :- if the FXML file cannot be parsed properly, the controller can’t be found or the function provided for some action is not found in the controller.
Controller
import java.net.URL;
import java.util.ResourceBundle;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.concurrent.Worker.State;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
public class LoginController extends AnchorPane implements Initializable {
@FXML
TextField userId;
@FXML
PasswordField password;
@FXML
Button login;
@FXML
TextArea console;
private LoginModel model;
private BotApp application;
private ReadOnlyObjectProperty<Worker.State> stateProperty;
public void setApp(BotApp application){
this.application = application;
}
@Override
public void initialize(URL location, ResourceBundle resources) {
model = new LoginModel();
console.textProperty().bind(model.worker.messageProperty());
stateProperty = model.worker.stateProperty();
login.disableProperty().bind(stateProperty.isNotEqualTo(Worker.State.READY));
}
public void processLogin(ActionEvent event) throws Exception {
model.setUsernameAndPassword(userId.getText(), password.getText());
new Thread((Runnable) model.worker).start();
stateProperty.addListener(new ChangeListener<State>() {
public void changed(ObservableValue ov, State oldState, State newState) {
if (newState == Worker.State.SUCCEEDED) {
application.setClient(model.worker.valueProperty().get());
application.gotoFollow();
}
}
});
}
}
Avoid any heavy processing on the JavaFX application thread. Delegate work to Worker threads using Tasks or Services. To interact in any way with the worker threads, use bindings or listeners on the properties of the worker threads. Notice the value property return the type of object associated with the worker.
Model
import javafx.concurrent.Worker;Task
public class LoginModel {
public Worker<Client> worker;
public LoginModel() {
worker = new Login();
}
public void setUsernameAndPassword(String username, String password) {
((Login)worker).setUsernameAndPassword(username, password);
}
}
import javafx.concurrent.Task;Use the protected update methods of Task class to set values for progress and messages. You can follow it up with binding properties like attaching one ReadOnlyStringProperty with another ReadOnlyStringProperty as shown in the controller to attach the console text area's text property with the message property of the worker.
public class Login extends Task<Client>{
String username = null;
String password = null;
String loginURL = “https://abc.xyz/login”;
String log = "";
private Client login() {
Client client = new Client(); //Wrapper for HttpURLConnection
String content = client.post(loginURL , payload);
//doSomething
//update Progress property
log += "Payload = " + payload +"\n";
updateMessage(log);
//doSomething
//update properties as required
return client;
}
public void setUsernameAndPassword(String username, String password) {
this.username = username;
this.password = password;
}
@Override
protected Client call() throws Exception {
return login();
}
}