javajavafxfxml

Embedding fxml in fxml: Access to the parent controller and switch between fxmls


I have an application, where the ParentView consists of a horizontal SplitPane with two buttons at the left side to choose a ChildView, which is embedded at the right side.

With the two ParentView buttons I want to choose between two ChildViews "subView1" and "subView2". Each ChildView has a button, which shall change the text of a label in the ParentView.

I'm looking for a way to embed a ChildView fxml, by clicking a button. I've already tried to use <fx:include fx:id="childView" source=sub-view-1.fxml/>. But this only enables to embed the chosen fxml right from the beginning and I can't change the ChildView.

Further I'm looking for a way to gain access from the ChildViewController to ParentView elements, in this case the ParentView label.

My example code is:

The ParentView .fxml (with the fx:include method)

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

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/17.0.12" fx:controller="de.demo.ParentViewController">
    <padding>
        <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
    </padding>
   <SplitPane dividerPositions="0.5" prefHeight="160.0" prefWidth="200.0">
     <items>
       <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0">
            <children>
                <Button layoutX="24.0" layoutY="21.0" onAction="#openSubView1" text="Subview1" />
            
                <Label fx:id="welcomeText" layoutX="46.0" layoutY="71.0" />
               <Button layoutX="21.0" layoutY="111.0" mnemonicParsing="false" onAction="#openSubView2" text="SubView2" />
            </children>
         </AnchorPane>
         <fx:include fx:id="subViewPlaceholder" source="sub-view-1.fxml" />
     </items>
   </SplitPane>
</VBox>

ParentView Controller

package de.demo

import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class ParentViewController {


    @FXML
    protected Label welcomeText;

    @FXML
    protected void openSubView1() {
        //here should be the code to open the subview1.fxml
    }

    @FXML
    protected void openSubView2() {
        //here should be the code to open the subview2.fxml
    }
}

ChildView fxmls

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


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

<VBox xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="de.demo.SubView1Controller"
            prefHeight="400.0" prefWidth="600.0" spacing="20.0" alignment="CENTER">
    <Label fx:id="subView1Label" text="This is SubView 1"/>
    <Button text="Change Main View Text" onAction="#changeMainViewText"/>

</VBox>

ChildView Controller

package de.demo;

import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class SubView1Controller{

    @FXML
    private Label subView1Label;

    @FXML
    private void changeMainViewText() {
        subView1Label.setText("Sub View 1 Button Clicked!");
// Code to access the label of the ParentView
    }
}

Solution

  • The easiest way to do this from your current setup is to place a container in the right side of the split pane. Then just fill the container with the appropriate FXML in the button handlers. Here I use a BorderPane, which is probably the most convenient (as you can just make calls to setCenter(...) to replace any current content with new content).

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.geometry.*?>
    <?import javafx.scene.control.*?>
    <?import javafx.scene.layout.*?>
    
    <VBox alignment="CENTER" spacing="20.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/17.0.12" fx:controller="de.demo.ParentViewController">
        <padding>
            <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
        </padding>
       <SplitPane dividerPositions="0.5" prefHeight="160.0" prefWidth="200.0">
         <items>
           <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0">
                <children>
                    <Button layoutX="24.0" layoutY="21.0" onAction="#openSubView1" text="Subview1" />
                
                    <Label fx:id="welcomeText" layoutX="46.0" layoutY="71.0" />
                   <Button layoutX="21.0" layoutY="111.0" mnemonicParsing="false" onAction="#openSubView2" text="SubView2" />
                </children>
             </AnchorPane>
             <BorderPane fx:id="subViewContainer" />
         </items>
       </SplitPane>
    </VBox>
    

    and

    package de.demo
    
    import java.io.IOException;
    
    import javafx.fxml.FXML;
    import javafx.scene.control.Label;
    import javafx.scene.layout.BorderPane;
    
    public class ParentViewController {
    
    
        @FXML
        protected Label welcomeText;
    
        @FXML
        private BorderPane subViewContainer;
    
        @FXML
        protected void openSubView1() throws IOException {
            // Assumes subview1.fxml is in the same package as this controller. 
            // Modify the path if this is not the case
            Parent subView1 = FXMLLoader.load(getClass().getResource("subview1.fxml");
            subViewContainer.setCenter(subView1);
        }
    
        @FXML
        protected void openSubView2() throws IOException {
            // Assumes subview2.fxml is in the same package as this controller. 
            // Modify the path if this is not the case
            Parent subView2 = FXMLLoader.load(getClass().getResource("subview2.fxml");
            subViewContainer.setCenter(subView2);
        }
    }
    

    This approach works for this simple case, where you are only changing the content of the "child views" from the parent. If you want to be able to change the views from elsewhere, it's probably better to factor the functionality out into a separate "view model" which stores the state of the current view. Exact details will depend on which flavor of the MV* set of design patterns you favor. I created an example a couple of years ago which may be worth studying if you want to see this kind of approach.

    Further I'm looking for a way to gain access from the ChildViewController to ParentView elements, in this case the ParentView label.

    Don't. You should not modify elements of an FXML from outside the controller of that FXML. Instead, use some form of MVC approach. Modify the model and have the label reflect the property in the model. The example linked above includes this functionality.