英文:
How to properly use nested controllers to access an attribute value from another class?
问题
I want to pass an attribute value from a controller to another using nested controllers technique. But in the current setting the nested controller instance yields a null value.
Here's the code:
public class MainUI extends Application {
@Override
public void start(Stage stage) throws IOException {
String fxmlFile = "main.fxml";
FXMLLoader loader = new FXMLLoader();
Parent rootNode = (Parent) loader.load(getClass().getResource(fxmlFile));
// ...
}
}
main.fxml:
<AnchorPane>
<!-- ... -->
<fx:include fx:id="someInnerComponent" source="someInnerComponent.fxml" visible="false" />
</AnchorPane>
someInnerComponent.fxml:
<AnchorPane>
<!-- ... -->
<!-- nothing so special about the imports here -->
</AnchorPane>
mainController:
public class MainController {
// ...
private SomeInnerComponent someInnerComponentController;
public void setSomeInnerComponentController(SomeInnerComponent someInnerComponentControllerParam) {
this.someInnerComponentController = someInnerComponentControllerParam;
}
public void testValueOfController() {
someInnerComponentController.getAttVal(); // Now you can access it without static
// ...
}
}
And finally, SomeInnerComponent.java:
public class SomeInnerComponent {
private SomeType attVal;
@Override
@FXML
public void initialize(URL location, ResourceBundle resources) {
attVal = new SomeType();
// for holding a reference of this controller to the mainController class
mainController.setSomeInnerComponentController(this);
// ...
}
public void testSetAttVal() {
setAttVal(attVal); // Now this value can be accessed from the mainController
// ...
}
}
This eliminates the static reference and uses instance variables to pass the controller reference, achieving the desired nested controller behavior.
英文:
I want to pass an attribute value from a controller to another using nested controllers technique. But in the current setting the nested controller instance yields a null value.
Here's the code:
`public class MainUI extends Application{
@Override
public void start(Stage stage) throws IOException {
String fxmlFile = "main.fxml";
FXMLLoader loader = new FXMLLoader();
Parent rootNode = (Parent) loader.load(getClass().getResource(fxmlFile));
...
}`
main.fxml:
`<AnchorPane>
...
<fx:include fx:id="someInnerComponent" source="someInnerComponent.fxml" visible="false" />
</AnchorPane>`
someInnerComponent.fxml:
`<AnchorPane>
...
<!-- nothing so special about the imports here -->
</AnchorPane>`
mainController:
``public class MainController{
...
public static SomeInnerComponent someInnerComponentController = new SomeInnerComponent();
public static void setMainController(SomeInnerComponent someInnerComponentControllerrparam) {
someInnerComponentController = someInnerComponentControllerrparam;
}
...
public void testValueOfController(){
someInnerComponentController.getAttVal(); // returns empty string in case I remove static for someInnerComponentController
}
}`
and finally, SomeInnerComponent.java:
`public class SomeInnerComponent {
public SomeType attVal ;
@Override
@FXML
public void initialize(URL location, ResourceBundle resources) {
attVal = new SomeType();
//for holding a reference of this controller to the mainController class
MainController.setMainController(this);
...
}
public void testSetAttVal(){
setAttVal(attVal);// I want this value to be accessible from the mainController
...
}`
`
This works but the static reference to someInnerComponentController
in the mainController is annoying, how to get rid of that using a nested controller trick?
答案1
得分: 4
FXML 控制器
状态
一个 FXML 控制器几乎不应该有静态状态(即,没有 static
字段)。每次加载 FXML 文件时,都会创建其关联控制器类的新实例。如果有静态字段,那么该状态会在控制器类的所有实例之间共享,这在概念上是不合理的。
控制器之间的通信
控制器之间的通信应该很少直接完成。相反,您应该使用适当的应用程序架构,如模型-视图-控制器(MVC)或模型-视图-视图模型(MVVM)。然后,不同控制器之间以及因此视图之间的通信是通过与 模型 交互来完成的。模型应该以某种方式可观察,以便模型中的状态更改会适当地反映在其他视图中。唯一的例外是在嵌套 FXML 文件的上下文中。但即便如此,最好只是将嵌套控制器注入以传递共享模型的实例,然后将进一步的通信留给该模型。
请注意,FXML 控制器在 MVC 意义上不是一个“控制器”。使用 MVC 是一个“更高级”的概念。此外,FXML 控制器的功能更像是 Model-View-Presenter(MVP)架构中的“主持人”。
有关更多信息,请参阅诸如 Applying MVC With JavaFx 的问答。还可以寻找有关这些概念的教程、文章、书籍等。
注入嵌套控制器
如果您想访问嵌套 FXML 文件的控制器实例,那么您应该将其注入到“父”控制器中,类似于任何其他 FXML 组件。也就是说,给 fx:include
元素一个 fx:id
属性,然后在控制器中定义一个适当的字段。例如,如果您有:
<!--
其中 'Foo.fxml' 的根元素是 StackPane,
控制器是 'FooController' 的实例
-->
<fx:include fx:id="foo" source="Foo.fxml"/>
然后在控制器中会有:
// 用于注入嵌套 FXML 文件的根元素(如果需要)
@FXML private StackPane foo;
// 用于注入嵌套 FXML 文件的控制器实例(如果需要)
@FXML private FooController fooController; // 请注意字段名是 '<fx:id>Controller'
请注意,在初始化“父”控制器之前,嵌套控制器将被完全初始化。
控制器初始化
您问题中的嵌套控制器有以下方法:
@FXML
public void initialize(URL location, ResourceBundle resources)
但该类没有实现 javafx.fxml.Initializable
。这意味着这个 initialize
方法实际上没有被调用。
您应该 选择:
import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.Initializable;
public class Controller implements Initializable {
@Override
public void initialize(URL location, ResourceBundle resources) {
// ...
}
}
或者:
import java.net.URL; // 如果不需要,请省略
import java.util.ResourceBundle; // 如果不需要,请省略
import javafx.fxml.FXML;
public class Controller {
@FXML private URL location; // 注入位置;如果不需要,请省略
@FXML private ResourceBundle resources; // 注入资源;如果不需要,请省略
@FXML
private void initialize() { // 一个无参数的 'initialize' 方法
// ...
}
}
请注意,不实现 Initializable
的后一种方法是首选方法。
如果您的控制器不需要任何额外的初始化,那么可以完全省略 initialize
方法。
演示
这个演示只展示了如何注入嵌套控制器并调用它以及它包含的对象的方法。它没有展示如何使用诸如 MVC 这样的架构。
演示中,嵌套控制器初始化了一个“模型”对象,该对象只是具有整数属性。该嵌套控制器每次按下按钮时都会递增此属性。该“父”控制器通过该“模型”对象获取属性,并将标签的文本属性绑定到该属性。
请注意,此示例期望 FXML 资源位于类路径/模块路径的根目录(即,未命名/默认包中)。
代码
module-info.java(仅在代码是模块化时需要):
module app {
requires javafx.controls;
requires javafx.fxml;
exports com.example.app to javafx.graphics;
opens com.example.app to javafx.fxml;
}
Main.java:
package com.example.app;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(Main.class.getResource("/Main.fxml"));
primaryStage.setScene(new Scene(root, 500, 300));
primaryStage.show();
}
}
MainController.java:
package com.example.app;
import javafx.beans.binding.StringBinding;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class MainController {
@FXML private NestedController nestedController; // 注入嵌套控制器
@FXML private Label label;
@FXML
private void initialize() {
// 用于显示控制器初始化顺序的打印语句
System.out.println("初始化主控制器...");
// 显示初始化“
<details>
<summary>英文:</summary>
# FXML Controllers
## State
An FXML controller should virtually never have static state (i.e., no `static` fields). Each time you load an FXML file, a new instance of its associated controller class is created. If you have static fields, then that state is shared across all instances of the controller class, and that doesn't make sense conceptually.
#### Communication between controllers
Communicating between controllers should rarely, if ever, be done directly. Instead, you should be using a proper application architecture like Model-View-Controller (MVC) or Model-View-ViewModel (MVVM). Then communication between the different controllers, and thus views, is done by interacting with the _model_. The model would be observable in some way so that state changes in the model are reflected in other views as appropriate. The only exception to this is in the context of nested FXML files. But even then, it would probably be best to only inject the nested controller to pass it an instance of a shared model, then leave further communication to be done via said model.
Note an FXML controller is not a "controller" in the MVC sense. Using MVC is a "higher level" concept. Besides, the FXML controller functions more like a "presenter" from the Model-View-Presenter (MVP) architecture.
See Q&As like [Applying MVC With JavaFx][1] for more information. Also look for tutorials, articles, books, etc. on the concepts in general.
## Inject Nested Controller
If you want access to the nested FXML file's controller instance, then you should inject it into the "parent" controller similar to any other FXML component. That is to say, give the `fx:include` element an `fx:id` attribute, then define an appropriate field in the controller. For instance, if you had:
```lang-xml
<!--
Where the root element of 'Foo.fxml' is a StackPane and the
controller is an instance of 'FooController'
-->
<fx:include fx:id="foo" source="Foo.fxml"/>
Then in the controller you'd have:
// to inject root element of nested FXML file (if needed)
@FXML private StackPane foo;
// to inject nested FXML file's controller instance (if needed)
@FXML private FooController fooController; // note field name is '<fx:id>Controller'
Note a nested controller will be fully initialized before the "parent" controller is initialized.
Controller Initialization
The nested controller in your question has the following method:
@FXML
public void initialize(URL location, ResourceBundle resources)
But the class does not implement javafx.fxml.Initializable
. That means this initialize
method is not actually being invoked.
You should have either:
import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.Initializable;
public class Controller implements Initializable {
@Override
public void initialize(URL location, ResourceBundle resources) {
// ...
}
}
Or:
import java.net.URL; // omit if not needed
import java.util.ResourceBundle; // omit if not needed
import javafx.fxml.FXML;
public class Controller {
@FXML private URL location; // inject location; omit if not needed
@FXML private ResourceBundle resources; // inject resources; omit if not needed
@FXML
private void initialize() { // a no-argument 'initialize' method
// ...
}
}
Note the latter approach—the one that does not implement Initializable
—is the preferred approach.
If your controller does not need any additional initialization, then you can omit the initialize
method entirely.
Demonstration
This demo only shows injecting a nested controller and calling methods on it and the objects it contains. It does not show how to use an architecture like MVC.
The demo has the nested controller initialize a "model" object that simply has an integer property. Said nested controller increments this property every time the button is fired. The "parent" controller goes through this "model" object to get the property and binds a label's text property to it.
Note this example expects the FXML resources to be at the root of the class-path/module-path (i.e., in the unnamed/default package).
Code
module-info.java (only needed if code is modular):
module app {
requires javafx.controls;
requires javafx.fxml;
exports com.example.app to javafx.graphics;
opens com.example.app to javafx.fxml;
}
Main.java:
package com.example.app;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(Main.class.getResource("/Main.fxml"));
primaryStage.setScene(new Scene(root, 500, 300));
primaryStage.show();
}
}
MainController.java:
package com.example.app;
import javafx.beans.binding.StringBinding;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class MainController {
@FXML private NestedController nestedController; // inject nested controller
@FXML private Label label;
@FXML
private void initialize() {
// print statement to show order of controller initialization
System.out.println("Initializing main controller...");
// Show use of nested controller during initialization of "parent" controller
StringBinding binding = nestedController
.getModel()
.countProperty()// access attribute of "another class"
.asString("You clicked the button %,d times(s)!");
label.textProperty().bind(binding);
}
}
Main.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.StackPane?>
<BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
fx:controller="com.example.app.MainController">
<padding>
<Insets topRightBottomLeft="10"/>
</padding>
<center>
<StackPane>
<Label fx:id="label"/>
</StackPane>
</center>
<bottom>
<fx:include fx:id="nested" source="Nested.fxml"/>
</bottom>
</BorderPane>
NestedController.java:
package com.example.app;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
public class NestedController {
private final ClickCountModel model = new ClickCountModel();
public ClickCountModel getModel() {
return model;
}
@FXML
private void initialize() {
// print statement to show order of controller initialization
System.out.println("Initializing nested controller...");
}
@FXML
private void handleAction(ActionEvent event) {
event.consume();
model.incrementCount();
}
}
Nested.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.StackPane?>
<StackPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
fx:controller="com.example.app.NestedController">
<Button text="Click me!" onAction="#handleAction"/>
</StackPane>
ClickCountModel.java:
package com.example.app;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class ClickCountModel {
private final IntegerProperty count = new SimpleIntegerProperty(this, "count");
public final void setCount(int count) { this.count.set(count); }
public final int getCount() { return count.get(); }
public final IntegerProperty countProperty() { return count; }
public void incrementCount() {
setCount(getCount() + 1);
}
}
In Action
GIF:
Console output:
Initializing nested controller...
Initializing main controller...
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论