0

I am attempting to add data to a TableView that has a different controller than the main application. After some tinkering, I did this for some time with a static TableView and a static method for calling the updates. However, I've run into problems with this approach elsewhere in the code and some other research led me to believe that the FXMLLoader might help.

But the data that should be added does not show up in the table. The System.out.println("adding info"); shows up in the console twice as expected, but the table stays empty. It was populated when using the static approach. I am guessing my FXMLLoader is creating a different instance than the one that was created at program start. What is the problem in the code below, mostly in the main class the showMainStage part?

Main class:

package test.controller;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class InteractionTest extends Application {

    private Stage mainStage;

    /**
     * Show the main window.
     *
     * @throws IOException
     */
    private void showMainStage() throws IOException {
        final FXMLLoader loader = new FXMLLoader();
        loader.setLocation(getClass().getResource("/fxml/Main.fxml"));
        final Parent root = loader.load();
        final BorderPane bp = (BorderPane) root;
        mainStage = new Stage();
        mainStage.setScene(new Scene(bp));
        mainStage.setTitle("Interaction test");
        mainStage.setMaximized(false);
        mainStage.show();

        final Info i1 = new Info("1");
        i1.setPosition(1);
        i1.setTitle("Info 1");
        final Info i2 = new Info("2");
        i2.setPosition(2);
        i2.setTitle("Info 2");
        final List<Info> infoList = new ArrayList<>();
        infoList.add(i1);
        infoList.add(i2);
        final FXMLLoader tableLoader = new FXMLLoader(getClass().getResource("/fxml/InfoTable.fxml"));
        final Parent parent = tableLoader.load();
        final InfoTableController itc = (InfoTableController) tableLoader.getController();
        itc.updateTable(infoList);
    }

    @Override
    public void start(final Stage initStage) throws Exception {
        try {
            showMainStage();
        } catch (final IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * Main method.
     *
     * @param args
     */
    public static void main(final String[] args) {
        launch();
    }

}

TableController:

package test.controller;

import java.io.IOException;
import java.util.List;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.SortType;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;

/**
 * Holds a {@link TableView} to display the infos for further interaction. Is
 * the controller for InfoTable.fxml.
 *
 */
public class InfoTableController {

    /**
     * A {@link ScrollPane} for the {@link Info} table.
     */
    @FXML
    private ScrollPane infoTablePane;

    /**
     * A {@link TableView} for the {@link Info}s.
     */
    private final TableView<Info> table = new TableView<>();

    /**
     * Build the column headers during initialization.
     */
    @FXML
    public void initialize() {
        final TableColumn<Info, String> positionColumn = new TableColumn<>("#");
        positionColumn.setEditable(false);
        positionColumn.setPrefWidth(15.0);
        positionColumn.setMaxWidth(50.0);
        positionColumn.setSortable(true);
        positionColumn.setSortType(SortType.ASCENDING);
        positionColumn.setCellValueFactory(new PropertyValueFactory<>("position"));
        final TableColumn<Info, String> titleColumn = new TableColumn<>("Title");
        titleColumn.setEditable(true);
        titleColumn.setPrefWidth(200.0);
        titleColumn.setMaxWidth(1000.0);
        titleColumn.setCellValueFactory(new PropertyValueFactory<>("title"));

        table.getColumns().addAll(positionColumn, titleColumn);
        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
        table.getSortOrder().add(positionColumn);
        table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        table.getSelectionModel().setCellSelectionEnabled(true);
        table.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
            final String id = newValue.getId();
            final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/InfoDisplay.fxml"));
            try {
                final Parent parent = loader.load();
            } catch (final IOException e1) {
                e1.printStackTrace();
            }
            final InfoDisplayController idc = (InfoDisplayController) loader.getController();
            idc.displayDocument(id);
        });
        infoTablePane.setContent(table);
        infoTablePane.setFitToWidth(true);
        infoTablePane.setFitToHeight(true);
    }

    /**
     * Sorts the {@link #table}.
     */
    public void sortTable() {
        table.sort();
    }

    /**
     * Adds table entries.
     *
     */
    public void updateTable(final List<Info> infoList) {
        table.getItems().clear();
        for (final Info info : infoList) {
            System.out.println("adding info");
            table.getItems().add(info);
        }
    }
}

