0

I am currently trying to create lists to show on a board (each list has buttons and a title and an additional vbox to store things inside of it). When I create a new list to show it on my board, all the @FXML annotated fields are left with null (but the children of the object exist). The specific line of code is: ListCtrl listObject = new ListCtrl(); I am suspecting the injector is at fault as I am not really sure how to use it. Here is my code.

List.fxml

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

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="240.0" prefWidth="180.0" style="-fx-border-color: black; -fx-border-width: 10;" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="client.scenes.ListCtrl">
   <children>
      <HBox alignment="TOP_RIGHT" prefHeight="19.0" prefWidth="140.0">
         <children>
            <Button fx:id="listEditButton" mnemonicParsing="false" text="Edit" />
            <Button fx:id="listCloseButton" mnemonicParsing="false" text="X" />
         </children>
      </HBox>
      <Label id="listTitle" fx:id="listTitle" alignment="CENTER" prefHeight="17.0" prefWidth="206.0" text="Default List Name" />
      <VBox fx:id="cardBox" prefHeight="142.0" prefWidth="140.0" />
      <Button id="listAddCard" fx:id="listAddCard" alignment="CENTER" mnemonicParsing="false" prefHeight="25.0" prefWidth="202.0" text="Add card" />
   </children>
   <padding>
      <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
   </padding>
</VBox>

The controller of the list

package client.scenes;

import jakarta.inject.Inject;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.fxml.Initializable;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox;

import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;

public class ListCtrl extends AnchorPane implements Initializable{
    @FXML
    private Label listTitle;
    @FXML
    private VBox cardBox;
    @FXML
    private Button listAddCard;
    @FXML
    private Button listCloseButton;
    @FXML
    private Button listEditButton;

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

    }
    @Inject
    public ListCtrl(){
        super();
        try
        {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("List.fxml"));
            Parent root = loader.load();
            //Node n = loader.load();
            //this.getChildren().add(n);

        } catch (IOException ix){

        }
    }


    /** Sets the text of the title of the list
     * @param text
     */
    public void setListTitleText(String text) {
        listTitle.setText(text);
    }

    public void addCardToList(Node card){
        cardBox.getChildren().add(card);
    }

    /**
     * @return the title of the list
     */
    public Label getListTitle() {
        return listTitle;
    }

    /**
     * @return the Edit button of the list
     */
    public Button getListEditButton() {
        return listEditButton;
    }

    /**
     * @return the X button for the list
     */
    public Button getListCloseButton() {
        return listCloseButton;
    }

    /**
     * @return the list button of the list
     */
    public Button getListAddCardButton() {
        return listAddCard;
    }
}

The part of the code where I am creating a new list

public void refresh() {
        mainBoard.getChildren().clear();
        var lists = fakeServer.getBoardLists();
        data = FXCollections.observableList(lists);

        for (BoardList currentList : data) {
            ListCtrl listObject = new ListCtrl(); ///Instantiating a new list to be shown
            listObject.setListTitleText(currentList.title); //Setting the title of the list
            ObservableList<Card> cardsInList =
                FXCollections.observableList(fakeServer.getCards(currentList));
            for (Card currentCard : cardsInList) {
                CardCtrl cardObject = new CardCtrl(); ///Instantiating a new card to be shown
                cardObject.setCardTitleText(currentCard.title); //Setting the title of the card
                listObject.addCardToList(cardObject); //Adding the card to the list
            }
            listObject.getListAddCardButton().setOnAction(event -> mainCtrl.showAddCard(currentList));
            mainBoard.getChildren().add(listObject);
        }
    }

I tried looking up information on how to use the injector for objects created through code (so objects which are not already on the main board) and didn't succeed. Thank you in advance!

EDIT:

I changed my refresh method to this:

public void refresh() {
        mainBoard.getChildren().clear();
        var lists = fakeServer.getBoardLists();
        data = FXCollections.observableList(lists);

        for (BoardList currentList : data) {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("List.fxml"));
            ListCtrl listObject = loader.getController(); ///Instantiating a new list to be shown
            listObject.setListTitleText(currentList.title); //Setting the title of the list
            ObservableList<Card> cardsInList =
                FXCollections.observableList(fakeServer.getCards(currentList));
            for (Card currentCard : cardsInList) {
                CardCtrl cardObject = new CardCtrl(); ///Instantiating a new card to be shown
                cardObject.setCardTitleText(currentCard.title); //Setting the title of the card
                listObject.addCardToList(cardObject); //Adding the card to the list
            }
            listObject.getListAddCardButton().setOnAction(event -> mainCtrl.showAddCard(currentList));
            //mainBoard.getChildren().add(listObject);
        }
    }

