1

I have two windows and two controllers.

In the first window is a button which opens a second window and TableView. The second window is a form where I insert properties of my class. After inserting I click on "Save" and add new created object to a static list. This should be shown in a TableView. When I use only one window and one controller everything works fine but splitting the functionality as described in two windows the TableView is not refreshed.

Here is my minimal example:

MAIN CONTROLLER

public class Controller {

    @FXML
    public TableView tableView;

    @FXML
    private Button openWindow;


    // called by the FXML loader after the labels declared above are injected:
    public void initialize() {

        TableColumn mailColumn = new TableColumn("E-Mail");
        tableView.getColumns().addAll(mailColumn);
        mailColumn.setCellValueFactory(new PropertyValueFactory<Person,String>("mail"));


        openWindow.setOnAction((event) -> {
            new PersonInputForm(event);
        });


    }

    // ######   Receiver TableView Action Handling #######
    public void updateReceiverList(){
        final ObservableList<Person> data = FXCollections.observableArrayList(Memory.receiverList);
        tableView.getItems().clear();
        tableView.getItems().addAll(data);
    }

}

SECONDARY CONTROLLER

public class PersonInputFormController {


    @FXML
    private TextField mail;

    @FXML
    private AnchorPane personInputFormAnchorPane;

    private Controller mainController ;
    Stage stage;

    public void setStage() {
        stage = (Stage) personInputFormAnchorPane.getScene().getWindow();
    }

    public void setMainController(Controller mainController){
        this.mainController = mainController;
    }

    public void save(){
        Memory.saveReceiver(mail);
        mainController.updateReceiverList();
        stage.close();
    }


}

MAIN WINDOW

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{

        FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml"));
        Parent root = loader.load();
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.setResizable(false);
        primaryStage.show();
    }

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

SECONDARY WINDOW

public class PersonInputForm {

    public PersonInputForm(ActionEvent event) {

        Stage stage = new Stage();

        FXMLLoader mainFxmlLoader = new FXMLLoader(getClass().getResource("../sample.fxml"));
        try {
            mainFxmlLoader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }
        Controller mainController = mainFxmlLoader.getController();


        FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("PersonInputForm.fxml"));
        Parent root = null;
        try {
            root = (Parent)fxmlLoader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }

        stage.setScene(new Scene(root));
        stage.initOwner(
                ((Node) (event.getSource())).getScene().getWindow() );

        PersonInputFormController controller = fxmlLoader.<PersonInputFormController>getController();
        controller.setStage();
        controller.setMainController(mainController);

        stage.show();

    }
}

MEMORY

public class Memory {

    public static sample.Person sender = new sample.Person();
    public static ArrayList<sample.Person> receiverList = new ArrayList();

    static public void saveReceiver(TextField mail){
        Person receiver = new Person();
        receiver.setMail(mail.getText());
        receiverList.add(receiver);
    }

}

OBJECT

public class Person {

    private SimpleStringProperty mail = new SimpleStringProperty();

    public String getMail() {
        return mail.get();
    }

    public SimpleStringProperty mailProperty() {
        return mail;
    }

    public void setMail(String mail) {
        this.mail.set(mail);
    }

}

I found the following similar sounding topics:

How to set items of a TableView in a different controller class in JavaFX?

Javafx Update a tableview from another FXML

But, I still don't understand how my problem can be solved.

Thanks in advance!

Mr. T
  • 35
  • 7
  • @kleopatra thants, for your comment. i updated the source code. – Mr. T Mar 30 '19 at 11:38
  • 1
    neither _C_ (don't even see a tableView in your snippets, also the fxml is missing ;) nor _M_ (no need for all the unrelated ui fields, a single textfield and a button to change the data is enough) - the former makes it not _V_, the latter reduces the willingness to help your debugging :) Best to start over again (as suggested in the referenced help page): write a mimimalistic example just for the sake of reproducing the problem .. chances are, that you'll find the reason yourself, if not, chances are better that one of us finds it :) – kleopatra Mar 30 '19 at 12:02
  • You mean I should create new project? – Mr. T Mar 30 '19 at 12:14
  • Yes, create an example, which just shows your issue, nothing else. – Samuel Philipp Mar 30 '19 at 13:01
  • ok, I have created a small project with a minimal example, I hope this example provides enough information – Mr. T Mar 30 '19 at 14:22
  • thanks :-) hmm.. are you loading sample.fxml twice? if so the table you are seeing is different from the table you are updating - sry for not testing, on mobile in the first spring sun :-) – kleopatra Mar 30 '19 at 15:31
  • thanks for your response, can you suggest the solution? maybe also a better way to pass objects from first controller to the second one? – Mr. T Mar 30 '19 at 15:47
  • Can someone help me please? – Mr. T Mar 30 '19 at 21:45

1 Answers1

0

In your PersonInputForm class' constructor you create an FXMLLoader to load sample.fxml. You then give the controller resulting from this load—a Controller instance—to your PersonInputFormController instance you generate later on. The problem with this is the Controller instance that invoked new PersonInputForm(event) is not the same instance you give to the PersonInputFormController. Remember, each time you call FXMLLoader.load you create a new scene graph connected to a new controller instance.

As you are calling new PersonInputForm(event) from within an instance of Controller, and it's this instance of Controller you need to call updateReceiverList() on, the easiest fix is to pass this to PersonInputForm as well. Then remove the code responsible for loading sample.fxml.

public PersonInputForm(ActionEvent event, Controller mainController) {
    // code...
    controller.setMainController(mainController);
}

Note: It feels wrong to have all this behavior in a constructor.

While the above will solve your problem (I believe), having to clear-and-replace the items of a TableView is not the ideal approach. Take a look at updateReceiverList():

public void updateReceiverList(){
    // Copies Memory.receiverList into new ObservableList
    final ObservableList<Person> data = FXCollections.observableArrayList(Memory.receiverList);
    tableView.getItems().clear();
    // Copies data into the TableView's items list
    tableView.getItems().addAll(data);
}

That code copies a collection into another collection twice. This can become quite expensive when you have a large number of elements. It would be better if you had an observable model that your different controllers could observe and react to.

For instance, you're using an ArrayList to store/share your Persons where you could be using an ObservableList. You could then use tableView.setItems(Memory.receiverList) and any updates your PersonInputFormController makes to the receiverList will be automatically seen by the TableView. As you don't appear to be using multi-threading, this setup shouldn't cause any problems. When, or if, you start utilizing background threads, remember that you must never update the UI—directly or indirectly—from a background thread.

However, using global state (i.e. public static variables) is not ideal either. I recommend reading up on different application architectures such as: Model-View-Controller (MVC), Model-View-Presenter (MVP), Model-View-ViewModel (MVVP), etc. Then try to apply one of them to your application as appropriate.


Some links:


I'd like to point out one more thing about your code. You're using a lot of raw types. Don't use raw types. For instance, you should have:

@FXML private TableView<Person> tableView;

And:

TableColumn<Person, String> mailColumn = new TableColumn<>("E-Mail");
// which lets you use the diamond operator here
mailColumn.setCellValueFactory(new PropertyValueFactory<>("mail"));

And:

// You're missing the <> on the right hand side
public static ArrayList<Person> receiverList = new ArrayList<>();
Slaw
  • 37,820
  • 8
  • 53
  • 80
  • Dear @Slaw, thank you very much vor your answer. It is great, and, as I see the solution, I have to admit that my quastion was actually quite trivial. But this is just a hobby project by which I hope to understand the way this programming language works. Thanks a lot! But I'have some questions concerning your answer. These are: 1. What is the advantage of avoiding raw types? 2. What is the difference between my architecture and MVC. I would say that my Model is the static Memory-class? I don't exactly understand the difference between the classical MVC and my usage of the Memory-class. – Mr. T Mar 31 '19 at 12:05
  • 3. What do you mean by: "As you don't appear to be using multi-threading, this setup shouldn't cause any problems. When, or if, you start utilizing background threads, remember that you must never update the UI—directly or indirectly—from a background thread." Why does it make a difference and what do you mean by "remember that you must never update the UI—directly or indirectly—from a background thread" – Mr. T Mar 31 '19 at 12:07
  • And again thanks a lot for your answer as it contains a lot of very interesting and important information – Mr. T Mar 31 '19 at 12:08
  • (1) The link I provided gives good reasons for avoiding raw types. Generics were added to give compile-time type safety. Using raw types removes that type safety. (2) There's not a great difference; you have a model, view, and controller. It really has to do with how you implemented it. Specifically, it's the global state that gives me pause. As your model becomes more complicated it'll be easier to maintain if you encapsulate the data. (3) JavaFX, like most UI frameworks, is single-threaded. There is a special thread which, in JavaFX, is called the _JavaFX Application Thread_. It's (cont.) – Slaw Mar 31 '19 at 19:22
  • ... this thread that **must** be the only thread to access or modify a live scene graph. This is documented by `Node`: "_Node objects may be constructed and modified on any thread as long they are not yet attached to a Scene in a Window that is showing. An application must attach nodes to such a Scene or modify them on the JavaFX Application Thread._". This has to do with memory visibility/consistency; as JavaFX is not thread safe, non-synchronized access can lead to undefined behavior—if not just an exception (some JavaFX code checks what thread it's on). And I mention "indirectly" (cont.) – Slaw Mar 31 '19 at 19:26
  • ... because with the setup I proposed, you could modify the `receiverList` from a background thread, which would cause a change event to be fired, which would cause the `TableView` to be modified by said background thread. The point is, you need to make sure updates to the UI only ever happen on the _JavaFX Application Thread_. To do this you need to set up points of inter-thread communication. Check out `Platform.runLater`. – Slaw Mar 31 '19 at 19:31
  • Dear @Slaw thank you for a very detailed explaination and for the time you have invested to help me!!!!! – Mr. T Apr 01 '19 at 05:38