DisplayController:

package test.controller;

import javafx.fxml.FXML;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;

/**
 * Displays infos for reading. Controller for InfoDisplay.fxml.
 *
 */
public class InfoDisplayController {

    @FXML
    private TabPane infoDisplayTabPane;

    /**
     * Constructor.
     */
    public InfoDisplayController() {

    }

    @FXML
    public void initialize() {
        infoDisplayTabPane.getTabs().add(new Tab("test in class"));
    }

    /**
     * Displays the selected document in tabs.
     *
     * @param id The selected document's id.
     */
    public void displayDocument(final String id) {
        // get the underlying Lucene document with the id; omitted for this example
        System.out.println("attempting to display " + id);
        infoDisplayTabPane.getTabs().clear();
        final TextArea textArea = new TextArea("My info text.");
        final ScrollPane scrollPane = new ScrollPane(textArea);
        final Tab tab = new Tab("info", scrollPane);
        infoDisplayTabPane.getTabs().add(tab);
    }
}

Info object:

package test.controller;

import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;

public class Info {

    /**
     * The id as pulled from the Lucene index.
     */
    private final String id;

    /**
     * The position.
     */
    private final SimpleIntegerProperty position = new SimpleIntegerProperty(0);

    /**
     * The title.
     */
    private final SimpleStringProperty title = new SimpleStringProperty("Title");

    /**
     * Constructor.
     */
    public Info(final String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }

    public int getPosition() {
        return position.get();
    }

    public void setPosition(final int position) {
        this.position.set(position);
    }

    public String getTitle() {
        return title.get();
    }

    public void setTitle(final String title) {
        this.title.set(title);
    }
}

Main.fxml:

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

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Text?>

<BorderPane id="BorderPane"
    fx:controller="test.controller.InteractionTest"
    xmlns:fx="http://javafx.com/fxml">
    <top>
    </top>
    <center>
        <SplitPane dividerPositions="0.5" orientation="HORIZONTAL"
            focusTraversable="true">
            <items>
                <fx:include source="InfoTable.fxml" />
                <fx:include source="InfoDisplay.fxml" />
            </items>
        </SplitPane>
    </center>
    <bottom>
    </bottom>
</BorderPane>

InfoTable.fxml

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

<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>

<ScrollPane xmlns:fx="http://javafx.com/fxml/1"
    fx:controller="test.controller.InfoTableController"
    fx:id="infoTablePane">
</ScrollPane>

InfoDisplay.fxml

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

<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane xmlns:fx="http://javafx.com/fxml/1"
    prefHeight="500" prefWidth="500" minHeight="200" minWidth="200"
    fx:controller="test.controller.InfoDisplayController">
    <center>
        <TabPane fx:id="infoDisplayTabPane"><Tab text="test"></Tab></TabPane>
    </center>
</BorderPane>
ramundsen
  • 73
  • 4

2 Answers2

0

Well your DataTable have no relation to the MainStage you initialized earlier. The included Datatable in the FXML-File is a different from your created one afterwards. I would suggest that you create a Node in your Main.fxml and when you load the second file, you override this node. For example, one of my code bases contains:

loadingWidget.start();
    Task <Parent> task = new Task<Parent>() {
        @Override
        protected Parent call() throws Exception {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("/translationCreator.fxml"));

            return loader.load();
        }       
    };

    task.setOnFailed(new EventHandler<WorkerStateEvent>() {
        @Override
        public void handle(WorkerStateEvent event) {
            loadingWidget.close();
            task.getException().printStackTrace();
            Notifications.create().title("Something went wrong!").text("Error while loading resource tableWindow.fxml").show(); 
        }       
    });

    task.setOnSucceeded(new EventHandler <WorkerStateEvent>() {
        @Override
        public void handle(WorkerStateEvent event) {
            loadingWidget.close();
            contentPane.getChildren().clear();
            contentPane.getChildren().add(task.getValue()); 
        }           
    });

    new Thread(task).start();

