Steve On Java

Hacking Java, JavaFX, and Flash with Agility

Stephen Chin

JavaFX in Spring Day 2 - Configuration and FXML

Welcome to part 2 of the JavaFX in Spring blog series. Yesterday I talked about some of the advantages of using Spring in client applications and showed how to initialize your application. For easy reference, you can flip to any of the blogs (as they are published) here:

Today we will dig into the configuration in more detail, covering details on how you can handle FXML-based UIs. But before we get into all that work, a short aside about the San Antonio Java User Group I met earlier tonight.

It was a great user group to speak to with lots of very enthusiastic (and sharp) folks. I lightly covered the topic of Spring and JavaFX integration, and when I asked, almost everyone in the room used Spring in their day jobs. Here is a quick photo I snapped of the group after the presentation:

IMG0562

I am not going to embed the entire presentation, but as promised, here is the full slide deck for reference.

Now, back to the Spring configuration example, we are building out. Here is what the ScreensConfiguration class looks like for my sample Customer application:

@Configuration
@Lazy
public class ScreensConfiguration {
    private Stage primaryStage;

    public void setPrimaryStage(Stage primaryStage) {
        this.primaryStage = primaryStage;
    }

    public void showScreen(Parent screen) {
        primaryStage.setScene(new Scene(screen, 800, 500));
        primaryStage.show();
    }

    @Bean
    CustomerDataScreen customerDataScreen() {
        return new CustomerDataScreen(customerDataScreenController());
    }

    @Bean
    CustomerDataScreenController customerDataScreenController() {
        return new CustomerDataScreenController(model, this);
    }

    @Bean
    @Scope("prototype")
    AutowireFXMLDialog errorDialog() {
        return new AutowireFXMLDialog(getClass().getResource("Error.fxml"), primaryStage, StageStyle.UNDECORATED);
    }

    @Bean
    @Scope("prototype")
    AutowireFXMLDialog addUserDialog() {
        return new AutowireFXMLDialog(getClass().getResource("AddUser.fxml"), primaryStage);
    }

    @Bean
    @Scope("prototype")
    AutowireFXMLDialog loginDialog() {
        return new AutowireFXMLDialog(getClass().getResource("Login.fxml"), primaryStage, StageStyle.UNDECORATED);
    }
}

There is a lot going on here, so let me break it down in a few bullets:

  • The configuration class is annotated with @Lazy so that any bean references will only be created on demand. (Other than this, it follows the basic Spring Java Config pattern using the @Configuration and @Bean annotations.)
  • The configuration allows us to set a primary stage (which we did during initialization in CustomerApp), and then display a screen in that stage by calling showScreen.
  • Each screen and dialog in the UI is represented as a Bean class. Notice that some are declared as @Scope("prototype"), which means a new instance will be created every time we use it. Without this annotation, the screen is a singleton so it will get created only once no matter how many times the method is called (such as customerDataScreen).

As you can see, with very little code, we are able to construct all the screens for the application and declare their behavior and dependencies. Most of these screens are FXML-based, although I did one screen/controller pair as standard JavaFX classes (the CustomerDataScreen extending StackPane). However, the much harder case is to work with FXML screens in Spring since JavaFX creates the controller for you, making bean configuration more difficult.

To encapsulate the logic for how to inject variables into the FXML controller, I created a small helper class called AutowireFXMLDialog. The source code for that is as follows:

public class AutowireFXMLDialog extends Stage {
    @Autowired
    private ApplicationContext context;

    protected final Object controller;

    public AutowireFXMLDialog(URL fxml, Window owner) {
        this(fxml, owner, StageStyle.DECORATED);
    }

    public AutowireFXMLDialog(URL fxml, Window owner, StageStyle style) {
        super(style);
        initOwner(owner);
        initModality(Modality.WINDOW_MODAL);
        FXMLLoader loader = new FXMLLoader(fxml);
        try {
            setScene(new Scene((Parent) loader.load()));
            controller = loader.getController();
            if (controller instanceof DialogController) {
                ((DialogController) controller).setDialog(this);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @PostConstruct
    private void postConstruct() {
        context.getAutowireCapableBeanFactory().autowireBeanProperties(controller, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
    }
}

Given an FXML file url as input, it does a few different things:

  • Instantiate the FXML UI and add it to the Stage. While this implementation is oriented towards dialogs, it would be quite easy to create a version that loaded it into a reusable Node.
  • Inject the view into the controller. We do this through a DialogController interface, which can optionally be implemented by the FXML controller class to get a reference to the view.
  • Autowire the controller class. For any remaining dependencies defined in the controller, we let Spring do its autowiring magic to inject all of the beans and proxies.

So this seems fairly straightforward, but coming up with this magic formula was fraught with peril. Here are some of the things that you might think are a good idea, but simply don't work:

  1. Why can't I autowire in the constructor? - Context is null until dependency injection happens in this class, so you would have to pass in a reference to the context (which defeats the purpose of DI)
  2. Can't I just have Spring inject an instance to the dialog? - This seems like a good idea, but is only possible after this class is fully initialized. For singleton beans this happens to work as long as you autowire in the PostConstruct method, but once you switch it to a prototype it fails miserably.
  3. Doesn't that parameter say "NO" autowiring? - Probably a poor choice of name by the Spring devs, it just means that you have to explicitly declare what you want wired (with the @Autowire annotation). If you accidentally use AUTOWIRE_BY_TYPE or AUTOWIRE_BY_NAME, you are in for a world of hurt, because it will attempt to autowire the reference to this dialog, which fails in the prototype case because of [2]. Same thing applies for validation (3rd parameter), which seems to think it should check parameters that are not explicitly wired even though you defined the autowire type as AUTOWIRE_NO.

So the short summary is do it my way or you will be a sad panda.

sad panda

Finally, to bring this all together, here is what the controller looks like for the ErrorDialog:

public class ErrorController implements DialogController {
    private AutowireFXMLDialog dialog;

    public void setDialog(AutowireFXMLDialog dialog) {
        this.dialog = dialog;
    }

    @FXML
    public void close() {
        dialog.close();
    }
}

Notice that it extends DialogController and gets a reference to the view injected, which makes it quite easy to dismiss the dialog when the user clicks the close button. You can also access any other Spring beans simply by prefacing them with the @Autowire annotation as we will see tomorrow when we hook up Authentication in the LoginDialog controller.

The next blog will finish out this example including the full source code so you can try it yourself. Until then, here is a small screen capture of the finished error dialog:

error dialog

Categories
JavaFX