Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it a best practice to separate logic from presentation more? [duplicate]

How can I pass parameters to a secondary window in javafx? Is there a way to communicate with the corresponding controller?

For example: The user chooses a customer from a TableView and a new window is opened, showing the customer's info.

Stage newStage = new Stage();
try 
{
    AnchorPane page = (AnchorPane) FXMLLoader.load(HectorGestion.class.getResource(fxmlResource));
    Scene scene = new Scene(page);
    newStage.setScene(scene);
    newStage.setTitle(windowTitle);
    newStage.setResizable(isResizable);
    if(showRightAway) 
    {
        newStage.show();
    }
}

newStage would be the new window. The problem is, I can't find a way to tell the controller where to look for the customer's info (by passing the id as parameter).

Any ideas?

like image 710
Alvaro Avatar asked Nov 24 '22 22:11

Alvaro


2 Answers

Using MVC

Most of this answer focuses on a direct call to pass a parameter from a calling class to the controller.

If instead, you want to decouple the caller and controller and use a more general architecture involving a model class with settable and listenable properties to achieve inter-controller communication, see the following basic overview:

  • Applying MVC With JavaFx

Recommended Approach

This answer enumerates different mechanisms for passing parameters to FXML controllers.

For small applications I highly recommend passing parameters directly from the caller to the controller - it's simple, straightforward and requires no extra frameworks.

For larger, more complicated applications, it would be worthwhile investigating if you want to use Dependency Injection or Event Bus mechanisms within your application.

Passing Parameters Directly From the Caller to the Controller

Pass custom data to an FXML controller by retrieving the controller from the FXML loader instance and calling a method on the controller to initialize it with the required data values.

Something like the following code:

public Stage showCustomerDialog(Customer customer) {
  FXMLLoader loader = new FXMLLoader(
    getClass().getResource(
      "customerDialog.fxml"
    )
  );

  Stage stage = new Stage(StageStyle.DECORATED);
  stage.setScene(
    new Scene(loader.load())
  );

  CustomerDialogController controller = loader.getController();
  controller.initData(customer);

  stage.show();

  return stage;
}

...

class CustomerDialogController {
  @FXML private Label customerName;
  void initialize() {}
  void initData(Customer customer) {
    customerName.setText(customer.getName());
  }
}

A new FXMLLoader is constructed as shown in the sample code i.e. new FXMLLoader(location). The location is a URL and you can generate such a URL from an FXML resource by:

new FXMLLoader(getClass().getResource("sample.fxml"));

Be careful NOT to use a static load function on the FXMLLoader, or you will not be able to get your controller from your loader instance.

FXMLLoader instances themselves never know anything about domain objects. You do not directly pass application specific domain objects into the FXMLLoader constructor, instead you:

  1. Construct an FXMLLoader based upon fxml markup at a specified location
  2. Get a controller from the FXMLLoader instance.
  3. Invoke methods on the retrieved controller to provide the controller with references to the domain objects.

This blog (by another writer) provides an alternate, but similar, example.

Setting a Controller on the FXMLLoader

CustomerDialogController dialogController = 
    new CustomerDialogController(param1, param2);

FXMLLoader loader = new FXMLLoader(
    getClass().getResource(
        "customerDialog.fxml"
    )
);
loader.setController(dialogController);

Pane mainPane = loader.load();

You can construct a new controller in code, passing any parameters you want from your caller into the controller constructor. Once you have constructed a controller, you can set it on an FXMLLoader instance before you invoke the load() instance method.

To set a controller on a loader (in JavaFX 2.x) you CANNOT also define a fx:controller attribute in your fxml file.

Due to the limitation on the fx:controller definition in FXML, I personally prefer getting the controller from the FXMLLoader rather than setting the controller into the FXMLLoader.

Having the Controller Retrieve Parameters from an External Static Method

This method is exemplified by Sergey's answer to Javafx 2.0 How-to Application.getParameters() in a Controller.java file.

Use Dependency Injection

FXMLLoader supports dependency injection systems like Guice, Spring or Java EE CDI by allowing you to set a custom controller factory on the FXMLLoader. This provides a callback that you can use to create the controller instance with dependent values injected by the respective dependency injection system.

