Linking content from different fxml files - java

I have a JavaFx project I created using SceneBuilder.
I am also using a Guice plugin architecture.
I have one .fxml file that has a pane that I want to be the the content of another .fxml file.
Is there any easy way to link .fxml content from one file to another?
I have not used fx.guice plugin architecture before. Is there an easier way to this with plugin control?
Thanks!!

This was a big problem for us since we also are using Guice and JavaFX.
tl;dr I've attached some code at the bottom that we've been using for a year and a bit now without issue.
edit: I should've mentioned we did all this stuff before fx,guice existed, so this exists entirely outside that, and I probably should be using it.
Yes, but you will have to modify the view tree from java, and you must instantiate two controllers. If you're willing to let the fxml loader instantiate your controllers (we're not, more in a sec), then you simply need to have code along the lines of
solution 1:
loadMergedView(){
fxmlLoader.setLocation(getClass().getResource("/com/yourpkg/YourOuterView.fxml"));
Pane outerRoot = fxmlLoader.load();
fxmlLoader.setLocation(getClass().getResource("/com/yourpkg/YourInnerView.fxml"));
Pane innerView = fxmlLoader.load();
((Region)outerRoot.getChildren().get(2))...getChildren().add(innerView);
}
which isn't nice because it means the FX loader will try to create your controller for you, but you probably want guice to do that
luckily you can call setController (or setControllerFactory) to leverage guice, so now we have
solution 2:
#Inject private OuterController outerController
#Inject private InnerController innerController
loadMergedView(){
fxmlLoader.setLocation(getClass().getResource("/com/yourpkg/YourOuterView.fxml"));
//using the setControllerFactory instead of the setController
//means you can still declare the controller type in FXML
//which is good for our IDE intelliJ and general readability
fxmlLoader.setControllerFactory(() -> outerController);
Pane outerRoot = fxmlLoader.load();
fxmlLoader.setLocation(getClass().getResource("/com/yourpkg/YourInnerView.fxml"));
fxmlLoader.setControllerFactory(() -> innerController);
Pane innerView = fxmlLoader.load();
((Region)outerRoot.getChildren().get(2))...getChildren().add(innerView);
}
which is better but requires a 3rd party to load your components. What you really want, a le dependency-injection, is to have the child view be resolved first and as part of the resolution of the parent view.
For us, this brings us to
Solution 3:
class OuterController{
#FXML Pane rootPane;
#FXML Stuff otherStuffBoundInFXML;
#FXML AnchorPane innerContactPaneOne;
#Inject
public OuterController(InnerController inner, FXMLLoader loader){
loader.setControllerFactory(type -> this);
loader.setLocation(getClass().getResource("/com/yourpkg/YourOuterView.fxml"));
loader.load();
innerContactPaneOne.getChildren().add(inner.getRootView());
}
}
class InnerController{
#FXML Pane innerContentPaneTwo; //this will be a child of PaneOne in OuterController
#FXML Button otherStuff;
#Inject
public InnerController(FXMLLoader loader){
loader.setControllerFactory(type -> this);
loader.setLocation(getClass().getResource("/com/yourpkg/YourInnerVIew.fxml"));
loader.load();
}
public Node getRootView(){
return innerContentPaneTwo;
}
}
//with somebody calling
OuterController rootController = injector.getInstance(OuterController.class);
And finally, in the name of 'convention over configuration', we created a few classes (attached below) that attempt to 'automatically' find the view by reflecting on the controllers name (eg OuterController) and assuming that it will find an FXML view view in the same directory as the controller's class file with the word controller replaced with view (eg OuterView.fxml)). We also leveraged a neat trick in super-ctor-order in java to allow us to have pre-setup FXML values.
So now we get:
solution4 :
class OuterController extends PreloadedFX{
#FXML Pane rootPane;
#FXML Stuff otherStuffBoundInFXML;
#FXML AnchorPane innerContactPaneOne;
#FXML Checkbox importantCheckbox;
#FXML Label importantLabel;
// because of 'PrealoadedFX' getting called first,
// you can actually inline initialize object constants
// like this
private final ObservableBooleanValue isSelected = importantCheckbox.selectedProperty();
// or using an initializer
{
int x = 4;
importantLabel.setText(importantLable.getText() + x);
}
#Inject
public OuterController(InnerController inner, FXMLLoader loader){
super(loader);
innerContactPaneOne.getChildren().add(inner.getRootView());
}
}
class InnerController extends PreloadedFX{
#FXML Pane innerContentPaneTwo; //this will be a child of PaneOne in OuterController
#FXML Button otherStuff;
#Inject
public OuterController(FXMLLoader loader){
super(loader);
}
public Node getRootView(){
return innerContentPaneTwo;
}
}
//with somebody calling
OuterController rootController = injector.getInstance(OuterController.class);
you can get the source code for the PreloadedFX and View-By-Convention code here:
https://gist.github.com/Groostav/ff35eb2d19b348f2e25c
which is as elegant as I've been able to make this particular union of frameworks.
Hope that helps!

