1

I'd like to create 2 independent windows at once. One window would be able to hold an observable list, the other one would show the selected listobject's properties. I'm trying to create the listview as a generic list, and combine it with an object specific window (eg. customer properties, beer properties, store properties).

In short: if the user clicks 'Customers', it shows the listview with all the customers, and the first customer's properties are shown in a seperate, customer-specific window.

If the user clicks 'Stores', it shows the same listview, but instead filled with stores. The store-specific window is also opened and contains the first store's properties.

I tried using 2 FXMLLoaders, but for some reason I can't figure out how to use them. I'm pretty mediocre at JavaFX so I can't even figure out where to start. This is what I've got, but it just seems wrong.

 FXMLLoader loader = new FXMLLoader(getClass().getResource("List.fxml"));
 loader.setRoot(this);
 loader.setController(this);
 FXMLLoader loader2 = new FXMLLoader(getClass().getResource("StoreWindow.fxml"));
 loader2.setRoot(this);
 loader2.setController(this);
 try {
        loader.load();
        loader2.load(); 
     } catch (IOException ex) {
        throw new RuntimeException(ex);
     }
iCV
  • 547
  • 1
  • 8
  • 29
  • 2
    Sharing the same controller between two different FXML files is bad practice. You should create a model object that you share between two controllers. Each controller would observe the model and react when changes are made to it (either by the other controller or by some other code). – Slaw Apr 13 '19 at 20:16

2 Answers2

4

You basically have to follow @Slaw instructions. Create a Model. Share the Model between the two Controllers. Observe the model's current Customer and react accordingly. MCVE Below:

Main Class: (load both stages with the correct Scene. Create model and pass it to both Controllers):

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

/**
 *
 * @author sedri
 */
public class JavaFXApplication36 extends Application {

    @Override
    public void start(Stage stage) {
        try {
            FXMLLoader listViewFXMLLoader = new FXMLLoader(getClass().getResource("ListViewFXML.fxml"));
            Parent listViewRoot = listViewFXMLLoader.load();
            ListViewController listViewController = listViewFXMLLoader.getController();
            Scene scene1 = new Scene(listViewRoot);
            stage.setScene(scene1);           

            FXMLLoader detailsFXMLLoader = new FXMLLoader(getClass().getResource("DetailsFXML.fxml"));
            Parent detailsRoot = detailsFXMLLoader.load();
            DetailsController detailsController = detailsFXMLLoader.getController();
            Scene scene2 = new Scene(detailsRoot);
            Stage stage2 = new Stage();
            stage2.setScene(scene2);

            DataModel model = new DataModel();
            listViewController.initModel(model);
            detailsController.initModel(model);

            stage.show();
            stage2.show(); 
        } catch (IOException ex) {
            Logger.getLogger(JavaFXApplication36.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

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

}

Model Class: (Keep up with current Customer and ObservableList of Customer)

import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

/**
 *
 * @author sedrick
 */
public class DataModel {
    private final ObservableList<Customer> customerList = FXCollections.observableArrayList(customer -> new Observable[]{customer.nameProperty(), customer.ageProperty()});
    private final ObjectProperty<Customer> currentCustomer = new SimpleObjectProperty();

    public ObjectProperty<Customer> currentCustomerProperty() {
        return currentCustomer;
    }

    public void setCurrentCustomer(Customer currentCustomer) {
        this.currentCustomer.set(currentCustomer);
    }

    public Customer getCurrentCustomer() {
        return this.currentCustomer.get();
    }  


    public ObservableList<Customer> loadCustomers()
    {        
        customerList.add(new Customer("John Doe", 21));
        customerList.add(new Customer("Jane Joe", 20));

        return customerList;
    }
}

Customer Class:

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/**
 *
 * @author sedrick
 */
public class Customer {
    private final StringProperty name = new SimpleStringProperty();
    private final IntegerProperty age = new SimpleIntegerProperty();
    public Customer(String name, int age) {
        this.name.set(name);
        this.age.set(age);
    }

    public String getName()
    {
       return this.name.get();
    }

    public void setName(String name)
    {
        this.name.set(name);
    }

    public StringProperty nameProperty()
    {
        return this.name;
    }

    public int getAge()
    {
       return this.age.get();
    }

    public void setAge(int age)
    {
        this.age.set(age);
    }

    public IntegerProperty ageProperty()
    {
        return this.age;
    }
}

ListView Controller: (Initialize model, setup ListView and observe current customer property)

import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;

/**
 *
 * @author sedri
 */
public class ListViewController implements Initializable {
   @FXML private ListView<Customer> listView;

   private DataModel model;



    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // TODO
    }    

    public void initModel(DataModel model)
    {
        // ensure model is only set once:
        if (this.model != null) {
            throw new IllegalStateException("Model can only be initialized once");
        }

        listView.getSelectionModel().selectedItemProperty().addListener((obs, oldCustomer, newCustomer) -> 
            model.setCurrentCustomer(newCustomer));

        model.currentCustomerProperty().addListener((obs, oldCustomer, newCustomer) -> {
            if (newCustomer == null) {
                listView.getSelectionModel().clearSelection();
            } else {
                listView.getSelectionModel().select(newCustomer);
            }
        });

        listView.setCellFactory(lv -> new ListCell<Customer>() {
            @Override
            public void updateItem(Customer customer, boolean empty) {
                super.updateItem(customer, empty);
                if (empty) {
                    setText(null);
                } else {
                    setText("Name: " + customer.getName() +  "  Age: " + customer.getAge());
                }
            }
        });

        listView.setItems(model.loadCustomers());
    }
}

ListView FXML:

<StackPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="javafxapplication36.ListViewController">
   <children>
      <ListView fx:id="listView" prefHeight="200.0" prefWidth="200.0" />
   </children>
</StackPane>

DetailsController:(Initialize model and observe current customer property)

import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TextField;

/**
 * FXML Controller class
 *
 * @author sedri
 */
public class DetailsController implements Initializable {
    @FXML TextField tfName, tfAge;

    private DataModel model;
    /**
     * Initializes the controller class.
     */
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // TODO
    }    

     public void initModel(DataModel model) {
        // ensure model is only set once:
        if (this.model != null) {
            throw new IllegalStateException("Model can only be initialized once");
        }

        this.model = model ;

        model.currentCustomerProperty().addListener((observable, oldCustomer, newCustomer) -> {
            if(newCustomer == null){
                tfName.setText("");
                tfAge.setText("");
            }
            else{
                tfName.setText(newCustomer.getName());
                tfAge.setText(Integer.toString(newCustomer.getAge()));
            }
        });
    }
}

Details FXML:

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

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.VBox?>


<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="javafxapplication36.DetailsController">
   <children>
      <Label text="Name" />
      <TextField fx:id="tfName" />
      <Label text="Age" />
      <TextField fx:id="tfAge" />
   </children>
   <padding>
      <Insets left="20.0" right="20.0" />
   </padding>
</VBox>

More info:

@James D answer on Model-View-Controller(MVC).
GitHub code.

SedJ601
  • 12,173
  • 3
  • 41
  • 59
3

The following is a demonstration of two windows (stages) sharing the same model.
The demonstration is kept as simple as possible: one window displays a list. The second window dynamically displays the items that where selected on the first:

enter image description here

The shared model holds the information that the two windows need. Basically a list of items, and a list of selected items:

package two_windows;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

public class Model {

