Communication between two JavaFx controllers


Question

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.

1
6
8/6/2017 5:19:14 AM

Accepted Answer

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 :)

7
3/4/2014 6:26:54 PM

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.


Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon