Search code examples
javajavafxfxmlopenjfx

Javafx not injecting to @FXML annotated members when creating new class


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?


Solution

  • 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);
        }
    }