Update:
This code is now integrated into the fx-guice project where it will be maintained. I am not intending to modify the GitHub gists so this post now only serves to document the code as seen here.
It is very simple to inject controllers into an FXML control using Guice but once the UI starts to get bigger it is likely that we will need to split it out into smaller files. I wanted to do this with a list builder control simple to that in SceneBuilder used to build a list of CSS classes.
This code is now integrated into the fx-guice project where it will be maintained. I am not intending to modify the GitHub gists so this post now only serves to document the code as seen here.
It is very simple to inject controllers into an FXML control using Guice but once the UI starts to get bigger it is likely that we will need to split it out into smaller files. I wanted to do this with a list builder control simple to that in SceneBuilder used to build a list of CSS classes.
In my UI the controller of the parent control (called parent controller from now on) would tell the list builder control what items it could add to the list and what items were already in the list using the list builder's controller (the child controller). Getting this to work this is far from simple.
It is not possible to get or set a controller on an arbitrary control node. I fired up VisualVM to see where the list builder controller was referenced and the only incoming references were to the event handlers I had set up. If there were no event handlers it looks like the controller would get garbage collected. It was also not possible to set a Guice singleton scope on the controller as there may be several list builders in the user interface at the same time.
Here is a run down of my final solution although it is not altogether as simple as I would have liked it. This tutorial is much more complex than my previous FXML/Guice post, I had to look up my own tutorial on Guice scopes to work it out. Please let me know if anything is not clear
1. Add the Guice Module
Add the following Guice module to your Injector. This example also assumes that you are using the same Injector to inject controllers when loading FXML, this is detailed on my previous Guice/FXML post.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.google.inject.AbstractModule; | |
import com.google.inject.Provides; | |
public class FXMLLoadingModule extends AbstractModule { | |
private final FXMLLoadingScope fxmlLoadingScope; | |
public FXMLLoadingModule() { | |
fxmlLoadingScope = new FXMLLoadingScope(); | |
} | |
@Override | |
protected void configure() { | |
bindScope(FXMLLoadingScoped.class, fxmlLoadingScope); | |
} | |
@Provides | |
public ControllerLookup provideControllerLookup() { | |
return new ControllerLookup(fxmlLoadingScope.getControllers()); | |
} | |
@Provides | |
public FXMLLoadingScope provideFxmlLoadingScope() { | |
return fxmlLoadingScope; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package projmon.guice; | |
import java.util.ArrayList; | |
import com.google.inject.Key; | |
import com.google.inject.Provider; | |
import com.google.inject.Singleton; | |
@Singleton | |
public class FXMLLoadingScope extends EnterableScope { | |
private ArrayList<IdentifiableController> identifiables; | |
@Override | |
public void enter() { | |
super.enter(); | |
identifiables = new ArrayList<IdentifiableController>(); | |
} | |
@Override | |
public void exit() { | |
super.exit(); | |
identifiables = null; | |
} | |
@Override | |
protected <T> Object provideObject(Key<T> key, Provider<T> unscoped) { | |
Object providedObject = unscoped.get(); | |
if(providedObject instanceof IdentifiableController) { | |
if(identifiables != null) { | |
identifiables.add((IdentifiableController) providedObject); | |
} | |
} | |
return providedObject; | |
} | |
public ArrayList<IdentifiableController> getControllers() { | |
return identifiables; | |
} | |
} |
2. Use a scope annotation on the controller
Reusable controllers are annotated with the custom @FXMLLoadingScope.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Target({ TYPE, METHOD }) @Retention(RUNTIME) @ScopeAnnotation | |
public @interface FXMLLoadingScoped { | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import javafx.fxml.FXML; | |
import javafx.scene.Parent; | |
@FXMLLoadingScoped | |
public class ListBuilderController<T> implements IdentifiableController { | |
@FXML | |
private Parent root; | |
@Override | |
public String getId() { | |
return getParentId(root); | |
} | |
private String getParentId(Parent parent) { | |
if(parent.getId() != null && !"Content".equals(parent.getId())) { | |
return parent.getId(); | |
} | |
else { | |
return getParentId(parent.getParent()); | |
} | |
} | |
} |
3. Implement the IdentifiableController interface in the controller
Reusable controllers must implement the IdentifiableController interface which has one method, #getId(). #getId() must return a non-null String containing the ID of the parent of the root pane of the control. If this ID is null then return the second parent-parent's ID. Keep going until a non-null ID is found.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public interface IdentifiableController { | |
String getId(); | |
} |
4. Set an ID on the parent
When importing one FXML file into a parent, set the container ID to the ID you want for the controller.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<AnchorPane fx:id="factorsList" minHeight="138.0" prefHeight="138.0" prefWidth="418.0"> | |
<children> | |
<fx:include source="listBuilder.fxml" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" /> | |
</children> | |
<VBox.margin> | |
<Insets left="2.0" right="2.0" /> | |
</VBox.margin> | |
</AnchorPane> |
5. Retrieve the child controller
In our parent controller (the one that that will tell the list builder what to do) we need to retrieve the list builders controller to populate options and so on. To do this, Inject an instance of ControllerLookup into the parent controller.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import java.util.List; | |
public class ControllerLookup { | |
private final List<IdentifiableController> identifiables; | |
public ControllerLookup(List<IdentifiableController> identifiables) { | |
this.identifiables = identifiables; | |
} | |
@SuppressWarnings("unchecked") | |
public <T> T lookup(String id) { | |
for (IdentifiableController controller : identifiables) { | |
if(controller.getId().equals(id)) { | |
return (T) controller; | |
} | |
} | |
throw new IllegalArgumentException("Could not find a controller with the ID '" + id + "'"); | |
} | |
} |
I am using this in my own JavaFX project and although the implementation was not simple I have found it quite easy to use.