将 Spring 与 FXML 嵌套控制器集成
Integrating Spring with FXML nested controllers
我正在使用 JavaFX 实现一个大型应用程序,但不确定如何处理嵌套控制器和 Spring。
FXML已经由设计组提供,使用include机制最多可嵌套3层FXML
模型将在 Spring
中定义
- 我需要将模型注入所有嵌套控制器
- 有多个相同类型的控制器实例。即屏幕的某些部分是相同的,但由相同模型类型的不同实例提供支持。
我目前的工作
我读过 Stephen Chin's blog - JavaFX in Spring Day 2 – Configuration and FXML 和其他 SO 问题,但是这些只涉及顶级控制器。
我尝试了 FXMLLoader.setControllerFactory() 机制并在应用程序上下文中定义了控制器,但是这只给出了要创建的控制器的 class,这意味着有无法区分两个相同类型但数据不同的控制器。
使用控制器工厂时出现问题:
loader.setControllerFactory(new Callback<Class<?>, Object>() {
@Override
public Object call(Class<?> param) {
// OK but doesn't work when multiple instances controller of same type
return context.getBean(param);
}
});
- 到目前为止,我最好的方法是使用 Stephen Chin 方法连接顶级控制器。
- 对于有多个bean 实例的情况,父控制器将通过@Autowire/@Qualifer 获取对特定bean 的引用,然后在相应的控制器上进行设置。
- 也可以通过在顶层控制器上公开它们并调用 autowire() 来连接下一级控制器
具体问题
- 有没有办法使用控制器工厂机制,这样我就可以在 spring 上下文中定义控制器,这样更容易连接它们?
- 我可以使用其他方法吗?
例子
Spring 上下文
<context:annotation-config />
<bean id="modelA" class="org.example.Model">
<property name="value" value="a value" />
</bean>
<bean id="modelB" class="org.example.Model">
<property name="value" value="b value" />
</bean>
顶级
<HBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.TopLevelController">
<children>
<TabPane tabClosingPolicy="UNAVAILABLE">
<tabs>
<Tab text="A">
<content>
<fx:include fx:id="a" source="nested.fxml" />
</content>
</Tab>
<Tab text="B">
<content>
<fx:include fx:id="b" source="nested.fxml" />
</content>
</Tab>
</tabs>
</TabPane>
</children>
</HBox>
嵌套级别
<HBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="200.0" prefWidth="200.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.NestedController">
<children>
<TextField fx:id="value" />
</children>
</HBox>
主要应用程序
public class NestedControllersSpring extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("/app-context.xml");
FXMLLoader loader = new FXMLLoader(getClass().getResource("/top-level.fxml"));
stage.setScene(new Scene(loader.load()));
TopLevelController top = loader.getController();
context.getAutowireCapableBeanFactory().autowireBean(top);
context.getAutowireCapableBeanFactory().autowireBean(top.getAController());
context.getAutowireCapableBeanFactory().autowireBean(top.getBController());
top.init(); // needed because autowire doesn't call @PostConstruct
stage.show();
}
}
顶级控制器
public class TopLevelController {
@FXML
private NestedController aController;
@FXML
private NestedController bController;
@Autowired
@Qualifier("modelA")
private Model a;
@Autowired
@Qualifier("modelB")
private Model b;
@PostConstruct
public void init() {
aController.setModel(a);
bController.setModel(b);
}
public NestedController getAController() {
return aController;
}
public NestedController getBController() {
return bController;
}
}
我看不出有什么方法可以完全干净地做到这一点。正如您所注意到的,控制器工厂获得的唯一信息是控制器 class,因此无法区分实例。您可以使用 FXML 为这些控制器提供更多值,但这些值将在 FXML 初始化阶段设置,这将在 Spring 初始化阶段之后发生。 (我认为这实际上就是你的 @PostConstruct
不起作用的原因:@PostConstruct
方法将在构造之后,但在 FXML 注入之前调用。)Spring 注入的控制器应该 总是 有 prototype
范围,恕我直言,因为你永远不会希望不同的 FXML 加载元素共享同一个控制器。
我认为我这样做的方法是使用模型的 prototype
范围将模型实例注入嵌套控制器(因此每个控制器都有自己的模型),然后从中配置这些模型顶层控制器。这仍然涉及一些连接 "by hand"(您不能使用 Spring 将您想要的值直接注入到模型中),但感觉比您上面概述的方法更简洁。当然,不确定它是否真的适用于您的实际用例。
所以:
申请-context.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="model" class="application.Model" scope="prototype" />
<bean id="nestedController" class="application.NestedController" scope="prototype" autowire="byName"/>
<bean id="topController" class="application.TopLevelController" scope="prototype" />
</beans>
Model.java:
package application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Model {
private final StringProperty text = new SimpleStringProperty();
public final StringProperty textProperty() {
return this.text;
}
public final String getText() {
return this.textProperty().get();
}
public final void setText(final String text) {
this.textProperty().set(text);
}
}
NestedController.java:
package application;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class NestedController {
@FXML
private Label label ;
private Model model ;
public Model getModel() {
return model ;
}
public void setModel(Model model) {
this.model = model ;
}
public void initialize() {
label.textProperty().bindBidirectional(model.textProperty());
}
}
Nested.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Tab?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.NestedController">
<center>
<Label fx:id="label"/>
</center>
</BorderPane>
TopLevel.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.Tab?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1"
fx:controller="application.TopLevelController">
<center>
<TabPane>
<tabs>
<Tab text="First">
<content>
<fx:include fx:id="firstTab" source="Nested.fxml" />
</content>
</Tab>
<Tab>
<content>
<fx:include fx:id="secondTab" source="Nested.fxml" />
</content>
</Tab>
</tabs>
</TabPane>
</center>
</BorderPane>
TopLevelController
必须将值连接到模型中,这有点麻烦。我不确定这是否比手动连接整个模型更清洁...
package application;
import javafx.fxml.FXML;
public class TopLevelController {
@FXML
private NestedController firstTabController ;
@FXML
private NestedController secondTabController ;
public void initialize() {
firstTabController.getModel().setText("First Tab");
secondTabController.getModel().setText("Second Tab");
}
}
应用程序组装相当简单:
package application;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class NestedControllersSpring extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
try (ClassPathXmlApplicationContext context
= new ClassPathXmlApplicationContext("application-context.xml")) {
FXMLLoader loader = new FXMLLoader(getClass().getResource("TopLevel.fxml"));
loader.setControllerFactory(cls -> context.getBean(cls));
primaryStage.setScene(new Scene(loader.load(), 600, 400));
primaryStage.show();
}
}
public static void main(String[] args) {
launch(args);
}
}
迄今为止最好的...
- 只要存在 FXML 注释而不是节点,递归地遍历控制器
- 使用 autowireBean 连接 @Inject / @Autowire 字段
- 如果存在,使用 initializeBean() 调用 @PostConstruct。
代码
public void recursiveWire(ClassPathXmlApplicationContext context, Object root) throws Exception {
context.getAutowireCapableBeanFactory().autowireBean(root);
context.getAutowireCapableBeanFactory().initializeBean(root, null);
for (Field field : root.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(FXML.class) &&
! Node.class.isAssignableFrom(field.getType())) {
// <== assume if not a Node, must be a controller
recursiveWire(context, field.get(root));
}
}
}
用法
@Override
public void start(Stage stage) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("/app-context.xml");
FXMLLoader loader = new FXMLLoader(getClass().getResource("/top-level.fxml"));
stage.setScene(new Scene(loader.load()));
recursiveWire(context, loader.getController());
stage.show();
}
不使用递归函数可以更简单一些:
public class GuiContext {
private static final ApplicationContext applicationContext = new AnnotationConfigApplicationContext(GuiAppConfiguration.class);
public Object loadFxml(final String fileName) throws IOException {
return FXMLLoader.load(App.class.getResource(fileName), null, null, applicationContext::getBean);
}
}
另外,可以在class开头加上“prototype”。
@Component
@Scope("prototype")
public class ExampleController implements Initializable {
我正在使用 JavaFX 实现一个大型应用程序,但不确定如何处理嵌套控制器和 Spring。
FXML已经由设计组提供,使用include机制最多可嵌套3层FXML
模型将在 Spring
中定义
- 我需要将模型注入所有嵌套控制器
- 有多个相同类型的控制器实例。即屏幕的某些部分是相同的,但由相同模型类型的不同实例提供支持。
我目前的工作
我读过 Stephen Chin's blog - JavaFX in Spring Day 2 – Configuration and FXML 和其他 SO 问题,但是这些只涉及顶级控制器。
我尝试了 FXMLLoader.setControllerFactory() 机制并在应用程序上下文中定义了控制器,但是这只给出了要创建的控制器的 class,这意味着有无法区分两个相同类型但数据不同的控制器。
使用控制器工厂时出现问题:
loader.setControllerFactory(new Callback<Class<?>, Object>() {
@Override
public Object call(Class<?> param) {
// OK but doesn't work when multiple instances controller of same type
return context.getBean(param);
}
});
- 到目前为止,我最好的方法是使用 Stephen Chin 方法连接顶级控制器。
- 对于有多个bean 实例的情况,父控制器将通过@Autowire/@Qualifer 获取对特定bean 的引用,然后在相应的控制器上进行设置。
- 也可以通过在顶层控制器上公开它们并调用 autowire() 来连接下一级控制器
具体问题
- 有没有办法使用控制器工厂机制,这样我就可以在 spring 上下文中定义控制器,这样更容易连接它们?
- 我可以使用其他方法吗?
例子
Spring 上下文
<context:annotation-config />
<bean id="modelA" class="org.example.Model">
<property name="value" value="a value" />
</bean>
<bean id="modelB" class="org.example.Model">
<property name="value" value="b value" />
</bean>
顶级
<HBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.TopLevelController">
<children>
<TabPane tabClosingPolicy="UNAVAILABLE">
<tabs>
<Tab text="A">
<content>
<fx:include fx:id="a" source="nested.fxml" />
</content>
</Tab>
<Tab text="B">
<content>
<fx:include fx:id="b" source="nested.fxml" />
</content>
</Tab>
</tabs>
</TabPane>
</children>
</HBox>
嵌套级别
<HBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="200.0" prefWidth="200.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.NestedController">
<children>
<TextField fx:id="value" />
</children>
</HBox>
主要应用程序
public class NestedControllersSpring extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) throws Exception {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("/app-context.xml");
FXMLLoader loader = new FXMLLoader(getClass().getResource("/top-level.fxml"));
stage.setScene(new Scene(loader.load()));
TopLevelController top = loader.getController();
context.getAutowireCapableBeanFactory().autowireBean(top);
context.getAutowireCapableBeanFactory().autowireBean(top.getAController());
context.getAutowireCapableBeanFactory().autowireBean(top.getBController());
top.init(); // needed because autowire doesn't call @PostConstruct
stage.show();
}
}
顶级控制器
public class TopLevelController {
@FXML
private NestedController aController;
@FXML
private NestedController bController;
@Autowired
@Qualifier("modelA")
private Model a;
@Autowired
@Qualifier("modelB")
private Model b;
@PostConstruct
public void init() {
aController.setModel(a);
bController.setModel(b);
}
public NestedController getAController() {
return aController;
}
public NestedController getBController() {
return bController;
}
}
我看不出有什么方法可以完全干净地做到这一点。正如您所注意到的,控制器工厂获得的唯一信息是控制器 class,因此无法区分实例。您可以使用 FXML 为这些控制器提供更多值,但这些值将在 FXML 初始化阶段设置,这将在 Spring 初始化阶段之后发生。 (我认为这实际上就是你的 @PostConstruct
不起作用的原因:@PostConstruct
方法将在构造之后,但在 FXML 注入之前调用。)Spring 注入的控制器应该 总是 有 prototype
范围,恕我直言,因为你永远不会希望不同的 FXML 加载元素共享同一个控制器。
我认为我这样做的方法是使用模型的 prototype
范围将模型实例注入嵌套控制器(因此每个控制器都有自己的模型),然后从中配置这些模型顶层控制器。这仍然涉及一些连接 "by hand"(您不能使用 Spring 将您想要的值直接注入到模型中),但感觉比您上面概述的方法更简洁。当然,不确定它是否真的适用于您的实际用例。
所以:
申请-context.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="model" class="application.Model" scope="prototype" />
<bean id="nestedController" class="application.NestedController" scope="prototype" autowire="byName"/>
<bean id="topController" class="application.TopLevelController" scope="prototype" />
</beans>
Model.java:
package application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Model {
private final StringProperty text = new SimpleStringProperty();
public final StringProperty textProperty() {
return this.text;
}
public final String getText() {
return this.textProperty().get();
}
public final void setText(final String text) {
this.textProperty().set(text);
}
}
NestedController.java:
package application;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class NestedController {
@FXML
private Label label ;
private Model model ;
public Model getModel() {
return model ;
}
public void setModel(Model model) {
this.model = model ;
}
public void initialize() {
label.textProperty().bindBidirectional(model.textProperty());
}
}
Nested.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Tab?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.NestedController">
<center>
<Label fx:id="label"/>
</center>
</BorderPane>
TopLevel.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.Tab?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1"
fx:controller="application.TopLevelController">
<center>
<TabPane>
<tabs>
<Tab text="First">
<content>
<fx:include fx:id="firstTab" source="Nested.fxml" />
</content>
</Tab>
<Tab>
<content>
<fx:include fx:id="secondTab" source="Nested.fxml" />
</content>
</Tab>
</tabs>
</TabPane>
</center>
</BorderPane>
TopLevelController
必须将值连接到模型中,这有点麻烦。我不确定这是否比手动连接整个模型更清洁...
package application;
import javafx.fxml.FXML;
public class TopLevelController {
@FXML
private NestedController firstTabController ;
@FXML
private NestedController secondTabController ;
public void initialize() {
firstTabController.getModel().setText("First Tab");
secondTabController.getModel().setText("Second Tab");
}
}
应用程序组装相当简单:
package application;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class NestedControllersSpring extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
try (ClassPathXmlApplicationContext context
= new ClassPathXmlApplicationContext("application-context.xml")) {
FXMLLoader loader = new FXMLLoader(getClass().getResource("TopLevel.fxml"));
loader.setControllerFactory(cls -> context.getBean(cls));
primaryStage.setScene(new Scene(loader.load(), 600, 400));
primaryStage.show();
}
}
public static void main(String[] args) {
launch(args);
}
}
迄今为止最好的...
- 只要存在 FXML 注释而不是节点,递归地遍历控制器
- 使用 autowireBean 连接 @Inject / @Autowire 字段
- 如果存在,使用 initializeBean() 调用 @PostConstruct。
代码
public void recursiveWire(ClassPathXmlApplicationContext context, Object root) throws Exception {
context.getAutowireCapableBeanFactory().autowireBean(root);
context.getAutowireCapableBeanFactory().initializeBean(root, null);
for (Field field : root.getClass().getDeclaredFields()) {
if (field.isAnnotationPresent(FXML.class) &&
! Node.class.isAssignableFrom(field.getType())) {
// <== assume if not a Node, must be a controller
recursiveWire(context, field.get(root));
}
}
}
用法
@Override
public void start(Stage stage) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("/app-context.xml");
FXMLLoader loader = new FXMLLoader(getClass().getResource("/top-level.fxml"));
stage.setScene(new Scene(loader.load()));
recursiveWire(context, loader.getController());
stage.show();
}
不使用递归函数可以更简单一些:
public class GuiContext {
private static final ApplicationContext applicationContext = new AnnotationConfigApplicationContext(GuiAppConfiguration.class);
public Object loadFxml(final String fileName) throws IOException {
return FXMLLoader.load(App.class.getResource(fileName), null, null, applicationContext::getBean);
}
}
另外,可以在class开头加上“prototype”。
@Component
@Scope("prototype")
public class ExampleController implements Initializable {