Reputation: 125
I have a main form (MainForm.fxml) that has its controller defined in the fxml file. In this same fxml file I have 2 subforms (Subform1.fxml and Subform2.fxml) that I have included with fx:include. Subform1 has a concrete controller. Subform2 is a general purpose 'select and edit' form with abstract code behind it. I want to display Subform2 with different concrete implementations of the abstract code depending on the context. If I define the controller in the fxml then it will not be general purpose anymore.
I am only using FXMLLoader to load the MainForm, and I can't find anywhere a way of changing the controller for the subforms. I have gone all around the houses trying different things. Any help would be much appreciated.
Updates to my question Thanks to James_D for the help so far. The definition of my Subform1 in the fxml file:
<children>
<!--<fx:include source="Subform1.fxml" />-->
<!-- <Subform1 controller="${ISubform}" /> -->
<Subform1 controller="${Subform1Controller}" />
<!-- <Subform1 /> -->
</children>
I have created an interface as follows:
package testsubforms;
public interface ISubform {
}
And this is my controller:
package testsubforms;
public class Subform1Controller implements ISubform {
public Subform1Controller() {
System.out.println("Inside Subform1Controller");
}
}
The following is my Subform1 class:
package testsubforms;
import java.io.IOException;
import javafx.beans.NamedArg;
import javafx.fxml.FXMLLoader;
import javafx.scene.layout.GridPane;
public class Subform1 extends GridPane {
private ObjectProperty controller;
public ObjectProperty controllerProperty() {
return this.controller;
}
public void setController(Subform1Controller controller) {
this.controllerProperty().set(controller);
}
public Subform1(@NamedArg("controller") Subform1Controller controller) throws IOException {
this.controller = new SimpleObjectProperty(this, "controller", controller);
FXMLLoader loader = new FXMLLoader(getClass().getResource("Subform1.fxml"));
loader.setRoot(this);
loader.setController(controller);
loader.load();
}
public Subform1() throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("Subform1.fxml"));
loader.setRoot(this);
loader.setController(this);
loader.load();
}
}
My current problem is the runtime error "javafx.fxml.LoadException: Cannot bind to untyped object" where I specify Subform1 in the fxml file. Any help to get this final piece in the jigsaw to work would be much appreciated. Once I get this last piece to work I will post the complete example for others to use later.
Upvotes: 1
Views: 1683
Reputation: 209330
One approach would be to specify an interface as the controller class in Subform2.fxml
. For example, define
public interface Subform2Controller {
}
then you can just specify that interface as the controller "class":
<GridPane xmlns="..." fx:controller="my.package.Subform2Controller">
<!-- -->
</GridPane>
Now specify a controller factory that specifically handles that case:
Object subform2Controller = /* any controller implementation you like... */ ;
FXMLLoader loader = new FXMLLoader(getClass().getResource("path/to/main.fxml"));
loader.setControllerFactory(type -> {
try {
if (type == Subform2Controller.class) {
return subform2Controller ;
}
// default implementation:
return type.newInstance();
} catch (Exception exc) {
// this is pretty much fatal...
throw new RuntimeException(exc);
}
});
Parent root = loader.load();
The idea here is that the controller factory is a function that the FXMLLoader
uses to map the declared class in the FXML file to a specific object. (By default it just calls newInstance()
on the Class
that is specified.) When you use FXML includes, the controller factory is propagated down to loading the included files. This implementation just intercepts the specific case where the interface is defined and returns whatever object you dynamically specified in the code.
As far as I know, there is no actual requirement for the object returned to be an instance of the class that is specified (though I guess I have never tested this out). At any rate, it might help your sanity if you do make sure the controller is an instance of a class implementing the interface declared in the fx:controller
attribute (and this also gives you the chance to specify any functionality you expect that controller to provide).
Another approach is to use the FXML "custom component" pattern. This essentially reverses the creational roles of the FXML and the controller, meaning that instead of loading an FXML file, which more or less silently creates the controller instance, you create a Java object that serves as the controller, and it takes responsibility for loading the FXML.
Using this approach you could create multiple "custom components" that all load the same FXML file.
So if before you had a Subform2.fxml
that looked like:
<!-- headers, etc -->
<GridPane xmlns="..." fx:controller="...">
<!-- -->
</GridPane>
you would replace the root element with:
<!-- headers, etc -->
<fx:root type="GridPane" xmlns="..." >
<!-- -->
</fx:root>
Note the controller is no longer specified here.
Now you can create a controller-like class, which just has to extend GridPane
(or more generally, it extends the class specified in the "type" attribute of the fx:root
element). In the constructor, create an FXMLLoader
for the FXML file, and set both the root and the controller to the current object:
public class Subform2 extends GridPane {
@FXML
private TextField someTextField ;
// etc
public Subform2() throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/path/to/Subform2.fxml"));
loader.setRoot(this);
loader.setController(this);
loader.load();
}
@FXML
public void handleSomeEvent(ActionEvent event) {
// ...
}
// ...
}
To use this, you can do so in Java simply with
GridPane subform2 = new Subform2();
If you want to use it in FXML, instead of using an <fx:include>
, just use a regular instance element. You can, of course, specify any properties as usual, whether they are inherited from GridPane
or whether you define them in the class itself:
<Subform2 alignment="center">
<padding>
<Insets top="5" right="5" bottom="5" left="5"/>
</padding>
</Subform2>
This may meet your needs, as you can define just a single FXML file, but arbitrarily many different classes like this that all load that single FXML file.
As a slight variant of this, you can make the GridPane
subclass simply the root of the FXML, and pass another object to its constructor to act as the controller. So, for example, if you have defined an interface (or abstract class) representing the controller for the FXML file:
public interface Subform2Controller {
/* methods */
}
you can do
public class Subform2 extends GridPane {
public Subform2(@NamedArg("controller") Subform2Controller controller) throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/path/to/Subform2.fxml"));
loader.setRoot(this);
loader.setController(controller);
loader.load();
}
}
}
This allows you to do things like
<Subform2 controller="${subform2Controller}" />
which, again, lets you load an FXML file and specify the controller dynamically.
Upvotes: 1