简体   繁体   中英

JavaFX fxml - How to use Spring DI with nested custom controls?

I've been through a number of tutorials on integrating Spring DI with JavaFx but I've hit a wall that the simple examples dont cover (and I cant figure out).

I want clean separation between the view and presentation layers. I would like to use fxml to define composable views and Spring to wire it all together. Here's a concrete example:

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));
        }
    }
}

So when DashboardPresenter gets loaded the SpringFxmlLoader correctly injects the controller with the loader.setControllerFactory.

However, the custom TransactionHistoryPresenter control is loaded with a new instance and not from the spring context. It must be using its own FXMLLoader?

Any ideas how to make custom controls play nice with Spring? I really dont want to go down the path of having the controllers / presenters manually wiring them up.

The main problem here, is make sure that Spring is initialized on the same thread of the JavaFX application. This usually means that Spring code must be executed on the JavaFX application thread; other time-consuming jobs can of course be executed on their own thread.

This is the solution I put together using this tutorial and my own knowledge of Spring Boot :

@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);
  }
}

This class is both a JavaFX application entry point and a Spring Boot initialization entry point, hence the passing around of varargs. Importing an external configuration file makes it easier to keep the main class uncluttered while getting other Spring-related stuff ready (ie setting up Spring Data JPA, resource bundles, security...)

On the JavaFX "start" method, the main ApplicationContext is initialized and lives. Any bean used at this point must be retrieved via ApplicationContext.getBean(), but every other annotated bean (provided it is in a descendant package of this main class) will be accessible as always.

In particular, Controllers are declared in this other class:

@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();
      }
    }
  }
}

You can see any Controller (I have just one, but it can be many) is annotated with @Bean and the whole class is a Configuration.

Finally, here is 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);
  }
}

This Controller is declared as a @Bean, so it can be @Autowired with and from any other @Beans (or Services, Components, etc.). Now for example you can have it answer to a button press and delegate logic performed on its fields to a @Service. Any component declared into the Spring-created Controllers will be managed by Spring and thus aware of the context.

It is all quite easy and straightforward to configure. Feel free to ask if you have any doubts.

It is possible. Create custom BuilderFactory that delivers spring beans. Then assign it to the FXMLLoader fxmlLoader.setBuilderFactory(beanBuilderFactory);

@Component
public class BeanBuilderFactory implements BuilderFactory {

  @Autowired
  private ConfigurableApplicationContext context;

  public BeanBuilderFactory() {

  }

  @Override
  public Builder<?> getBuilder(Class<?> type) {
    try {
      Object bean = this.context.getBean(type);
      if (bean.getClass().isAssignableFrom(type))
        return new Builder() {
          @Override
          public Object build() {
            return bean;
          }
        };
      else
        return null;
    } catch (BeansException e) {
      return null;
    }
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM