Communication between two JavaFx controllers
Asked Answered
T

4

12

I made a structure to of Controllers and Views (fxml) to separate my code as much as I could, and I'm wondering how to communicate between 2 controllers. I mean, a controller have to call some functions of another controller to set it up to date.

I think a schema of my current structure will be more explicit:

          Controller 1
           /              \
   fx:include    fx:include
       /                     \
Controller2      Controller3

Each controller has is own fxml view.
- Controller 1 : a container controller which has a TabPane element with 2 tabs (each tab correspond to 1 controller)
- Controller 2 : a list
- Controller 3 : a form

You've probably guessed that I want my form (controller 3) to automatically update my list (controller 2). For the moment, the form is only a "creation form", so I just want to add row in my list.

I've already tried to get my Controller 2 with FXMLoader and call the functions to relaod my tableView, no success..

Controller 1 (.java + .fxml) :

package pappu.controllers;

import pappu.core.controller.AbstractController;

public class FolderController extends AbstractController
{

}


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

<VBox fx:id="view" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="pappu.controllers.FolderController">
  <TabPane>
    <tabs>
      <Tab text="RECHERCHE">
        <content>
          <AnchorPane id="Content">
            <children>
                <fx:include source="FolderList.fxml" />  
            </children>
          </AnchorPane>
        </content>
      </Tab>
      <Tab text="DOSSIER">
        <content>
          <AnchorPane id="Content">
            <children>
                <fx:include source="FolderFormAdd.fxml" />  
            </children>
          </AnchorPane>
        </content>
      </Tab>
    </tabs>
  </TabPane>
</VBox>

Controller 2 (.java + .fxml) :

package pappu.controllers;

import java.net.URL;
import java.util.Date;
import java.util.List;
import java.util.ResourceBundle;

import org.hibernate.Session;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellDataFeatures;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.util.Callback;
import pappu.core.controller.AbstractController;
import pappu.entities.Folder;

public class FolderListController extends AbstractController implements Initializable
{
    /**
     * TableView object
     */
    @FXML private TableView<Folder> foldersTableView;

    /**
     * FolderNumber column object
     */
    @FXML private TableColumn<Folder, String> colFolderNumber;

    /**
     * Person column object
     */
    @FXML private TableColumn<Folder, String> colPerson;

    /**
     * Birthday date column object
     */
    @FXML private TableColumn<Folder, Date> colBirthdayDate;

    /**
     * List of folders
     */
    private static List<Folder> foldersList;

    /**
     * Constructor
     * Will make a call to initializeFoldersList()
     */
    public FolderListController()
    {
        initializeFoldersList();
    }


    /**
     * Initialize implementation of the Initializable interface
     * 
     * @param location
     * @param resources
     */
    @Override 
    public void initialize(URL location, ResourceBundle resources) 
    {
        initializeTableColumns();
        loadData();
    }

    /**
     * Query the database to retrieve the folder list
     */
    @SuppressWarnings("unchecked") 
    public void initializeFoldersList()
    {
        Session session = sessionFactory.getCurrentSession();
        session.beginTransaction();
        foldersList = session.createQuery("from Folder").list();
        session.close();
    }

    /**
     * Initialize columns binding to folders properties
     */
    public void initializeTableColumns()
    {
        colFolderNumber.setCellValueFactory(
                  new PropertyValueFactory<Folder,String>("folderNumber")
                      );
        colPerson.setCellValueFactory(
                new Callback<CellDataFeatures<Folder, String>, ObservableValue<String>>() {
                     public ObservableValue<String> call(CellDataFeatures<Folder, String> p) {
                         return new SimpleStringProperty(p.getValue().getFirstName() + " " + p.getValue().getLastName());
                     }}
          );
        colBirthdayDate.setCellValueFactory(
                  new PropertyValueFactory<Folder,Date>("birthdayDate")
                      );

    }

    /**
     * Put the folders list in the TableView object
     */
    public void loadData()
    {   
        ObservableList<Folder> listFold = FXCollections.observableArrayList(foldersList);       
        foldersTableView.setItems(listFold);
    }   
}


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

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


<VBox fx:id="view" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="pappu.controllers.FolderListController">
    <Label fx:id="lblTest"></Label>
    <TableView fx:id="foldersTableView">
        <columns>
            <TableColumn prefWidth="75.0" text="N°" fx:id="colFolderNumber">
            </TableColumn>
            <TableColumn prefWidth="75.0" text="Personne" fx:id="colPerson">
            </TableColumn>
            <TableColumn prefWidth="75.0" text="Date de naissance" fx:id="colBirthdayDate">
            </TableColumn>
        </columns>
    </TableView>
</VBox>

Controller 3 (.java + .fxml) :

package pappu.controllers;

import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;

import org.hibernate.Session;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.TextField;
import javafx.scene.layout.Pane;
import pappu.core.AppFactory;
import pappu.core.controller.AbstractController;
import pappu.entities.Folder;
import pappu.entities.Gender;

public class FolderFormAddController extends AbstractController
{   
    @FXML TextField folderNumber;
    @FXML TextField firstName;
    @FXML TextField lastName;
    public void submitForm() throws IOException
    {   
        Session session = sessionFactory.getCurrentSession();
        session.beginTransaction();

        Folder folder = new Folder();

        folder.setFolderNumber(folderNumber.getText());
        folder.setFirstName(firstName.getText());
        folder.setLastName(lastName.getText());
        folder.setGender(Gender.m);

        session.save(folder);
        session.getTransaction().commit();
            // This doesn't work.. even tried with a simple Label
        AppFactory app = new AppFactory();
        FolderListController flc = app.folderListController();
        flc.initializeFoldersList();
        flc.loadData();
    }
}


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

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

<VBox fx:id="view" prefHeight="216.0" prefWidth="421.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="pappu.controllers.FolderFormAddController">
  <children>
    <Label prefHeight="26.0" prefWidth="102.0" text="Numéro de dossier" />
    <TextField prefWidth="200.0" fx:id="folderNumber"/>
    <Label text="Prénom" />
    <TextField prefWidth="200.0" fx:id="firstName"/>
    <Label text="Nom" />
    <TextField prefWidth="200.0" fx:id="lastName"/>
    <Button mnemonicParsing="false" onAction="#submitForm" text="Enregistrer" />
  </children>
</VBox>

Precisions:
I made my application on this base: http://www.zenjava.com/2011/10/25/views-within-views-controllers-within-controllers/ and I use JavaFX 2 on Java JDK 7

I feel something missing in global functioning of a JavaFX application.

Treasonable answered 4/3, 2014 at 17:14 Comment(2)
possible duplicate of Passing Parameters JavaFX FXMLRecoverable
You can use the DataFX Controller Framework and inject the same datamodel in both controllers: guigarage.com/2013/12/datafx-controller-framework-previewDanika
H
9

Two ways come into my mind:

  1. Based on the "Nested Controllers" section of "Introduction to FXML" (link), you could inject the children controllers (2 & 3) into the parent (1) and have the parent coordinate their interactions:

    FXML (1):

    <fx:include source="FolderList.fxml" fx:id="list" />
    ...
    <fx:include source="FolderFormAdd.fxml" fx:id="addForm" />
    

    Java (1) (beware the names of the fields; must match <fx:id>Controller, i.e.):

    public class FolderController extends AbstractController {
        @FXML private FolderListController listController;
        @FXML private FolderFormAddController addFormController;
        void initialize() {
            // add code to coordinate them
        }
    }
    

    I do not like this solution much for this case, as it leads to strong coupling between the components. On the other hand, it may be the quickest.

  2. Use an event bus (e.g. from Google Guava). This can actually decouple your logic (e.g. the list component listens to the PersonAdded event, no matter how it was created; the form generates this event, without caring who is listening - if any). I think I would prefer this solution in your case. The event bus can optionally be retrieved using dependency injection.

Check out the answer pointed by the comment from jewelsea, it is great - I have already upvoted it myself :)

Hallux answered 4/3, 2014 at 18:26 Comment(3)
Thanks you I've used EventBus API eventbus.org which is really simple to use and to implement !Treasonable
@NikosParaskevopoulos, what if I have loaded one of the above sub fxml documents multiple times.?Lucianaluciano
You need to define the fx:id for each fx:include element for the controller injection to work. The fx:ids cannot be the same anyway, so things still work.Hallux
P
10

Nikos makes a good point (Software Engineering principle) about coupling. There is one way though, to accomplish the "spirit" of the first (simple) approach and not encroach upon this principle by using the Mediator pattern. As taken from wikipedia (which is referencing the GoF):

"The essence of the Mediator Pattern is to "define an object that encapsulates how a set of objects interact". It promotes loose coupling by keeping objects from referring to each other explicitly, and it allows their interaction to be varied independently. Client classes can use the mediator to send messages to other clients, and can receive messages from other clients via an event on the mediator class."

Here you can consider your Controllers as the clients. What you need to have happen then is use the mediator to mediate "conversations" between each other.

First create a mediator interface:

public interface IMediateControllers {
    void registerController2(Controller2 controller);
    void registerController3(Controller3 controller);
    void controller2DoSomething();
    void controller3OperateOn(String data);
}

And then a concrete mediator (as a Singleton)

public class ControllerMediator implements IMediateControllers {
    private Controller2 controller2;
    private Controller3 controller3;

    @Override
    void registerController2(Controller2 controller) {
        controller2 = controller;
    }

    @Override
    void registerController3(Controller3 controller) {
        controller3 = controller;
    }

    @Override
    void controller2DoSomething() {
         controller2.doSomething();
    }

    void controller3OperateOn(String data) {
        controller3.operateOn(data);
    }

    /**
     * Everything below here is in support of Singleton pattern
     */
    private ControllerMediator() {}

    public static ControllerMediator getInstance() {
        return ControllerMediatorHolder.INSTANCE;
    }

