-2

I want to pass an attribute value from a controller to another using nested controllers technique. But in the current setting the nested controller instance yields a null value.

Here's the code:

`public class MainUI extends Application{
 @Override
    public void start(Stage stage) throws IOException {


        String fxmlFile = "main.fxml";
         FXMLLoader loader = new FXMLLoader();
        Parent rootNode = (Parent) loader.load(getClass().getResource(fxmlFile));
...
}`

main.fxml:

`<AnchorPane>
...
 <fx:include fx:id="someInnerComponent" source="someInnerComponent.fxml" visible="false" />
 </AnchorPane>`

someInnerComponent.fxml:

`<AnchorPane>
...
<!-- nothing so special about the imports here -->
</AnchorPane>`

mainController:

 ``public class MainController{

         ...

         public static SomeInnerComponent someInnerComponentController = new SomeInnerComponent();

     public static void  setMainController(SomeInnerComponent someInnerComponentControllerrparam) {
        someInnerComponentController = someInnerComponentControllerrparam;
    }
    
    
    ...
    
        public void testValueOfController(){
            someInnerComponentController.getAttVal(); // returns empty string in case I remove static for someInnerComponentController     
        }
  }`


and finally, SomeInnerComponent.java:

`public class SomeInnerComponent  {
        public  SomeType attVal ;
        @Override
    @FXML
    public void initialize(URL location, ResourceBundle resources) {
         attVal = new SomeType();
         //for holding a reference of this controller to the mainController class 
         MainController.setMainController(this);
         
         ...
         }
         
        public void testSetAttVal(){ 
                  setAttVal(attVal);// I want this value to be accessible from the mainController
                      ...
         }`


`

This works but the static reference to someInnerComponentController in the mainController is annoying, how to get rid of that using a nested controller trick?

ites
  • 11
  • 4
  • 3
    See https://stackoverflow.com/a/59552853/6395627 – Slaw May 30 '23 at 03:03
  • i dont see how that relates. it is not helping – ites May 30 '23 at 20:52
  • You want access to the nested controller inside the "main" controller, yes? That answer shows exactly how to do that. You can inject the controller just like any other FXML component, except the field name would be `Controller` (e.g., if you had `fx:id="foo"` then the field name would be `fooController`). – Slaw May 30 '23 at 21:06
  • if I inject the controller as you show, the class attribute value accessed from mainController will be null, I want it to be the changed value instead (changed in SomeInnerComponent.java) – ites May 30 '23 at 21:36

1 Answers1

4

FXML Controllers

State

An FXML controller should virtually never have static state (i.e., no static fields). Each time you load an FXML file, a new instance of its associated controller class is created. If you have static fields, then that state is shared across all instances of the controller class, and that doesn't make sense conceptually.

Communication between controllers

Communicating between controllers should rarely, if ever, be done directly. Instead, you should be using a proper application architecture like Model-View-Controller (MVC) or Model-View-ViewModel (MVVM). Then communication between the different controllers, and thus views, is done by interacting with the model. The model would be observable in some way so that state changes in the model are reflected in other views as appropriate. The only exception to this is in the context of nested FXML files. But even then, it would probably be best to only inject the nested controller to pass it an instance of a shared model, then leave further communication to be done via said model.

Note an FXML controller is not a "controller" in the MVC sense. Using MVC is a "higher level" concept. Besides, the FXML controller functions more like a "presenter" from the Model-View-Presenter (MVP) architecture.

See Q&As like Applying MVC With JavaFx for more information. Also look for tutorials, articles, books, etc. on the concepts in general.

Inject Nested Controller

If you want access to the nested FXML file's controller instance, then you should inject it into the "parent" controller similar to any other FXML component. That is to say, give the fx:include element an fx:id attribute, then define an appropriate field in the controller. For instance, if you had:

<!-- 
Where the root element of 'Foo.fxml' is a StackPane and the
controller is an instance of 'FooController'
-->
<fx:include fx:id="foo" source="Foo.fxml"/>

Then in the controller you'd have:

// to inject root element of nested FXML file (if needed)
@FXML private StackPane foo;
// to inject nested FXML file's controller instance (if needed)
@FXML private FooController fooController; // note field name is '<fx:id>Controller'

Note a nested controller will be fully initialized before the "parent" controller is initialized.

Controller Initialization

The nested controller in your question has the following method:

@FXML
public void initialize(URL location, ResourceBundle resources)

But the class does not implement javafx.fxml.Initializable. That means this initialize method is not actually being invoked.

You should have either:

import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.Initializable;

public class Controller implements Initializable {

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // ...
    }
}

