Events JavaFX&;FXML:控制器事件处理程序和初始化最佳实践

Events JavaFX&;FXML:控制器事件处理程序和初始化最佳实践,events,javafx,dependency-injection,event-handling,initialization,Events,Javafx,Dependency Injection,Event Handling,Initialization,我遇到了一个设计问题,它与JavaFX控制器的事件处理和初始化顺序有关 每当选择相应的选项卡时,我都要更新选项卡窗格。为了实现这一点,我使用FXML注册事件处理程序,如下所示: <Tab fx:id="browseCollectionTab" onSelectionChanged="#tabChanged" text="Browse Images"> FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(fxml

我遇到了一个设计问题,它与JavaFX控制器的事件处理和初始化顺序有关

每当选择相应的选项卡时,我都要更新选项卡窗格。为了实现这一点,我使用FXML注册事件处理程序,如下所示:

<Tab fx:id="browseCollectionTab" onSelectionChanged="#tabChanged" text="Browse Images">
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(fxmlFile));
    Parent root = fxmlLoader.load(); 

    AbstractController ctrl = (AbstractController)fxmlLoader.getController();
    ctrl.setModel(this.model);
    ctrl.setUp();
updateImageView依次使用使用依赖项注入传递给控制器的数据源加载图像

选项1: 此依赖项注入目前的实现方式如下:

<Tab fx:id="browseCollectionTab" onSelectionChanged="#tabChanged" text="Browse Images">
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(fxmlFile));
    Parent root = fxmlLoader.load(); 

    AbstractController ctrl = (AbstractController)fxmlLoader.getController();
    ctrl.setModel(this.model);
    ctrl.setUp();
选项2 我可以使用控制器的initialize()方法使用单例对其进行初始化。这确实打破了依赖注入,不是我的首选解决方案

选项3 我可以避免使用FXML并手动实例化所有内容。这允许我在调用JavaFX/FXML之前实例化控制器并执行依赖项注入。网上有很多例子,对于复杂的GUI来说,这些例子最终都是一团糟。我想坚持使用FXMLLoader,因为这似乎是一种整洁而舒适的方式。如果这实际上不是最佳做法,请指出

选项4 我可以在控制器的initialize()方法中手动注册事件处理程序(或者在从其他地方执行依赖项注入/设置控制器之后)。这违背了首先在FXML中定义事件处理程序的意义

那么,选项1和选项2有什么问题?tabChanged实际上是在控制器上执行任何初始化之前调用的,这会导致空指针异常。现在,我可以忽略所有事件,直到控制器被初始化——这可能是个坏主意,因为只出现一次的事件将被忽略。另一种选择是(可能)在许多事件处理程序中强制初始化。这似乎也不是一个可行的选择

我一定错过了什么明显的东西。我知道这与常见的设计选择/最佳实践有关;然而,我无法向谷歌提供正确的关键词


我期待着您的帮助/建议-谢谢

您展示的示例实际上是一个非常不寻常的示例:通常在加载过程完成之前无法调用事件处理程序。选项卡选择是一种异常现象,因为您实际上是在响应以编程方式发生的属性更改,并且在将选项卡添加到空选项卡窗格时确实会发生更改。因此,这是一种不寻常的情况,在加载完成之前可以调用事件处理程序

简单解 考虑更改控制器与FXML文件的关联方式。一个选项是从FXML文件中删除
fx:controller
属性
,并在代码中设置控制器。这使您有机会首先正确初始化控制器:

FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(fxmlFile));
AbstractController ctrl = new ConcreteControllerImplementation();
ctrl.setModel(this.model);
ctrl.setUp();
Parent root = fxmlLoader.load(); 

更复杂的方法 另一个更复杂的选择是使用控制器工厂。这是一个将控制器类映射到实际控制器实例的函数。在这种情况下,FXML文件中仍然有
fx:controller
属性(这种创建FXML文件的标准方法可以认为是一种优势,因为它为SceneBuilder这样的工具提供了检查方法和
@FXML
-注释字段存在等的机会)。另一个好处是控制器工厂被传播到
中包含的任何FXML文件中,这允许您在使用它们之前初始化它们的控制器

在下文中,我假设您的
型号
属于
型号

首先,将控制器定义为具有一个构造函数,该构造函数将
模型
作为参数,即

public class ConcreteControllerImplementation extends AbstractController {

    private final Model model ;

    public ConcreteControllerImplementation(Model model) {
        this.model = model ;
        // do setup here, not in separate method...
    }

    public void initialize(URL url, ResourceBundle resources) {
        // normal controller setup stuff here
        // any @FXML annotated fields are now initialized
    }
}
要创建可重用控制器工厂,您需要进行一些反思:

Model model = ... ;

Callback<Class<?>, Object> controllerFactory = type -> {
    try {
        for (Constructor<?> c : type.getConstructors()) {
            if (c.getParameterCount() == 1 && c.getParameterTypes()[0].equals(Model.class)) {
                return c.newInstance(model);
            }
        }
        // no matching constructor: just use default (no-arg) constructor:
        return type.newInstance();
    } catch (Exception exc) {
        // fatal...
        throw new RuntimeException(exc);
    }
};
这种技术还允许您使用依赖项注入框架。如果你用弹簧,你就可以

ApplicationContext context = ... ;
FXMLLoader loader = new FXMLLoader(getClass().getResource(...));
loader.setControllerFactory(context::getBean);

现在,您的控制器实例将由Spring bean工厂创建和管理,您可以使用Spring依赖项注入将模型注入其中。

Hi zeroByte,我可以指出的最佳示例是在初始化程序之前创建FXMLLoader,然后在初始化过程中将fxml加载到父级。通过这种方式,可以在initialize函数外部访问该变量,但在运行时仍然准备就绪。通过更新FXMLLoader变量,然后将新FXML设置为initialize函数外部的父项,仍然可以调整所选FXML。