    private static class ControllerMediatorHolder {
        private static final ControllerMediator INSTANCE = new ControllerMediator();
    }
}

Now, since Controller1 has Controller2 and Controller3 injected (as noted in the fxml file), you could do the following in Controller1::initialize() method:

@Override
public void initialize(Url url, ResourceBundle resource) {
    ControllerMediator.getInstance().registerController2(controller2Controller);
    ControllerMediator.getInstance().registerController3(controller3Controller);
 }

Now, anywhere you need Controller2 to communicate with Controller3, you simple use the mediator:

// ... somewhere in Controller2
ControllerMediator.getInstance().controller3OperateOn("my data");

and Controller 3 can communicate back to Controller2 using the same mediator:

// ... somewhere in Controller3
ControllerMediator.getInstance().controller2DoSomething();

Of course, this relies on Controller2 having implemented the doSomething() operation and Controller3 having implemented the operateOn(String data) operation.

The important thing is that you've decoupled Controller2 and Controller3 (they don't know about each other). I just used this pattern in a little project I'm working on right now (inspired by Nikos' first solution, but thinking immediately of the Mediator pattern to remove the coupling he (properly) griped about.

Polyester answered 12/2, 2015 at 19:52 Comment(0)
H
9

Two ways come into my mind:

  1. Based on the "Nested Controllers" section of "Introduction to FXML" (link), you could inject the children controllers (2 & 3) into the parent (1) and have the parent coordinate their interactions:

    FXML (1):

    <fx:include source="FolderList.fxml" fx:id="list" />
    ...
    <fx:include source="FolderFormAdd.fxml" fx:id="addForm" />
    

    Java (1) (beware the names of the fields; must match <fx:id>Controller, i.e.):

    public class FolderController extends AbstractController {
        @FXML private FolderListController listController;
        @FXML private FolderFormAddController addFormController;
        void initialize() {
            // add code to coordinate them
        }
    }
    

    I do not like this solution much for this case, as it leads to strong coupling between the components. On the other hand, it may be the quickest.

  2. Use an event bus (e.g. from Google Guava). This can actually decouple your logic (e.g. the list component listens to the PersonAdded event, no matter how it was created; the form generates this event, without caring who is listening - if any). I think I would prefer this solution in your case. The event bus can optionally be retrieved using dependency injection.

Check out the answer pointed by the comment from jewelsea, it is great - I have already upvoted it myself :)

Hallux answered 4/3, 2014 at 18:26 Comment(3)
Thanks you I've used EventBus API eventbus.org which is really simple to use and to implement !Treasonable
@NikosParaskevopoulos, what if I have loaded one of the above sub fxml documents multiple times.?Lucianaluciano
You need to define the fx:id for each fx:include element for the controller injection to work. The fx:ids cannot be the same anyway, so things still work.Hallux
S
0

I figured out an easy to implement solution for programmers who are confused on how to pass fx:controller="Controller" from their FXML files into the Main Class, and/or Controllers. To allow for an object reference between these Classes.

From Main.java -> start method: (With fx:controller="Controller") in FXML File

    FXMLLoader loader = new FXMLLoader();
    Parent root = loader.load(getClass().getResource("Window1.fxml").openStream());
    Controller controller = loader.getController();
    controller.setReferenceToController(controller);

The last line [controller.setReferenceToController(controller);] passes the object "Controller" loaded from the FXML file to itself.

In the Controller Class:

    private Controller controller;
    private DataProcess data;

    public void setReferenceToController(Controller controller){
    this.controller = controller;
    data = new DataProcess(controller);
}

Now each time you open or start a new "Window" or create a separate class(such as DataProcess) just pass the controller object references between them. This will allow full communication between controllers, FXML, and Classes.

Example of methods:

    public DataProcess(Controller controller) {
            this.controller = controller;
        }

    //Call this method from Controller
    public void handleSaveClick(){
        if(file != null){

        //save a bunch of data
        controller.setSaveStatus(true);

        }
        else
        controller.setSaveStatus(false);

Hope this Helps.

Simmons answered 4/1, 2018 at 19:36 Comment(0)
D
0

so this is my first time answering a question. I am really new to programming. So what I discovered was you can only communicate with a controller only if you have the specific FXMLLoader you used to load that specific controller.

Even if you create a new FXMLLoader with the resource it's of no use. You need the exact one you used to load that specific scene.

What I did was whenever I loaded a controller I go and store the FXMLLoader used in a method. And when I need to access that controller I get it from that method.

MyController controller = MyMethod.getMyControllerFXMLLoader().getController; controller.methodYouNeedToAccess();

getMyControllerFXMLLoader() is the method I created so that it returns that specific fxmlloader.MyMethod is the class I specifically created to do this.

Also when that controller is closed the FXMLLoader value for that controller is set to become null.

This works really well. Whether it abides by the rules and regulations I don't really know.

Deherrera answered 22/5, 2023 at 7:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.