2

This is my first FXML application. Previously I've gotten pretty comfortable with Swing. I simply want to save several values from the controls on the stage, so that user-input values reload across runs. The ReliableOneShotCloseHandler is a JavaFX version of something I posted a while ago here. However, by the time the windowClosing function is called, it seems that the controls have already been disposed of ... ? I get a NullPointerException at the indicated line:

package testfxmlloader;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.fxml.JavaFXBuilderFactory;
import javafx.scene.control.Label;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;

/**
*
* @author maskedcoder
*/
public class FXMLDocumentController implements Initializable {

    @FXML
    private Label label;

    @FXML
    private AnchorPane apMain;

    @FXML
    private void handleButtonAction(ActionEvent event) {
        System.out.println("You clicked me!");
        label.setText("Hello World!");
    }

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        ReliableOneShotCloseHandler roscSaveSettings = new ReliableOneShotCloseHandler(apMain.getScene().getWindow(), new ReliableOneShotCloseHandler.CloseDuties() {

            @Override
            public boolean confirmClose(WindowEvent e) {
                return true;
            }

            @Override
            public void windowClosing(WindowEvent e) {
                saveSettings();
            }
        });
    }   

    public void saveSettings() {
        System.out.println("save setting: " + label.getText());  // gets NullPointerException because label is null.
    }

}

class ReliableOneShotCloseHandler {

    public interface CloseDuties {
        public boolean confirmClose(WindowEvent e);
        public void windowClosing(WindowEvent e);
    }

    private final CloseDuties closeDuties;
    private boolean windowClosingFired = false;
    private final EventHandler<WindowEvent> openEvent;
    private final EventHandler<WindowEvent> closeEvent;

    public ReliableOneShotCloseHandler(Window thisWindow, CloseDuties iniCloseDuties) {
        super();
        closeDuties = iniCloseDuties;
        openEvent = (WindowEvent event) -> {
            windowClosingFired = false;
        };
        closeEvent = (WindowEvent event) -> {
            if(!windowClosingFired) {
                if(closeDuties.confirmClose(event)) {
                    closeDuties.windowClosing(event);
                    windowClosingFired = true;
                }
                else
                    event.consume();
            }
        };

        thisWindow.setOnShowing(openEvent);
        thisWindow.setOnShown(openEvent);
        thisWindow.setOnCloseRequest(closeEvent);
        thisWindow.setOnHiding(closeEvent);
        thisWindow.setOnHidden(closeEvent);
    }
}

public class TestFXMLLoader extends Application {
    FXMLLoader fxmll;
    FXMLDocumentController fdc;

    @Override
    public void start(Stage stage) throws Exception {
        fdc = new FXMLDocumentController();
        fxmll = new FXMLLoader();
        fxmll.setBuilderFactory(new JavaFXBuilderFactory());
        Parent root = fxmll.load(getClass().getResource("FXMLDocument.fxml"));
        fxmll.setController(fdc);

        Scene scene = new Scene(root);

        stage.setScene(scene);
        stage.show();
    }

    /**
    * @param args the command line arguments
    */
    public static void main(String[] args) {
        launch(args);
    }

}

And here's the FXML:

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

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane id="AnchorPane" fx:id="apMain" prefHeight="200" prefWidth="320" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8.0.40" fx:controller="testfxmlloader.FXMLDocumentController">
    <children>
        <Button fx:id="button" layoutX="126" layoutY="90" onAction="#handleButtonAction" text="Click Me!" />
        <Label fx:id="label" layoutX="126" layoutY="120" minHeight="16" minWidth="69" />
    </children>
</AnchorPane>

The windowClosing function should be called before there's even permission to close, so the controls should all be functioning. The test code above is based off of the NetBeans' default JavaFXML Application, with just the addition of the ReliableOneShotCloseHandler and attendant code. Is there a flaw in my logic? Is there another way to do this?

Update: Some lines of code were included that shouldn't have been. They've been removed.

Community
  • 1
  • 1
Masked Coder
  • 280
  • 2
  • 15

1 Answers1

2

Well from an architectural point of view this code is quite a mess. In general I would clearly separate between the controller and the application class. Hence, the application class only loads the FXML-file, from where the controller class is refereed, and assigns it then to a scene or more precisely a stage. The controller class therefore is only responsible to manage any events from your control elements as your onCloseRequest-Event.

A further problem is might also the use of both an EventFilter and an EventHandler. In case you consume the EventFilter on your stage all its nodes and the stage itself will never get invoked their EventHandler.

The simplest solution therefore would be if you add an EventHandler directly on the stage in your controller class from where you have directly access to all other controls (e.g. your label).

Main.java

public class Main extends Application {

@Override
public void start(Stage primaryStage) throws Exception{
    FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml"));
    Parent root = (Parent) loader.load();
    primaryStage.setTitle("Hello World");
    primaryStage.setScene(new Scene(root, 300, 275));
    primaryStage.show();

    Controller controller = (Controller)loader.getController();
    controller.setStage(primaryStage);
}


public static void main(String[] args) {
    launch(args);
}

} 

sample.fxml

<AnchorPane fx:id="apMain" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
   <children>
      <Button fx:id="button" layoutX="39.0" layoutY="35.0" mnemonicParsing="false" onAction="#handleButtonAction" text="Button" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="10.0" />
      <Label fx:id="label" layoutX="52.0" layoutY="102.0" text="Label" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="50.0" />
   </children>
</AnchorPane>

Controller.java

public class Controller implements Initializable {

@FXML
public AnchorPane apMain;

@FXML
public Button button;

@FXML
public Label label;

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

}

public void handleButtonAction() {
    System.out.println("You clicked me!");
    label.setText("Hello World!");
}

public void setStage(Stage stage){
    stage.setOnCloseRequest(new EventHandler<WindowEvent>() {
        @Override
        public void handle(WindowEvent event) {
            System.out.println(label.getText());
        }
    });
}

}
Michael Stauffer
  • 298
  • 1
  • 4
  • 15
  • LOL I never expected my code to be considered a mess! I think you're basically saying I don't understand the FXML mechanism, which is true enough. I think you're also suggesting I should handle the `onCloseRequest` stuff in `FXMLDocumentController` which I just edited the code to reflect. I removed those `addEventFilter` lines which shouldn't have been there; they were commented out in the original code & I'm not sure how they got in. I added a variable for the top level `AnchorPane` but when I try `AnchorPane.getScene().getWindow()`, `AnchorPane.getScene()` is null too. – Masked Coder Sep 09 '15 at 22:11
  • The problem is that the stage is not yet existing at this moment. Hence, you need to pass it separately later on to the controller class. For further information I refer to this http://stackoverflow.com/questions/13246211/javafx-how-to-get-stage-from-controller-during-initialization. – Michael Stauffer Sep 10 '15 at 07:06
  • that makes sense to me but `.getController()` always returns null, whether I put it where you did, after `stage.show()` or earlier. I tried, in my original code, to simply _create_ and then _set_ the controller but that only returns me to my original problem; even using `setOnCloseRequest` in the `setStage` function as you have, `label` is still `null` by the time the `closeRequest` event handler is fired. – Masked Coder Sep 10 '15 at 08:03
  • Well what is also strange in your code is that you set your controller in your application class, even though you have already referred it in your FXML-file. However, I would suggest you simply copy paste my solution and try it. – Michael Stauffer Sep 10 '15 at 08:12
  • Just got a chance to try your code. It works! I don't see any difference from what I originally had (before even this question). But it's something I can work with. Thanks! – Masked Coder Sep 21 '15 at 00:55