JavaFX fxml - Как использовать Spring DI с вложенными пользовательскими элементами управления?

Я прошел ряд учебных пособий по интеграции Spring DI с JavaFx, но я ударил стену, что простые примеры не покрывают (и я не могу понять).

Я хочу чистое разделение между слоями представления и представления. Я хотел бы использовать fxml для определения композиционных представлений и Spring, чтобы связать все это вместе. Вот конкретный пример:

Dashboard.fxml:

<GridPane fx:id="view"
          fx:controller="com.scrub.presenters.DashboardPresenter"
          xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml">
   <children>
      <TransactionHistoryPresenter fx:id="transactionHistory"  />
   </children>
</GridPane>

Main.java:

public void start(Stage primaryStage) throws Exception{
    try {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppFactory.class);
        SpringFxmlLoader loader = context.getBean(SpringFxmlLoader.class);
        primaryStage.setScene(new Scene((Parent)loader.load("/views/dashboard.fxml")));
        primaryStage.setTitle("Hello World");
        primaryStage.show();
    } catch(Exception e) {
        e.printStackTrace();
    }
}

SpringFxmlLoader.java:

public class SpringFxmlLoader {

    @Autowired
    ApplicationContext context;

    public Object load(String url) {
        try {
            FXMLLoader loader = new FXMLLoader(getClass().getResource(url));
            loader.setControllerFactory(new Callback<Class<?>, Object>() {
                @Override
                public Object call(Class<?> aClass) {
                    return context.getBean(aClass);
                }
            });
            return loader.load();
        } catch(Exception e) {
            e.printStackTrace();
            throw new RuntimeException(String.format("Failed to load FXML file '%s'", url));
        }
    }
}

Поэтому, когда загружается DashboardPresenter, SpringFxmlLoader корректно вводит контроллер с помощью loader.setControllerFactory.

Однако настраиваемый элемент TransactionHistoryPresenter загружается с новым экземпляром, а не из контекста Spring. Он должен использовать собственный FXMLLoader?

Любые идеи о том, как сделать пользовательские элементы управления хорошими с Spring? Я действительно не хочу идти по пути, когда контроллеры/проводники вручную подключают их.

Ответ 1

Основная проблема здесь - убедиться, что Spring инициализируется в том же потоке приложения JavaFX. Обычно это означает, что код Spring должен выполняться в потоке приложения JavaFX; другие трудоемкие задания, конечно же, могут выполняться на их собственной ветке.

Это решение, которое я собрал, используя этот учебник и свои собственные знания Spring Загрузка:

@SpringBootApplication
@ImportResource("classpath:root-context.xml")
public class JavaFXSpringApplication extends Application {

  private static final Logger log = LoggerFactory.getLogger(JavaFXSpringApplication.class);

  private Messages messages;

  private static String[] args;

  @Override
  public void start(final Stage primaryStage) {

    // Bootstrap Spring context here.
    ApplicationContext context = SpringApplication.run(JavaFXSpringApplication.class, args);
    messages = context.getBean(Messages.class);
    MainPaneController mainPaneController = context.getBean(MainPaneController.class);

    // Create a Scene
    Scene scene = new Scene((Parent) mainPaneController.getRoot());
    scene.getStylesheets().add(getClass().getResource("/css/application.css").toExternalForm());

    // Set the scene on the primary stage
    primaryStage.setScene(scene);
    // Any other shenanigans on the primary stage...
    primaryStage.show();
  }

  public static void main(String[] args) {

    JavaFXSpringApplication.args = args;

    launch(args);
  }
}

Этот класс является как точкой входа приложения JavaFX, так и точкой входа инициализации загрузки Spring, следовательно, происходит переход от varargs. Импорт внешнего файла конфигурации упрощает сохранение основного класса при использовании других Spring связанных компонентов (т.е. Настройка Spring данных JPA, пакетов ресурсов, безопасности...)

В методе "запуска" JavaFX основной ApplicationContext инициализируется и живет. Любые bean, используемые в этой точке, должны быть получены через ApplicationContext.getBean(), но каждый другой аннотированный bean (если он находится в потоковом пакете этого основного класса) будет доступен, как всегда.

В частности, контроллеры объявляются в этом другом классе:

@Configuration
@ComponentScan
public class ApplicationConfiguration {

  @Bean
  public MainPaneController mainPaneController() throws IOException {
    return (MainPaneController) this.loadController("path/to/MainPane.fxml");
  }

  protected Object loadController(String url) throws IOException {
    InputStream fxmlStream = null;
    try {
      fxmlStream = getClass().getResourceAsStream(url);
      FXMLLoader loader = new FXMLLoader();
      loader.load(fxmlStream);
      return loader.getController();
    } finally {
      if (fxmlStream != null) {
        fxmlStream.close();
      }
    }
  }
}

Вы можете увидеть любой контроллер (у меня есть только один, но его может быть много) аннотируется с помощью @ Bean, а весь класс - это Конфигурация.

Наконец, вот MainPaneController.

public class MainPaneController {

  @Autowired
  private Service aService;

  @PostConstruct
  public void init() {
    // ...stuff to do with components...
  }

  /*
   * FXML Fields
   */
  @FXML
  private Node root;

  @FXML
  private TextArea aTextArea;

  @FXML
  private TextField aTextField;

  @FXML
  private void sayButtonAction(ActionEvent event) {
    aService.doStuff(aTextArea, aTextField);
  }
}

Этот контроллер объявлен как @ Bean, поэтому он может быть @Autowired с и из любого другого @Beans (или служб, компонентов и т.д.). Теперь, например, вы можете получить ответ на нажатие кнопки и делегировать логику, выполняемую в своих полях, на @Service. Любой компонент, объявленный в Spring -созданных контроллерах, будет управляться Spring и, таким образом, знать об этом контексте.

Все довольно легко и просто настроить. Не стесняйтесь спрашивать, есть ли у вас какие-либо сомнения.