So i just added my new Node to the Content Pane, i defined in my FXML-File. Afterwards this should easily work, you don't have to define something like Static Controllers. If you can't understand this, i will try to give you a full example :)

0

If you load a fxml using FXMLLoader and modify the resulting scene via controller, this has no effect on any scenes created based on the same fxml with another FXMLLoader.load call.

Your code assumes this is the case though:

After calling loader.load(); there are:

  • 2 instances of InteractionTest; only one of those is used as controller
  • 1 version of the InfoTable.fxml that is created based on <fx:include source="InfoTable.fxml" /> and one instance of InfoTableController that is used for the part of the scene.

Later in the showMainStage method you create another version of this scene together with another InfoTableController instance using tableLoader.load(). You never show this scene, but this is the only one you initialize the table for. For the first version of the fxml you never do this initialisation.

There is a similar issue in your listener to the selected item in the table.

I don't recommend using the Application class as controller. Instead create a seperate controller class and use it to pass the data to/between the nested fxmls:

Main.fxml

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

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Text?>

<BorderPane id="BorderPane"
    fx:controller="test.controller.MainController"
    xmlns:fx="http://javafx.com/fxml">
    <center>
        <SplitPane dividerPositions="0.5" orientation="HORIZONTAL"
            focusTraversable="true">
            <items>
                <fx:include fx:id="table" source="InfoTable.fxml" />
                <fx:include fx:id="display" source="InfoDisplay.fxml" />
            </items>
        </SplitPane>
    </center>
</BorderPane>
public class MainController {

    // fields for injection of nested controllers
    @FXML
    private InfoDisplayController displayController;
    @FXML
    private InfoTableController tableController;

    @FXML
    private void initialize() {
        // connect selected table item with displayed document
        tableController.selectedItemProperty().addListener((o, oldValue, newValue) -> {
            final String id = newValue.getId();
            displayController.displayDocument(id);
        });
    }

    public void updateTable(final List<Info> infoList) {
        tableController.updateTable(infoList);
    }
}
public class InfoTableController {

    ...

    @FXML
    public void initialize() {
        ...
//        table.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
//            final String id = newValue.getId();
//            final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/InfoDisplay.fxml"));
//            try {
//                final Parent parent = loader.load();
//            } catch (final IOException e1) {
//                e1.printStackTrace();
//            }
//            final InfoDisplayController idc = (InfoDisplayController) loader.getController();
//            idc.displayDocument(id);
//        });
        ...
    }

    public ObjectProperty<Info> selectedItemProperty() {
        return table.getSelectionModel().selectedItemProperty();
    }
}
private void showMainStage() throws IOException {
    final Info i1 = new Info("1");
    i1.setPosition(1);
    i1.setTitle("Info 1");
    final Info i2 = new Info("2");
    i2.setPosition(2);
    i2.setTitle("Info 2");
    final List<Info> infoList = new ArrayList<>();
    infoList.add(i1);
    infoList.add(i2);

    final FXMLLoader loader = new FXMLLoader();
    loader.setLocation(getClass().getResource("/fxml/Main.fxml"));
    final Parent root = loader.load();
    ((MainController)loader.getController()).updateTable(infoList);
    ...


//    final FXMLLoader tableLoader = new FXMLLoader(getClass().getResource("/fxml/InfoTable.fxml"));
    ...
}
Community
  • 1
  • 1
fabian
  • 80,457
  • 12
  • 86
  • 114
  • Thanks a lot, this has cleared it up for me. Initially, I kept wondering how the two @FXML fields in MainController were assigned. But it turns out I was missing the fx:id="table" and fx:id="display" attributes in the Main.fxml. Somehow, the "Controller" bit in the variable name, e.g. tableController, seems to be supplied automatically. Is that correct, or am I still missing something? – ramundsen Dec 04 '18 at 14:32
  • @ramundsen You always append `Controller` to the `fx:id` to get the field name, see also https://stackoverflow.com/questions/44467982/javafx8-fxml-naming-of-nested-controllers – fabian Dec 04 '18 at 14:43