An example of JavaFX application and controller dependency injection with Spring is provided in the answer to:

  • Adding Spring Dependency Injection in JavaFX (JPA Repo, Service)

A really nice, clean dependency injection approach is exemplified by the afterburner.fx framework with a sample air-hacks application that uses it. afterburner.fx relies on JEE6 javax.inject to perform the dependency injection.

Use an Event Bus

Greg Brown, the original FXML specification creator and implementor, often suggests considering use of an event bus, such as the Guava EventBus, for communication between FXML instantiated controllers and other application logic.

The EventBus is a simple but powerful publish/subscribe API with annotations that allows POJOs to communicate with each other anywhere in a JVM without having to refer to each other.

Follow-up Q&A

on first method, why do you return Stage? The method can be void as well because you already giving the command show(); just before return stage;. How do you plan usage by returning the Stage

It is a functional solution to a problem. A stage is returned from the showCustomerDialog function so that a reference to it can be stored by an external class which may wish to do something, such as hide the stage based on a button click in the main window, at a later time. An alternate, object-oriented solution could encapsulate the functionality and stage reference inside a CustomerDialog object or have a CustomerDialog extend Stage. A full example for an object-oriented interface to a custom dialog encapsulating FXML, controller and model data is beyond the scope of this answer, but may make a worthwhile blog post for anybody inclined to create one.


Additional information supplied by StackOverflow user named @dzim

Example for Spring Boot Dependency Injection

The question of how to do it "The Spring Boot Way", there was a discussion about JavaFX 2, which I anserwered in the attached permalink. The approach is still valid and tested in March 2016, on Spring Boot v1.3.3.RELEASE: https://stackoverflow.com/a/36310391/1281217


Sometimes, you might want to pass results back to the caller, in which case you can check out the answer to the related question:

  • JavaFX FXML Parameter passing from Controller A to B and back
like image 200
jewelsea Avatar answered Nov 26 '22 23:11

jewelsea


I realize this is a very old post and has some great answers already, but I wanted to make a simple MCVE to demonstrate one such approach and allow new coders a way to quickly see the concept in action.

In this example, we will use 5 files:

  1. Main.java - Simply used to start the application and call the first controller.
  2. Controller1.java - The controller for the first FXML layout.
  3. Controller2.java - The controller for the second FXML layout.
  4. Layout1.fxml - The FXML layout for the first scene.
  5. Layout2.fxml - The FXML layout for the second scene.

All files are listed in their entirety at the bottom of this post.

The Goal: To demonstrate passing values from Controller1 to Controller2 and vice versa.

The Program Flow:

  • The first scene contains a TextField, a Button, and a Label. When the Button is clicked, the second window is loaded and displayed, including the text entered in the TextField.
  • Within the second scene, there is also a TextField, a Button, and a Label. The Label will display the text entered in the TextField on the first scene.
  • Upon entering text in the second scene's TextField and clicking its Button, the first scene's Label is updated to show the entered text.

This is a very simple demonstration and could surely stand for some improvement, but should make the concept very clear.

The code itself is also commented with some details of what is happening and how.

THE CODE

Main.java:

import javafx.application.Application;
import javafx.stage.Stage;

public class Main extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {

        // Create the first controller, which loads Layout1.fxml within its own constructor
        Controller1 controller1 = new Controller1();

        // Show the new stage
        controller1.showStage();

    }
}

Controller1.java:

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.stage.Stage;

import java.io.IOException;

public class Controller1 {

    // Holds this controller's Stage
    private final Stage thisStage;

    // Define the nodes from the Layout1.fxml file. This allows them to be referenced within the controller
    @FXML
    private TextField txtToSecondController;
    @FXML
    private Button btnOpenLayout2;
    @FXML
    private Label lblFromController2;

    public Controller1() {

        // Create the new stage
        thisStage = new Stage();

        // Load the FXML file
        try {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("Layout1.fxml"));

            // Set this class as the controller
            loader.setController(this);

            // Load the scene
            thisStage.setScene(new Scene(loader.load()));

            // Setup the window/stage
            thisStage.setTitle("Passing Controllers Example - Layout1");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Show the stage that was loaded in the constructor
     */
    public void showStage() {
        thisStage.showAndWait();
    }

    /**
     * The initialize() method allows you set setup your scene, adding actions, configuring nodes, etc.
     */
    @FXML
    private void initialize() {

        // Add an action for the "Open Layout2" button
        btnOpenLayout2.setOnAction(event -> openLayout2());
    }

    /**
     * Performs the action of loading and showing Layout2
     */
    private void openLayout2() {

        // Create the second controller, which loads its own FXML file. We pass a reference to this controller
        // using the keyword [this]; that allows the second controller to access the methods contained in here.
        Controller2 controller2 = new Controller2(this);

        // Show the new stage/window
        controller2.showStage();

    }

    /**
     * Returns the text entered into txtToSecondController. This allows other controllers/classes to view that data.
     */
    public String getEnteredText() {
        return txtToSecondController.getText();
    }

    /**
     * Allows other controllers to set the text of this layout's Label
     */
    public void setTextFromController2(String text) {
        lblFromController2.setText(text);
    }
}

Controller2.java:

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.stage.Stage;

import java.io.IOException;

public class Controller2 {

    // Holds this controller's Stage
    private Stage thisStage;

    // Will hold a reference to the first controller, allowing us to access the methods found there.
    private final Controller1 controller1;

    // Add references to the controls in Layout2.fxml
    @FXML
    private Label lblFromController1;
    @FXML
    private TextField txtToFirstController;
    @FXML
    private Button btnSetLayout1Text;

    public Controller2(Controller1 controller1) {
        // We received the first controller, now let's make it usable throughout this controller.
        this.controller1 = controller1;

        // Create the new stage
        thisStage = new Stage();

        // Load the FXML file
        try {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("Layout2.fxml"));

            // Set this class as the controller
            loader.setController(this);

            // Load the scene
            thisStage.setScene(new Scene(loader.load()));

            // Setup the window/stage
            thisStage.setTitle("Passing Controllers Example - Layout2");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Show the stage that was loaded in the constructor
     */
    public void showStage() {
        thisStage.showAndWait();
    }

    @FXML
    private void initialize() {

        // Set the label to whatever the text entered on Layout1 is
        lblFromController1.setText(controller1.getEnteredText());

        // Set the action for the button
        btnSetLayout1Text.setOnAction(event -> setTextOnLayout1());
    }

    /**
     * Calls the "setTextFromController2()" method on the first controller to update its Label
     */
    private void setTextOnLayout1() {
        controller1.setTextFromController2(txtToFirstController.getText());
    }

}

Layout1.fxml:

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<AnchorPane xmlns="http://javafx.com/javafx/9.0.1" xmlns:fx="http://javafx.com/fxml/1">
    <VBox alignment="CENTER" spacing="10.0">
        <padding>
            <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
        </padding>
        <Label style="-fx-font-weight: bold;" text="This is Layout1!"/>
        <HBox alignment="CENTER_LEFT" spacing="10.0">
            <Label text="Enter Text:"/>
            <TextField fx:id="txtToSecondController"/>
            <Button fx:id="btnOpenLayout2" mnemonicParsing="false" text="Open Layout2"/>
        </HBox>
        <VBox alignment="CENTER">
            <Label text="Text From Controller2:"/>
            <Label fx:id="lblFromController2" text="Nothing Yet!"/>
        </VBox>
    </VBox>
</AnchorPane>

Layout2.fxml:

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<AnchorPane xmlns="http://javafx.com/javafx/9.0.1" xmlns:fx="http://javafx.com/fxml/1">
    <VBox alignment="CENTER" spacing="10.0">
        <padding>
            <Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
        </padding>
        <Label style="-fx-font-weight: bold;" text="Welcome to Layout 2!"/>
        <VBox alignment="CENTER">
            <Label text="Text From Controller1:"/>
            <Label fx:id="lblFromController1" text="Nothing Yet!"/>
        </VBox>
        <HBox alignment="CENTER_LEFT" spacing="10.0">
            <Label text="Enter Text:"/>
            <TextField fx:id="txtToFirstController"/>
            <Button fx:id="btnSetLayout1Text" mnemonicParsing="false" text="Set Text on Layout1"/>
        </HBox>
    </VBox>
</AnchorPane>
like image 28
Zephyr Avatar answered Nov 26 '22 22:11

Zephyr