Related

javafx choicebox to trigger a method onchange

Okay so i am very rusty on my java and even more on javafx. so i got a choicebox "categoryDrop" that when the value of the choicebox change i want to trigger this event that then takes the value of the choicebox and compare to an object "Folder" categorylist wich is an attribute it has.
here is my code
#FXML
private void folderByCategory(ActionEvent event) {
System.out.println("här1");
TreeItem<DocumentObject<?>> treeRoot = new TreeItem<>(new Folder());
for (Folder folder : logic.getFolderList()) {
if(f.getCategoryList().contains(categoryDrop.valueProperty())){
System.out.println("här2");
TreeItem<DocumentObject<?>> newFolders = new TreeItem<>(folder);
for(FileReference file : folder.getFileList()){
System.out.println(file.getName());
TreeItem<DocumentObject<?>> fileNode = new TreeItem<>(file);
newFolders.getChildren().add(fileNode);
}
treeRoot.getChildren().add(newFolders);
treeRoot.setExpanded(true);
}
treeNav.setRoot(treeRoot);
}
}
But then when i looked in scenebuilder i didnt see any good way to implement the method so it triggers when it changes. Anyone know a better way to do this? should i use a listener instead maybe?
ChoiceBox has an onAction property, so in FXML you can simply assign this controller method to this property:
<ChoiceBox fx:id="categoryDrop" onAction="#folderByCategory" />
Unfortunately, the current version of Scene Builder does not support this property, so you cannot set this directly from Scene Builder. There is a current issue filed for this.
Some workarounds are:
Edit the FXML manually to add the onAction attribute, as above.
Use a ComboBox instead of a ChoiceBox. The functionality is similar (though not identical) and a ComboBox will likely do what you need. Scene Builder does support the onAction property of a ComboBox.
Register the handler in the controller's initialize() method instead. All you need is
#FXML
private ChoiceBox<...> categoryDrop ;
public void initialize() {
categoryDrop.setOnAction(this::folderByCategory);
// existing code ...
}
#FXML
private void folderByCategory(ActionEvent event) {
// existing code...
}

JavaFx how to avoid creating one huge controller

I have an app in JavaFX, which has main scene with menu and toolbar, and smaller scenes, which are injected into this main scene, after one of menu buttons are being pressed.
Now, HomeCntroller is responsible for either scene components: Home Scene (with toolbar and menu), and injected scene. This leads me to create massive, huge and very unprofessional controller if number of injected scenes is more than one.
How to split controller responsibility?
Now my Controller looks like this:
changeDashboardPane method injects smaller Pane into my main HomePane.
#Component
#RequiredArgsConstructor(onConstructor = #__(#Autowired) )
public class HomeController extends AbstractController {
private static final Logger LOG = Logger.getLogger(HomeController.class);
private final BudgetProfileService budgetProfileService;
#FXML
private Label usernameLabel;
#FXML
private ComboBox<String> budgetProfilesComboBox;
#FXML
private AnchorPane dashBoardPane;
#FXML
public void initialize() {
refreshUsernameLabel();
getAllBudgetProfiles();
changeDashboardPane(PaneFactoryKeys.FINANCES_PANE);
}
private void refreshUsernameLabel() {
String username = UserAccountProvider.getLoggedUser().getUsername();
usernameLabel.setText(username);
}
private void getAllBudgetProfiles() {
List<String> budgetProfileNames = budgetProfileService.getAllBudgetProfileNames();
if (!budgetProfileNames.isEmpty()) {
budgetProfilesComboBox.getItems().clear();
budgetProfilesComboBox.getItems().addAll(budgetProfileNames);
}
}
#FXML
public void handleFinancesButtonAction() {
changeDashboardPane(PaneFactoryKeys.FINANCES_PANE);
}
#FXML
public void handlePeriodButtonAction() {
changeDashboardPane(PaneFactoryKeys.PERIOD_PANE);
}
#FXML
public void handleStatisticsButtonAction() {
changeDashboardPane(PaneFactoryKeys.STATISTICS_PANE);
}
#FXML
public void handleSettingsButtonAction() {
changeDashboardPane(PaneFactoryKeys.SETTINGS_PANE);
}
private final void changeDashboardPane(String paneFactoryKey) {
double injectedPanePosition = 0.0;
Pane paneToChange = getPaneFromFactory(paneFactoryKey);
dashBoardPane.getChildren().clear();
AnchorPane.setTopAnchor(paneToChange, injectedPanePosition);
dashBoardPane.getChildren().add(paneToChange);
}
}
To get this more clear, screens:
without injected second pane
with injected second pane
Any ideas guys?
I would recommend you to divide your main scene in smaller ones, for example you can have a tools scene, a header scene, a content scene and so on. Then you should have one controller for every scene.
After that I would use a publisher-subscriber pattern to deal with behaviors, like when you press a button on settings scene, it triggers an event that other scenes listen to and then they handle it changing their state accordingly.
I hope it was clear and can help!
Create multiple controllers , multiple FXML files - to continue on my answer that i provided you before, JavaFX how to inject new FXML content to current Scene each of those views that have separate fxml file also has
fx:controller="appplication.ExampleViewController"
attached to it.So what you do is create main controller as was mentioned , that is basically the FRAME CONTAINER that encapsulates controls to change your dynamic container.If your application is really ui rich and have a lot of functionality in one controller , you can break down your view even further:
For instance take out menu and put it into separated controller , and insert it into your main view with main controller
/same way as in method setView()/
, what you are doing is just taking it away to keep controller code smaller, YOU DONT DECREASE/INCREASE SCENE GRAPH THIS WAY, doesnt have a drawback its just a personal preference.
You gonna end up with more fxml files and controllers in the end.Its all the same thing as from your previous question there is no additional code needed you can actually reuse what was already provided.
Data between controllers are passed thru MODEL. - look more into MVC dont work with application data in controllers only care about view or passing them from/into model
To avoid a huge contoller class, as I am using multiple tabs, I split the tabs to single java files.
My solution was to create a cascade of classes:
Base: Containing all defs for FX types
Tab1 extends Base: Tab one implementation
Tab2 extends Tab1: Tab two implementation
Controller extends Tab2 implements Initializable: Implements initialize(URL url, ResourceBundle resourceBundle)
Important:
Any accessed object must be definded in the current tab or before.
Any Objects in Base are available in Controller whereas no object of Controller is accessable in Base, Tab1 or Tab2.
Feel free to add your opinion as comment or submit a improvement.

JavaFX FXML access succeed with lookup() method but not with #FXML annotation

I start exploring the JavaFX FXML application technology.
I use one main Stage accessed in Main class with Main.getStage() that is invoked in the start of application with the overriden method public void start(Stage stage). Having two public static Scene inside to keep the persistence while switching them.
#Override
public void start(Stage stage) throws Exception {
STAGE = stage;
LOGIN = new Scene(FXMLLoader.load(getClass().getResource("Login.fxml")));
REGISTER = new Scene(FXMLLoader.load(getClass().getResource("Register.fxml")));
STAGE.setScene(LOGIN);
STAGE.setTitle("FXApplication");
STAGE.show();
}
public static Stage getStage() {
return STAGE;
}
Both Scenes have the same controller class called MainController. Using:
Button with fx:id="buttonLoginRegister" to go to the REGISTER Scene
Button with fx:id="buttonRegisterBack" to go back to the LOGIN one.
and both having the same onClick event handleButtonAction(ActionEvent event). The TextFields are fields for a username to log in/register.
#FXML private Button buttonLoginRegister;
#FXML private Button buttonRegisterBack;
#FXML private TextField fieldLoginUsername;
#FXML private TextField fieldRegisterUsername;
#FXML
private void handleButtonAction(ActionEvent event) throws IOException {
Stage stage = Main.getStage();
if (event.getSource() == buttonLoginRegister) {
stage.setScene(Main.REGISTER);
stage.show();
// Setting the text, the working way
TextField node = (TextField) stage.getScene().lookup("#fieldRegisterUsername");
node.setText(fieldLoginUsername.getText());
// Setting the text, the erroneous way
// fieldRegisterUsername.setText(fieldLoginUsername.getText());
} else {
stage.setScene(Main.LOGIN);
stage.show();
}
}
My goal is to copy the value from the LOGIN TextField to the one in the REGISTER scene. It works well using the code above. However firstly I tried to access the element in the another Scene with:
fieldRegisterUsername.setText(fieldLoginUsername.getText());
And it's erroneous. To be exact, the fieldRegisterUsername is null.
Why are some elements found with the lookup(String id) method and not with #FXML annotation?
As mentioned in my comment, sharing a controller between different views is rarely a good idea, and I'd strongly advise you to make a separate controller for each view.
As to your problem itself - you have two instances of your controller class, one for each time you call FXMLLoader.load. Presumably, one view has the fieldLoginUsername TextField, while the other has fieldRegisterUsername.
If the condition of the if statement is met, it means the active scene was the Login scene, thus the controller handling it is the one which has fieldLoginUsername, so naturally fieldRegisterUsername will be null.
But on the first line inside the if clause you change the active scene to the Register one, so by the time you call scene#lookup you are referring to the scene whose controller is the Register controller, the one that does have fieldRegisterUsername.
If you were to call scene#lookup before changing the active scene you would find it returns null as well.
If you must use the same class for controller, you probably want to make sure you only have one instance of that class. That would necessitate using FXMLLoader#setController.

