Issue
I've got an issue with passing parameters to controller. The main program uses a controller factory to pass a stage object to my controller. The controller factory prints the address of the stage object, but the controller gets Null as stage object. Why ?
Though I've reduced my application to a simple dialogue, I can't find my fault. I hope you can help. Thanks!
The main program:
public class Main extends Application {
private final Logger logger = Logger.getLogger(this.getClass().getName());
private final String FXML_SIMPLE_DIALOG = "testDialog.fxml";
private MyHandler myHandler = new MyHandler();
@Override
public void init() {
try{
// Thread.currentThread is the FX-Launcher thread:
Thread.currentThread().setUncaughtExceptionHandler(myHandler);
try {
logger.addHandler(new FileHandler("java.myLOG"));
}
catch (IOException e) {
throw new IllegalStateException("IOException when adding File Handler");
}
}
catch (Exception ex) {
myHandler.uncaughtException(Thread.currentThread(), ex);
throw(ex);
}
}
@Override
public void start(Stage primaryStage) {
try{
logger.info("Test Application started");
Thread.currentThread().setUncaughtExceptionHandler(myHandler);
try{
URL location = new URL(this.getClass().getResource("resources/fxml/" + FXML_SIMPLE_DIALOG).toString());
FXMLLoader loader = new FXMLLoader(location);
loader.setControllerFactory(new SimpleControllerFactory(primaryStage));
Pane root = loader.load();
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.setTitle("*** TEST App ***");
primaryStage.show();
}
catch(IOException ex) {
ex.printStackTrace();
throw new IllegalStateException("check program source code!");
}
}
catch(Exception ex) {
myHandler.uncaughtException(Thread.currentThread(), ex);
throw(ex);
}
}
public static void main(String[] args) {
launch(args);
}
class MyHandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread thread, Throwable throwable) {
logger.log(Level.SEVERE, "** STOPP ** due to uncaught exception", throwable);
Platform.exit();
}
}
}
The code of the controller factory:
public class SimpleControllerFactory implements Callback<Class<?>,Object> {
private static final Logger logger = Logger.getLogger(SimpleControllerFactory.class.getName());
private final Stage primaryStage;
public SimpleControllerFactory(Stage stage) {
this.primaryStage = stage;
System.out.println("controller factory: value of stage: " + this.primaryStage);
}
public SimpleControllerFactory() { this(null); }
@Override
public Object call(Class<?> type) {
try {
for (var c : type.getConstructors()) {
switch(c.getParameterCount()) {
case 0 -> {}
case 1 -> {
if ( c.getParameterTypes()[0] == Stage.class) {
return c.newInstance(primaryStage) ;
}
else;
}
default -> {}
}
}
return type.getDeclaredConstructor().newInstance();
}
catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException ex) {
logger.log(Level.SEVERE,ex.toString(),ex);
throw new RuntimeException(ex);
}
}
}
This are the FXML-file and the simple controller:
<VBox prefHeight="150.0" prefWidth="250.0" spacing="5.0" style="-fx-padding: 5 5 5 5;-fx-font-size: 11px"
xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="TestSimpleDialog.DialogController">
<children>
<ButtonBar buttonOrder="UL_HE+FBIX_NCYOA_R" prefHeight="40.0">
<buttons>
<Button alignment="CENTER" mnemonicParsing="false" onAction="#time" text="time ?">
</Button>
</buttons>
</ButtonBar>
<StackPane VBox.vgrow="ALWAYS">
<children>
<Label fx:id="textTime" alignment="CENTER" text="" />
</children>
</StackPane>
</children>
simple controller:
public class DialogController implements Initializable {
private static final Logger logger = Logger.getLogger(DialogController.class.getName());
private final Stage recentStage;
DialogController(Stage stage) {
this.recentStage = stage;
}
DialogController() { this(null); }
@FXML private Label textTime;
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
System.out.println("Controller started, value of recentStage: " + this.recentStage);
}
@FXML
private void time(ActionEvent event) {
textTime.setText(LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy kk:mm:ss")));
}
}
Solution
The Class.getConstructors()
method
Returns an array containing
Constructor
objects reflecting all the public constructors of the class represented by thisClass
object.
(my emphasis).
Since your constructors are not declared public, the for
loop in the factory's call(...)
method iterates zero times, and it defaults to the call to the no-arg constructor.
Just declare the constructors as public
and it will work:
public class DialogController implements Initializable {
private final Stage recentStage;
public DialogController(Stage stage) {
this.recentStage = stage;
}
public DialogController() { this(null); }
// ...
}
If you really want to keep the constructors non-public, you can us the getDeclaredConstructor(...)
method to retrieve a specific constructor, which does not have to be public:
@Override
public Object call(Class<?> type) {
try {
System.out.println("Number of constructors found: "+type.getConstructors().length);
try {
Constructor<?> c = type.getDeclaredConstructor(Stage.class);
return c.newInstance(primaryStage);
} catch (NoSuchMethodException e) {
return type.getDeclaredConstructor().newInstance();
}
}
catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException ex) {
logger.log(Level.SEVERE,ex.toString(),ex);
throw new RuntimeException(ex);
}
}
Answered By - James_D
Answer Checked By - Marilyn (JavaFixing Volunteer)