2 Replies Latest reply: Mar 11, 2013 2:48 AM by 993317 RSS

    Setting Controller for <fx:include> element

    993317
      Is there a way to set the controller for the <fx:include> element before loading the fxml of the same ?? This is what i am trying but it seems like the FXMLLoader which is loading the <fx:include> element is setting some other controller for the same. Is there a way to access the FXMLLoader loading the <fx:include> and set my own controller before loading the fxml ???

      package fxminclude;
      
      import java.net.URL;
      import java.util.ResourceBundle;
      import javafx.fxml.FXML;
      import javafx.fxml.Initializable;
      import javafx.scene.control.TextField;
      import javafx.scene.layout.HBox;
      
      public class ParentController implements Initializable {
      
          @FXML
          private TextField nameField;
          @FXML
          private TextField valueField;
          @FXML
          private HBox title;
          @FXML
          private TitleController titleController;
          private Model model;
      
          public ParentController(Model model) {
              this.model = model;
              titleController = new TitleController(model);
          }
      
          @Override
          public void initialize(URL location, ResourceBundle resources) {
      
              nameField.setText(model.nameProperty().get());
              model.nameProperty().bind(nameField.textProperty());
      
              valueField.setText(model.valueProperty().get());
              model.valueProperty().bind(valueField.textProperty());
      
              titleController.initialize(location, resources);
      
          }
      }
      package fxminclude;
      
      import java.net.URL;
      import java.util.ResourceBundle;
      import javafx.fxml.FXML;
      import javafx.fxml.Initializable;
      import javafx.scene.control.TextField;
      
      public class TitleController implements Initializable {
      
          @FXML
          private TextField titleField;
          private Model model;
      
          public TitleController(Model model) {
              this.model = model;
          }
      
          @Override
          public void initialize(URL location, ResourceBundle resources) {
              titleField.setText(model.titleProperty().get());           // Getting NPE here
              model.titleProperty().bind(titleField.textProperty());
          }
      }
      <?xml version="1.0" encoding="UTF-8"?>
      
      <?import java.lang.*?>
      <?import java.util.*?>
      <?import javafx.scene.*?>
      <?import javafx.scene.control.*?>
      <?import javafx.scene.layout.*?>
      
      <VBox prefHeight="412.0" prefWidth="352.0" xmlns:fx="http://javafx.com/fxml">
        <children>
          <HBox alignment="CENTER" prefHeight="53.0" prefWidth="352.0">
            <children>
              <Label id="Name" text="Name" />
              <TextField fx:id="nameField" prefWidth="200.0" />
            </children>
          </HBox>
          <HBox alignment="CENTER" prefHeight="47.0" prefWidth="352.0">
            <children>
              <Label text="Value" />
              <TextField fx:id="valueField" prefWidth="200.0" />
            </children>
          </HBox>
          <Pane prefHeight="312.0" prefWidth="352.0">
            <children>
              <fx:include fx:id="title" source="title.fxml" layoutX="42.0" />   //here
            </children>
          </Pane>
        </children>
      </VBox>
      <?xml version="1.0" encoding="UTF-8"?>
      
      <?import java.lang.*?>
      <?import java.util.*?>
      <?import javafx.scene.*?>
      <?import javafx.scene.control.*?>
      <?import javafx.scene.layout.*?>
      
      <HBox alignment="CENTER" prefHeight="38.0" prefWidth="269.0" xmlns:fx="http://javafx.com/fxml">
        <children>
          <Label text="Title" />
          <TextField fx:id="titleField" prefWidth="200.0" />
        </children>
      </HBox>
      package fxminclude;
      
      import java.io.IOException;
      import javafx.fxml.FXMLLoader;
      import javafx.scene.layout.VBox;
      
      public class ParentView {
      
          protected VBox template;
      
          public ParentView(ParentController controller) {
      
              FXMLLoader loader = new FXMLLoader(getClass().getResource("View.fxml"));
              loader.setController(controller);
              try {
                  loader.load();
              } catch (IOException ex) {
                  ex.printStackTrace();
              }
      
              template = loader.getRoot();
          }
      
          public VBox getTemplate() {
              return template;
          }
      }
        • 1. Re: Setting Controller for <fx:include> element
          James_D
          Right now you have a null pointer exception, I think, because your TitleController isn't actually connected in any way to your title.fxml file. The only way I know of to map an included fxml to a controller is to specify the controller in the fxml file:

          title.fxml
          <?xml version="1.0" encoding="UTF-8"?>
           
          <?import java.lang.*?>
          <?import java.util.*?>
          <?import javafx.scene.*?>
          <?import javafx.scene.control.*?>
          <?import javafx.scene.layout.*?>
           
          <HBox alignment="CENTER" prefHeight="38.0" prefWidth="269.0" xmlns:fx="http://javafx.com/fxml" fx:controller="fxminclude.TitleController">
            <children>
              <Label text="Title" />
              <TextField fx:id="titleField" prefWidth="200.0" />
            </children>
          </HBox>
          This will now inject the titleController into your parentController; you need to remove the instantiation of titleController from the constructor of ParentController.

          The problem now, of course, is that the fxml instantiation of TitleController needs a constructor taking no arguments, and doesn't know anything about your model. I've fixed this problem in the past with a somewhat clunky approach, which I'll show you first. It occurs to me right now that there's a better way to do it using controller factories, which I'll also show, but I haven't tested that.

          The approach I've used before is to inject the model "by hand" into the title controller, using a setModel(...) or initiModel(...) method. I try to keep this flexible so that initialize(...) and initModel(...) can be called in any order, and everything that needs to be initialized gets initialized once both have been called. This looks something like this:

          TitleController.java
          package fxminclude;
           
          import java.net.URL;
          import java.util.ResourceBundle;
          import javafx.fxml.FXML;
          import javafx.fxml.Initializable;
          import javafx.scene.control.TextField;
           
          public class TitleController implements Initializable {
           
              @FXML
              private TextField titleField;
              private Model model;
           
              // public TitleController(Model model) {
              //     this.model = model;
              // }
           
              @Override
              public void initialize(URL location, ResourceBundle resources) {
                if (model != null) {
                  doInit();
                }
              }
          
              public void initModel(Model model) {
                if (this.model != null) {
                  throw new IllegalStateException("Model has already been initialized");
                }
                this.model = model ;
                if (titleField != null) {
                 doInit();
                }
              }
          
              private void doInit() {
                  titleField.setText(model.titleProperty().get());           // Getting NPE here
                  model.titleProperty().bind(titleField.textProperty());
          
              }
          }
          If you're not concerned about making sure the model isn't set more than once, just define setModel(...) instead of initModel(...) and don't check for the model already being initialized.

          Now you can just initialize the titleController's model from the parentController's initialize method:

          ParentController.java
          package fxminclude;
           
          import java.net.URL;
          import java.util.ResourceBundle;
          import javafx.fxml.FXML;
          import javafx.fxml.Initializable;
          import javafx.scene.control.TextField;
          import javafx.scene.layout.HBox;
           
          public class ParentController implements Initializable {
           
              @FXML
              private TextField nameField;
              @FXML
              private TextField valueField;
              @FXML
              private HBox title;
              @FXML
              private TitleController titleController;
              private Model model;
           
              public ParentController(Model model) {
                  this.model = model;
                  // titleController = new TitleController(model);
              }
           
              @Override
              public void initialize(URL location, ResourceBundle resources) {
           
                  nameField.setText(model.nameProperty().get());
                  model.nameProperty().bind(nameField.textProperty());
           
                  valueField.setText(model.valueProperty().get());
                  model.valueProperty().bind(valueField.textProperty());
           
                  // titleController.initialize(location, resources);
                 titleController.initModel(model);
              }
          }
          The approach using controller factories may be better, but as I said, I haven't tried it. In this case, I think you just define the controllers in each of your fxml files:

          View.fxml
          <?xml version="1.0" encoding="UTF-8"?>
           
          <?import java.lang.*?>
          <?import java.util.*?>
          <?import javafx.scene.*?>
          <?import javafx.scene.control.*?>
          <?import javafx.scene.layout.*?>
           
          <VBox prefHeight="412.0" prefWidth="352.0" xmlns:fx="http://javafx.com/fxml" fx:controller="fxminclude.ParentController">
            <children>
              <HBox alignment="CENTER" prefHeight="53.0" prefWidth="352.0">
                <children>
                  <Label id="Name" text="Name" />
                  <TextField fx:id="nameField" prefWidth="200.0" />
                </children>
              </HBox>
              <HBox alignment="CENTER" prefHeight="47.0" prefWidth="352.0">
                <children>
                  <Label text="Value" />
                  <TextField fx:id="valueField" prefWidth="200.0" />
                </children>
              </HBox>
              <Pane prefHeight="312.0" prefWidth="352.0">
                <children>
                  <fx:include fx:id="title" source="title.fxml" layoutX="42.0" />   //here
                </children>
              </Pane>
            </children>
          </VBox>
          title.fxml as above.

          Now let ParentController basically be exactly what you want it to be:
          package fxminclude;
           
          import java.net.URL;
          import java.util.ResourceBundle;
          import javafx.fxml.FXML;
          import javafx.fxml.Initializable;
          import javafx.scene.control.TextField;
          import javafx.scene.layout.HBox;
           
          public class ParentController implements Initializable {
           
              @FXML
              private TextField nameField;
              @FXML
              private TextField valueField;
              @FXML
              private HBox title;
              @FXML
              private TitleController titleController;
              private Model model;
           
              public ParentController(Model model) {
                  this.model = model;
                  // titleController = new TitleController(model);
              }
           
              @Override
              public void initialize(URL location, ResourceBundle resources) {
           
                  nameField.setText(model.nameProperty().get());
                  model.nameProperty().bind(nameField.textProperty());
           
                  valueField.setText(model.valueProperty().get());
                  model.valueProperty().bind(valueField.textProperty());
           
                  // titleController.initialize(location, resources);
           
              }
          }
          Similarly for TitleController, so that is exactly as in your post.

          Then you just need to tell the FXMLLoader how to instantiate the controllers (this is the bit I haven't tried). Pass the model (not the controller) to the ParentView and tell the FXMLLoader to use it to instantiate the controllers:
          package fxminclude;
           
          import java.io.IOException;
          import javafx.fxml.FXMLLoader;
          import javafx.scene.layout.VBox;
          import javafx.util.Callback ;
           
          public class ParentView {
           
              protected VBox template;
           
           //   public ParentView(ParentController controller) {
           
               public ParentView(final Model model) {
                  FXMLLoader loader = new FXMLLoader(getClass().getResource("View.fxml"));
                  // loader.setController(controller);
                  loader.setControllerFactory(new Callback<Class<?>, Object>() {
                    @Override
                    public Object call(Class<?> clazz) {
                       if (clazz.equals(ParentController.class)) {
                          return new ParentController(model);
                       } else if (clazz.equals(TitleController.class)) {
                         return new TitleController(model);
                       } else return null ;
                    }
                  });
                  try {
                      loader.load();
                  } catch (IOException ex) {
                      ex.printStackTrace();
                  }
           
                  template = loader.getRoot();
              }
           
              public VBox getTemplate() {
                  return template;
              }
          }
          Edited by: James_D on Mar 7, 2013 8:46 AM
          • 2. Re: Setting Controller for <fx:include> element
            993317
            Thank You James. Your solution works perfectly and it also helped me understand about setControllerFactory and callback of the FXMLLoader. I am going to mark this thread as answered but I have a new question about FXML loading for which i am going to start a new thread.

            Thanks :)