How can I pass objects between the JavaFX controllers in this source code?

Link to tutorial explaining the code.
https://www.youtube.com/watch?v=5GsdaZWDcdY
Overview:
class ScreensController extends StackPane //keep pane so we can remove add screens on top/bottom
It has this method for loading multiple screens.
public boolean loadScreen(String name, String resource) {
try { //fxml file
FXMLLoader myLoader = new FXMLLoader(getClass().getResource(resource));
//System.out.println(resource);
Parent loadScreen = (Parent) myLoader.load();//class cast to controlled screen
ControlledScreen myScreenControler = ((ControlledScreen) myLoader.getController());//Returns the controller associated with the root object.
//inject screen controllers to myscreencontroller
myScreenControler.setScreenParent(this);// inject screen controllers to each screen here
addScreen(name, loadScreen);
return true;
} catch (Exception e) {
System.out.println(e.getMessage());
return false;
}
}
Each screen controller has this object
ScreensController myController;
And this method for switching between screens (switch which controller is in control)
#FXML
private void goToScreen2(ActionEvent event){
myController.setScreen(ScreensFramework.screen2ID);
}
public interface ControlledScreen {
//This method will allow the injection of the Parent ScreenPane
public void setScreenParent(ScreensController screenPage);
}
This is a very brief synopsis and I'm sure I'm leaving out integral parts of the code.
My question is basically How can I send something (anything) from one screen to another?
I'd also like to be able to have a universal model object that all the controllers can access and control.
If you take a look at my code here
You will see I am putting all my model objects into the "initialize" method of each SEPARATE controller.
If any of you can give me some pointers or feedback on my code I would really appreciate it. Thanks.

JavaFX class controller scene reference

Is there any way of getting the Scene object of an FXML loaded file from the associated class controller.
I'm doing something like this:
#FXML
private AnchorPane anchor;
Scene scene = anchor.getScene();
but i'd like a solution that does not reference the AnchorPane control.
Why not? Controller is an abstract class, he's not aware about UI unless you deliberately make him know.
Nodes (inlcuding AnchorPane) are another story, they hardly exists outside for scenegraph. So it's perfectly fine to ask Node about his parent or scene.
If you still want to handle that separately there are next approaches:
you can create a custom controller and set scene after loader. Just note that at the time initialize() called it wouldn't yet initialized.
public class MyController {
private void Scene scene;
public void setScene(Scene scene) { this.scene = scene; }
}
// loading code
FXMLLoader fxmlLoader = new FXMLLoader();
AnchorPane root = (AnchorPane) fxmlLoader.load(getClass().getResource("MyApp.fxml"));
MyController myController = (MyController) fxmlLoader.getController();
myController.setScene(scene);
You can create a custom fxml control which will incorporate controller and he can just call getScene() for itself. See an example here: https://stackoverflow.com/a/10718683/1054140
I tried your answer, but it did not work, I found the reason here:
JavaFX: How to get stage from controller during initialization?
after the comment:
// loading code
don't use the static load method
AnchorPane root=(AnchorPane) FXMLLoader.load(getClass().getResource("MyApp.fxml"));
but instead use instantiated loader's method
AnchorPane root=(AnchorPane) fxmlLoaded.load(getClass().getResource("MyApp.fxml"));

Categories