    private final ObservableList<String> list;
    private ObservableList<String> selected;

    Model(){
        list = FXCollections.observableArrayList();
    }

    void addMessage(String msg){
        list.add(msg);
    }

    ObservableList<String> getMessages(){
        return list;
    }

    ObservableList<String> getSelectedMessages(){
        return selected;
    }

    void setSelected(ObservableList<String> selected) {
        this.selected = selected;
    }
}

The content of the first window is defined by List.fxml and its controller:

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

<?import javafx.scene.layout.Pane?>
<?import javafx.scene.control.ListView?>

<Pane xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1"
 fx:controller="two_windows.ListController">
   <children>
        <ListView fx:id="list" prefHeight="300.0" prefWidth="150.0" />
    </children>
</Pane>

The controller accepts a Model, sets the selected items list in the model, listens and responds to model changes:

package two_windows;

import java.util.List;
import javafx.collections.ListChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;

public class ListController {

    @FXML ListView<String> list;

    void setModel(Model model) {

        list.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);//allow multiple selection

        //sets the selected items of the list to the model 
        model.setSelected(list.getSelectionModel().getSelectedItems());

        //listen to changes in model, and respond
        model.getMessages().addListener(
                                        (ListChangeListener<String>) c -> {
                                            c.next();
                                            addElements(c.getAddedSubList());
                                        }
                                    );
    }

    private void addElements(List<? extends String> msgList){

        for(String msg : msgList){
            list.getItems().add(msg);
        }
    }
}

The content of the second window is very similar to the first, and is defined by Selected.fxml:

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

<?import javafx.scene.layout.Pane?>
<?import javafx.scene.control.ListView?>  

<Pane xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1"
 fx:controller="two_windows.SelectedController">
   <children>
        <ListView fx:id="selected" prefHeight="300.0" prefWidth="150.0" />
    </children>
</Pane>

And its controller, which like the other controller accepts a Model and responds to changes in it:

package two_windows;

import java.util.List;
import javafx.collections.ListChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;

public class SelectedController {

    @FXML ListView<String> selected;

    void setModel(Model model) {

        //listen to changes in model, and respond
        model.getSelectedMessages().addListener(
                    (ListChangeListener<String>) c -> {
                        c.next();
                        removeElements(c.getRemoved());
                        addElements(c.getAddedSubList());
                    }
                );
    }

    private void removeElements(List<? extends String> msgList){

        for(String msg : msgList){
            selected.getItems().remove(msg);
        }
    }

    private void addElements(List<? extends String> msgList){

        for(String msg : msgList){
            selected.getItems().add(msg);
        }
    }
}

Putting it all together and testing:

package two_windows;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class TwoWindows extends Application {

    private Model model;

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

        model = new Model();

        FXMLLoader listLoader = new FXMLLoader(getClass().getResource("List.fxml"));
        Parent list = listLoader.load();
        ListController listController = listLoader.getController();
        listController.setModel(model);

        FXMLLoader selectedLoader = new FXMLLoader(getClass().getResource("Selected.fxml"));
        Parent selected = selectedLoader.load();
        SelectedController selectedController = selectedLoader.getController();
        selectedController.setModel(model);

        primaryStage.setScene(new Scene(list));
        primaryStage.setX(350); primaryStage.setY(300);

        Stage secondaryStage = new Stage();
        secondaryStage.setScene(new Scene(selected));
        secondaryStage.setX(550); secondaryStage.setY(300);

        addMessages();
        primaryStage.show();
        secondaryStage.show();
    }

    private void addMessages() {

        int counter = 0;
        while(counter < 15) {
            model.addMessage("message number "+ counter++);
        }
    }

    public static void main(final String[] args) {
        launch(args);
    }
}
c0der
  • 18,467
  • 6
  • 33
  • 65