2

I'm trying to set up a simple example for my program using JavaFX. What I want to have is a Controller with a main.fxml; then the main.fxml will have a TabbedPane as root, with 2 tabs (tab1.fxml and tab2.fxml), each with its controller (Tab1Controller and Tab2Controller).

Now this might be the prolem, yet I don't see why it would be a problem:

Tab1Controller and Tab2Controller both extend Controller; because they share various fields which are manipulated, for example a bottom status bar which needs constant updates.

So far I managed to have working all controllers and .fxml.

When I try to set up the text of the label in the Controller from one of the subclasses it just points to null, yet the label is initialised in the gui with its default text.. Compiles correctly and lblController seems to be correctly linked to its fx:id in main.fxml.

Any help/links are much appreciated.

I've been looking at various posts but the only one getting close to what I need it this one: JavaFX 8, controllers inheritance

Main:

package sample;

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 {

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

    @Override
    public void start(Stage primaryStage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("../view/main.fxml"));
        primaryStage.setTitle("Hello World");
        primaryStage.setScene(new Scene(root, 600, 700));
        primaryStage.show();
    }
}

Main controller:

package sample;

import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class Controller {
    @FXML
    public Label lblController;

    public Controller() {
        System.out.println("CONTROLLER MAIN - CONSTRUCTOR");
    }

    @FXML
    public void initialize() {
        System.out.println("CONTROLLER MAIN - INITIALIZER");
    }

    protected void setSts(String sts) {
        lblController.setText(sts);
    }
}

main.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.*?>
<AnchorPane xmlns:fx="http://javafx.com/fxml/1"
            xmlns="http://javafx.com/javafx/8.0.60"
            fx:controller="sample.Controller">
    <BorderPane>
        <center>
            <TabPane tabClosingPolicy="UNAVAILABLE">
                <Tab text="Tab 1">
                    <AnchorPane>
                        <!--FOR fx:id HAS TO HAVE THE SAME NAME BUT IN LOWER CASE AS THE .fxml-->
                        <fx:include fx:id="tab1" source="tab/tab1.fxml"/>
                    </AnchorPane>
                </Tab>
                <Tab text="Tab 2">
                    <AnchorPane>
                        <fx:include fx:id="tab2" source="tab/tab2.fxml"/>
                    </AnchorPane>
                </Tab>
            </TabPane>
        </center>
        <bottom>
            <AnchorPane>
                <Label fx:id="lblController" text="Controller Test"/>
            </AnchorPane>
        </bottom>
    </BorderPane>
</AnchorPane>

Tab1Controller

package sample.ctrTab;

import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import sample.Controller;


public class Tab1Controller extends Controller {
    @FXML
    Label lbl1;
    @FXML
    TextField txt1;
    @FXML
    Button btn1Save, btn1Load;

    public Tab1Controller() {
        super();
        System.out.println("CONTROLLER TAB 1 - CONSTRUCTOR");
    }

    @FXML
    void btn1Save() {
        System.out.println("CONTROLLER TAB 1 - SAVE CLICK");
//        lblController.setText("ANYTHING"); //NULL POINTER HERE
//        setSts(txt1.getText()); //OR HERE
    }

    @FXML
    void btn1Load() {
        System.out.println("CONTROLLER TAB 1 - LOAD CLICK");
    }

    protected void setSts(String sts) {
        super.setSts(sts);
    }
}

tab1.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns:fx="http://javafx.com/fxml/1"
            xmlns="http://javafx.com/javafx/8.0.60" fx:controller="sample.ctrTab.Tab1Controller">
    <children>
        <Label fx:id="lbl1" layoutX="100" layoutY="50" text="Def tab 1"/>
        <TextField fx:id="txt1" layoutX="100" layoutY="70"/>
        <Button fx:id="btn1Save" onAction="#btn1Save" layoutX="100" layoutY="140" mnemonicParsing="false"
                text="Save text"/>
        <Button fx:id="btn1Load" onAction="#btn1Load" layoutX="200.0" layoutY="140" mnemonicParsing="false"
                text="Send to tab 2"/>
    </children>
</AnchorPane>
Community
  • 1
  • 1
4673_j
  • 477
  • 1
  • 6
  • 20

1 Answers1

2

The fields may be present in the controller classes, however FXMLLoader still creates new controller instances for every fxml loaded given these fxmls. Since tab1.fxml does not contain a Label tag with fx:id="lblController" as attribute, the lblController field is never injected to the Tab1Controller instance.

You would be better off not extending Controller, but passing a reference of it to the child controllers:

public class Controller {
    @FXML
    public Label lblController;
    @FXML
    private Tab1Controller tab1Controller;
    @FXML
    private Tab2Controller tab2Controller;

    public Controller() {
        System.out.println("CONTROLLER MAIN - CONSTRUCTOR");
    }

    @FXML
    public void initialize() {
        System.out.println("CONTROLLER MAIN - INITIALIZER");
        tab1Controller.initParentController(this);
        tab2Controller.initParentController(this);
    }

    public void setSts(String sts) {
        lblController.setText(sts);
    }
}
public class Tab1Controller {
    @FXML
    Label lbl1;
    @FXML
    TextField txt1;
    @FXML
    Button btn1Save, btn1Load;

    public Tab1Controller() {
        System.out.println("CONTROLLER TAB 1 - CONSTRUCTOR");
    }

    private Controller parentController;

    public void initParentController(Controller controller) {
         parentController = controller;
    }

    @FXML
    void btn1Save() {
        System.out.println("CONTROLLER TAB 1 - SAVE CLICK");
        parentController.lblController.setText("ANYTHING");
        parentController.setSts(txt1.getText());
    }

    @FXML
    void btn1Load() {
        System.out.println("CONTROLLER TAB 1 - LOAD CLICK");
    }

}

Note that for the child controllers you could actually use a abstract superclass implement the initParentController method only once.

fabian
  • 80,457
  • 12
  • 86
  • 114
  • I have seen this approach and I was trying to avoid the inititialize() for each sub-controller individually; which could set the main Controller. I'm still puzzled how inheritance is not working here, even if accessed with separated getters/setters. – 4673_j Dec 12 '16 at 17:59
  • 1
    @4673_j You simply get 3 different controller instances when loading `main.fxml`: one per fxml file used. I often see inexperienced users of fxml file expect fields to "magically" be injected everywhere; This isn't the case. Objects are only injected to the controller instance used with the fxml file where they are declared. – fabian Dec 12 '16 at 18:05
  • I understand what you mean, I do not expect magic, I expect to understand how it works and how to use it properly, and I don't understand how I cannot access the element if it has been initialised as per injection request? therefore apply simple inheritance; for a simple parenthesis, in my program (not this sample) I have already 9 controllers and still growing; – 4673_j Dec 12 '16 at 18:09
  • 1
    @4673_j you seem to be confusing "object/instance" and "class". There are three different controller instances created by your code. One is an instance of `Controller`, one an instance of `Tab1Controller`, and one an instance of `Tab2Controller`. With the inheritance you define, the latter two are of course also instances of `Controller`, so all three have a field called `lblController`. However that field is only initialised if the FXML file has an element with a `fx:id="lblController"`, which is *only* true in the first instance, not the other two. – James_D Dec 12 '16 at 20:00
  • @James_D this made things way more clearer, I seem to have forgotten the basics; going back to the book. Thanks for your effort. – 4673_j Dec 13 '16 at 08:58