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