Or:

import java.net.URL; // omit if not needed
import java.util.ResourceBundle; // omit if not needed
import javafx.fxml.FXML;

public class Controller {

    @FXML private URL location; // inject location; omit if not needed
    @FXML private ResourceBundle resources; // inject resources; omit if not needed

    @FXML
    private void initialize() { // a no-argument 'initialize' method
        // ...
    }
}

Note the latter approach—the one that does not implement Initializable—is the preferred approach.

If your controller does not need any additional initialization, then you can omit the initialize method entirely.


Demonstration

This demo only shows injecting a nested controller and calling methods on it and the objects it contains. It does not show how to use an architecture like MVC.

The demo has the nested controller initialize a "model" object that simply has an integer property. Said nested controller increments this property every time the button is fired. The "parent" controller goes through this "model" object to get the property and binds a label's text property to it.

Note this example expects the FXML resources to be at the root of the class-path/module-path (i.e., in the unnamed/default package).

Code

module-info.java (only needed if code is modular):

module app {
    requires javafx.controls;
    requires javafx.fxml;
    exports com.example.app to javafx.graphics;
    opens com.example.app to javafx.fxml;
}

Main.java:

package com.example.app;

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 {

    @Override
    public void start(Stage primaryStage) throws Exception {
        Parent root = FXMLLoader.load(Main.class.getResource("/Main.fxml"));
        primaryStage.setScene(new Scene(root, 500, 300));
        primaryStage.show();
    }
}

MainController.java:

package com.example.app;

import javafx.beans.binding.StringBinding;
import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class MainController {

    @FXML private NestedController nestedController; // inject nested controller
    @FXML private Label label;

    @FXML
    private void initialize() {
        // print statement to show order of controller initialization
        System.out.println("Initializing main controller...");

        // Show use of nested controller during initialization of "parent" controller
        StringBinding binding = nestedController
                .getModel()
                .countProperty()// access attribute of "another class"
                .asString("You clicked the button %,d times(s)!");
        label.textProperty().bind(binding);
    }
}

Main.fxml:

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

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.StackPane?>

<BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
            fx:controller="com.example.app.MainController">
    <padding>
        <Insets topRightBottomLeft="10"/>
    </padding>
    <center>
        <StackPane>
            <Label fx:id="label"/>
        </StackPane>
    </center>
    <bottom>
        <fx:include fx:id="nested" source="Nested.fxml"/>
    </bottom>
</BorderPane>

NestedController.java:

package com.example.app;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;

public class NestedController {

    private final ClickCountModel model = new ClickCountModel();

    public ClickCountModel getModel() {
        return model;
    }

    @FXML
    private void initialize() {
        // print statement to show order of controller initialization
        System.out.println("Initializing nested controller...");
    }

    @FXML
    private void handleAction(ActionEvent event) {
        event.consume();
        model.incrementCount();
    }
}

Nested.fxml:

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

<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.StackPane?>

<StackPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
           fx:controller="com.example.app.NestedController">
    <Button text="Click me!" onAction="#handleAction"/>
</StackPane>

ClickCountModel.java:

package com.example.app;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class ClickCountModel {

    private final IntegerProperty count = new SimpleIntegerProperty(this, "count");
    public final void setCount(int count) { this.count.set(count); }
    public final int getCount() { return count.get(); }
    public final IntegerProperty countProperty() { return count; }

    public void incrementCount() {
        setCount(getCount() + 1);
    }
}

In Action

GIF:

GIF of demo code in action

Console output:

Initializing nested controller...
Initializing main controller...
Slaw
  • 37,820
  • 8
  • 53
  • 80
  • **Comments have been [moved to chat](https://chat.stackoverflow.com/rooms/253917/discussion-on-answer-by-slaw-how-to-properly-use-nested-controllers-to-access-an); please do not continue the discussion here.** Before posting a comment below this one, please review the [purposes of comments](/help/privileges/comment). Comments that do not request clarification or suggest improvements usually belong as an [answer](/help/how-to-answer), on [meta], or in [chat]. Comments continuing discussion may be removed. – Dharman Jun 01 '23 at 15:37