And now listObject is null. Have I used the loader incorrectly?

  • 3
    The FXML-injected fields will only be injected into the controller instance managed by the `FXMLLoader`. You are creating instances of `ListCtrl` manually, and those instances are _not_ managed by an `FXMLLoader`. Thus, the fields are never injected and remain `null`. You have an `fx:controller` attribute in your FXML file. This means the `FXMLLoader` will instantiate the controller for you. That's the managed instance. If you must interact with this controller instance, then call `FXMLLoader#getController()` (after you load the FXML file). – Slaw Mar 09 '23 at 11:44
  • @Slaw I don't quite understand. So what I could do is remove the fx:controller attribute and then after creating `ListCtrl listObject = new ListCtrl();` what do I do? In my head, when I call the constructor that is when the listObject is shown. Where should I call `FXMLLoader#setController(listObject)`? – Alexandru Fazakas Mar 09 '23 at 11:44
  • 3
    The other option is to get rid of the `fx:controller` attribute. Now you are responsible for setting the controller instance. This is done by calling `FXMLLoader#setController(Object)` (before you load the FXML file). In your case, it would seem you'd want to call `loader.setController(this)` inside the `ListCtrl` constructor. But note that it does not make much sense for an FXML controller to load its associated FXML file inside the constructor unless you're using `fx:root`. Additionally, your controller should not extend a layout (e.g., `AnchorPane`) unless you're using `fx:root`. – Slaw Mar 09 '23 at 11:48
  • 3
    You should also never swallow an exception (i.e., never simply `catch` an exception and then do nothing with it). At the very least, you should call `printStackTrace()` on the exception so that you have some indication that something went wrong. Though failing to load an FXML file is typically not a recoverable situation. So, it would make sense to let the exception propagate out to the caller. If you don't want to declare the constructor or method to throw the checked `IOException`, then wrap the `IOException` in an `UncheckedIOException` and throw that instead. – Slaw Mar 09 '23 at 11:51
  • 2
    As an aside, I would expect your code to throw a `StackOverflowError`. Your `List.fxml` file has an `fx:controller="client.scenes.ListCtrl"` attribute. As stated earlier, this means the `FXMLLoader` will instantiate that controller for you. This obviously involves invoking a constructor--by default, the no-arg constructor. Yet in that constructor you load the very same `List.fxml` file. Ultimately that should cause infinite recursion. – Slaw Mar 09 '23 at 11:53
  • [mcve] please .. – kleopatra Mar 09 '23 at 11:53
  • Doesn’t this code generate a `StackOverflowError`? Creating a `ListCtrl` instance loads the FXML, which causes a `ListCtrl` instance to be created (via the `fx:controller` attribute), which loads the FXML again, etc? – James_D Mar 09 '23 at 11:53
  • @Slaw I have just looked up fx:root and that's exactly what I think I should be using in this case. The only thing I would have to modify is 1. Removing fx:controller 2. remvoing extend anchor pane 3. adding fx:root 4. adding loader.setController(this) Right? – Alexandru Fazakas Mar 09 '23 at 11:59
  • @kleopatra first question on stack overflow, sorry, didn't think about that but thanks for letting me know @James_D somehow it doesn't do that, probably because it doesn't get past `listObject.setTitle` since it throws a nullPointerException – Alexandru Fazakas Mar 09 '23 at 12:01
  • *” I have just looked up fx:root and that's exactly what I think I should be using in this case.”* I don’t think that’s the best solution. Just remove the constructor entirely from `ListCtrl` and remove `extends AnchorPane`. Move the `FXMLLoader` code to the loop in your `refresh()` method. Add `ListCtrl listObject = loader.getController()` and use that to call methods on the controller. (This is just what @slaw suggested in the first comment.) – James_D Mar 09 '23 at 12:02
  • Your current setup does not seem designed for `fx:root`. But I leave the choice of whether to use `fx:root` or not to you. For an example on how to use `fx:root`, check out [the documentation](https://openjfx.io/javadoc/19/javafx.fxml/javafx/fxml/doc-files/introduction_to_fxml.html#custom_components). – Slaw Mar 09 '23 at 12:13
  • 1
    As another aside, I see you've added Jakarta CDI as a dependency and have made use of the `@jakarta.inject.Inject` annotation. If you've added CDI in the hopes of fixing your `null` FXML-injected field problem, then note that CDI has nothing to do with FXML injection. They are two different frameworks. Otherwise, if you're using CDI throughout your project, and your goal is to use CDI injection (in addition to FXML injection) with your FXML controllers, then note that's a little more involved. You'll need the controller to be managed by CDI as well as the `FXMLLoader`. This can be done [cont]. – Slaw Mar 09 '23 at 12:18
  • @James_D I have also tried your approach (see edited question) but now my listObject is null. Any idea why that is happening? Should I provide more code? – Alexandru Fazakas Mar 09 '23 at 12:20
  • [cont]. by setting a [controller factory](https://openjfx.io/javadoc/19/javafx.fxml/javafx/fxml/FXMLLoader.html#setControllerFactory(javafx.util.Callback)) on the `FXMLLoader` and creating the controller instances via a [`jakarta.enterprise.inject.Instance`](https://jakarta.ee/specifications/cdi/4.0/apidocs/jakarta.cdi/jakarta/enterprise/inject/instance) (how you get a reference to an `Instance` I leave to you). Though note it doesn't make much sense to annotate a no-arg constructor with `@Inject`. If there's no parameters, there's nothing for CDI to inject. – Slaw Mar 09 '23 at 12:20
  • "_but now my listObject is null_" -- Note I mentioned that `getController()` must be called **after** you load the FXML file. Otherwise, the controller will not have been instantiated yet (assuming use of `fx:controller`) and that method will return `null`. – Slaw Mar 09 '23 at 12:26
  • @Slaw Wow thank you so much that solved it, now my @FXML members are no longer null. But now I have another problem. When I try to add the listObject to the main board (the line that was previously commented out `mainBoard.getChildren().add(listObject);`) It tells me that ListCtrl is not the same as Node (of course). Now I need a way to add the ListCtrl to the main board and I don't really know how to either downcast it to a node (doubt that it is possible) or to find another way to show it on the main board. – Alexandru Fazakas Mar 09 '23 at 12:39
  • 1
    Add the node you get from calling `loader.load()`, not the controller. – James_D Mar 09 '23 at 12:43
  • 1
    Thank you James and Slaw. You have managed to solve my problems. Many thanks. – Alexandru Fazakas Mar 09 '23 at 12:48

1 Answers1

4

Summarizing the comments below the OP as an answer:

While other solutions are possible (e.g. using a dynamic root to create a custom component), I would recommend using the standard FXML approach here. By this, I mean use the FXML file to define the UI and keep some degree of separation between the UI logic (the controller, which should not be a UI component) and the UI view (the FXML).

@FXML-annotated fields are only initialized in the controller object that is created by the FXMLLoader when load() is called; they are not somehow magically initialized in other instances of the controller class that are created by calling the constructor.

You should move thus not make your controller class a subclass of AnchorPane (or any other UI class) and should not load the FXML from the constructor (because the controller is created from loading the FXML, not the other way round).

public class ListCtrl {
    @FXML
    private Label listTitle;
    @FXML
    private VBox cardBox;
    @FXML
    private Button listAddCard;
    @FXML
    private Button listCloseButton;
    @FXML
    private Button listEditButton;

       

    /** Sets the text of the title of the list
     * @param text
     */
    public void setListTitleText(String text) {
        listTitle.setText(text);
    }

    public void addCardToList(Node card){
        cardBox.getChildren().add(card);
    }

    /**
     * @return the title of the list
     */
    public Label getListTitle() {
        return listTitle;
    }

    /**
     * @return the Edit button of the list
     */
    public Button getListEditButton() {
        return listEditButton;
    }

    /**
     * @return the X button for the list
     */
    public Button getListCloseButton() {
        return listCloseButton;
    }

    /**
     * @return the list button of the list
     */
    public Button getListAddCardButton() {
        return listAddCard;
    }
}

Move the responsibility for loading the FXML to the point where you need the UI defined there, and retrieve the controller by calling getController() on the FXML:

public void refresh() {
    mainBoard.getChildren().clear();
    var lists = fakeServer.getBoardLists();
    data = FXCollections.observableList(lists);

    for (BoardList currentList : data) {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("List.fxml"));
        Parent card = loader.load();
        ListCtrl listObject = loader.getController(); 
        listObject.setListTitleText(currentList.title); //Setting the title of the list
        ObservableList<Card> cardsInList =
            FXCollections.observableList(fakeServer.getCards(currentList));
        for (Card currentCard : cardsInList) {
            CardCtrl cardObject = new CardCtrl(); ///Instantiating a new card to be shown
            cardObject.setCardTitleText(currentCard.title); //Setting the title of the card
            listObject.addCardToList(cardObject); //Adding the card to the list
        }
        listObject.getListAddCardButton().setOnAction(event -> mainCtrl.showAddCard(currentList));
        mainBoard.getChildren().add(card);
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322