• Oct
    • 23
    • 2011

Better Controller Injection

Posted by In Uncategorized
This entry is part 2 of 15 in the series Building JEE applications in JavaFX 2.0

There are two things about our controller injection in our last post that I’m not entirely happy with. The first is that the SampleApp is the one responsible for loading the FXML file and wiring it up to the controller. We have a factory class for our UI (SampleAppFactory) so ideally all the work of creating and wiring up all the GUI elements should be done in here.

The second s that Richard’s post came from his earlier exploration of future directions for FXML and the discussions around that. Some of these features are not available yet, so Richard has found some creative ways to demo the concepts using scripting and the namespace. We probably don’t want to be using this style of coding for commercial apps so until the next release of FXML we need to work with what we’ve got – i.e. we need to have an explicitly named controller class in the FXML and let the loader instantiate our class and do our bindings. This has some drawbacks but using Spring (or Guice’s) annotation based configuration, we can make it work well enough.

For those that want to skip the details and just see the code: http://code.google.com/p/jfxee/source/browse/trunk/jfxspring2

So let’s first revert to the standard way of loading controllers in FXML and ditch the magical namespace and scripting. This standard way is well documented in the official FXML guide: http://download.oracle.com/javafx/2.0/api/javafx/fxml/doc-files/introduction_to_fxml.html

Our controller is now going to be defined and created in the FXML file, but using Spring’s (or Guice’s) annotation based injection we can still inject all the dependencies (so long as we use field injection and not constructor injection). One challenge though will be that both our controller and our view need to be available through the factory. The controller needs to be  exposed in order to get the Person bean injected into it, but our view needs to be exposed so that it can be added to the scene. The complication is that the FXML loader creates both in a single call, so we need to get creative.

There are a lot of ways to solve this problem, but the one that works best both now and for future benefits is to give the controller a reference to its view. Anyone wanting the view, can then just access the controller from the factory and retrieve the view from it.

So if we revert our controller back to the more traditional form, giving it access to its view is a simple case of binding the root node of the FXML to a variable in the controller. To keep life simple we’re not going to bother including the Person name on the button for now – we’ll add this back in later, it just confuses things at this stage. Here’s how it looks:

SampleController.java

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Node;
import org.springframework.beans.factory.annotation.Autowired;

public class SampleController
{
    @FXML private Node view;
    @Autowired private Person person;

    public Node getView()
    {
        return view;
    }

    public Person getPerson()
    {
        return person;
    }

    public void print(ActionEvent event)
    {
        System.out.println("Well done, " + person.getFirstName() + "!");
    }
}

sample.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?language javascript?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<StackPane fx:id="view"
           fx:controller="com.zenjava.jfxspring.SampleController"
           xmlns:fx="http://javafx.com/fxml">
    <children>
        <Button text="Click Me" fx:id="printBtn" onAction="#print" />
    </children>
</StackPane>

Great, we now have a nice, simple, traditional controller. The whole point of this however was to get dependency injection working. Haven’t we lost this now that the controller is being created by the FXMLLoader? Not quite, luckily with Spring’s annotation based configuration we are free to create the controller anyway we want, the injected properties are set only when we return the controller from a factory method marked with @Bean. Let’s update our factory then to use the new controller. At the same time we will also remove the FXML loading from the SampleApp class and move it inside the factory – this was the second problem we wanted to solve.

Here’s how our factory now looks:

import javafx.fxml.FXMLLoader;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;
import java.io.InputStream;

@Configuration
public class SampleAppFactory
{
    @Bean
    public Person person()
    {
        return new Person("Richard");
    }

    @Bean
    public SampleController sampleController() throws IOException
    {
        return (SampleController) loadController("/sample.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();
            }
        }
    }
}

And here’s the start method for our SampleApp class. We’ve been able to remove the FXMLLoading from here – the application has no knowledge of how the views are loaded. If we wanted to change the view to be a normal Java class instead of FXML neither the controller, nor the application would need to be updated. Only the factory, which is responsible for creating the view, would need to be changed as you would expect.


public void start(Stage stage) throws Exception
{
    AnnotationConfigApplicationContext context
            = new AnnotationConfigApplicationContext(SampleAppFactory.class);

    SampleController sampleController = context.getBean(SampleController.class);
    Scene scene = new Scene((Parent) sampleController.getView(), 320, 240);
    scene.getStylesheets().add("fxmlapp.css");
    stage.setScene(scene);
    stage.setTitle("JFX2.0 Sprung");
    stage.show();
}

All in all, a somewhat cleaner setup.

It’s worth noting that there are some definite limitations and disadvantages to using the traditional controller binding approach. All of these have been raised with the JFX team (in JIRA) and several are being actively explored for future releases. In my opinion, although these drawbacks are annoying, at this stage they are worth living with since by using the official controller option we don’t have to use scripting or namespaces, so our app will be easier to maintain and be better supported by RAD tools and will be easier to get help and find docco on.

For the record however the limitations to be aware of are these:

  • The Controller class is specified in two places – the FXML and then cast-to in the factory. If you forget to update your FXML (very easy to do) then you will get a class cast exception in the factory. Ideally we would not have to specify the controller in the FXML at all.
  • You cannot use constructor injection – since the FXML is instantiating the controller, it must have an empty constructor.
  • Callback methods for button clicks must have an ActionEvent as part of the signature. This is easy to forget and will result in a runtime method. With the scripting option you did not have this restriction (the tradeoff being that you could not get the MouseEvent for mouse-style callbacks).
  • You cannot use the same FXML definition with different instances of a controller, i.e. you cannot attach sample.fxml to a controller other than SampleController without duplicating the FXML file.
  • You are limited to a single controller per FXML file, so you couldn’t have sample.fxml trigger callbacks in both SampleController and another controller.
  • You cannot share the same controller across multiple views as the loader will create a new controller for each FXML file. You couldn’t reuse the same SampleController instance used by sample.fxml with another fxml file, there will be two instances of SampleController created.
  • FXML does not support controller base classes. If your controller extends a base class and that base class has an @FXML annotated field on it, it will not get picked up by the FXMLLoader. You must define all @FXML attributes in the actual controller class itself.
There are probably a few other minor limitations but they are the big ones that I can think of.
In the next post we're going to look at what happens when you have a couple of controllers and ways to share information and navigate between them.
Series Navigation<< JavaFX 2.0, FXML and SpringMultiple Controllers with Shared Resources >>

7 Comments

  • Victor Tortorello Neto
    October 29, 2011

    Hey! Do you have an example based on Guice? I’ve written one, but it simply don’t work.

    The start() function is:

    @Override
    public void start(Stage stage) throws Exception {
    injector = Guice.createInjector(new MyBatisModule() {
    @Override
    protected void initialize() {
    install(JdbcHelper.Firebird);
    environmentId(“production”);

    // Some MyBatis properties…
    }

    private Object loadController(String url) throws IOException {
    InputStream fxmlStream = null;

    try {
    fxmlStream = getClass().getResourceAsStream(url);

    FXMLLoader fxmlLoader = new FXMLLoader();
    fxmlLoader.setLocation(getClass().getResource(url));
    fxmlLoader.setBuilderFactory(new JavaFXBuilderFactory());
    fxmlLoader.load(fxmlStream);

    return fxmlLoader.getController();
    } finally {
    if (fxmlStream != null) {
    fxmlStream.close();
    }
    }
    }

    @Provides
    public MainController mainController() throws IOException {
    return (MainController) loadController(“Main.fxml”);
    }

    @Provides
    public SpedController spedController() throws IOException {
    return (SpedController) loadController(“Sped.fxml”);
    }
    });

    stage.setScene(new Scene((Parent) mainController.getViewRoot(), 500, 500));
    stage.show();
    }

    In the MainController I have:

    @Singleton
    public class MainController extends AppController implements Initializable {

    @FXML protected TextField testTxt;

    In the SpedController I have:

    @Inject private MainController mainController;

    @FXML
    protected void test(ActionEvent event) {
    System.out.println(mainController); // This is null ???
    System.out.println(mainController.testTxt);
    System.out.println(“Text -> ” + mainController.testTxt.getText());
    mainController.testTxt.setText(“Test!!!”);
    }

    Can you help me to solve this?

    Thanks,

    Victor (Brazil)

  • zonski Author
    October 30, 2011

    Hi Victor (Brazil),

    I’ve just put up a blog post outlining how to do this http://www.zenjava.com/2011/10/30/better-controller-injection-with-guice

    Feel free to post questions on that topic if you have any.

    Cheers,
    zonski (Australia)

  • Bob Moore
    July 14, 2012

    Hi Zen Java. I’ve learned a lot from this series so far. I am using JavaFX 2.2/Java SDK 1.7.0_u5/NetBeans 7.2 Beta but your source code crashes on the first SampleApp start line
    :
    {code}AnnotationConfigApplicationContext context
    = new AnnotationConfigApplicationContext(SampleAppFactory.class);{code}

    with an exception:

    {code} java.lang.IllegalStateException: Cannot load configuration class: com.zenjava.jfxspring.SampleAppFactory{code}

    The source came directly from the link and built without any error.
    I suspect that maybe the newer JavaFX 2.2 may or the JDK may be causing some incompatablity. When I run debug, break on the line above, and press “step into”, the exception occurs immediatly.

    I’d really like to get this Spring/FXML combination to work to provide a decent multi-scene context without getting too cludgy. I hope there’s an update or new tweek that can solve my immediate problem.

  • Bob Moore
    July 14, 2012

    Supplement to previous comment.
    Another difference in my configuration is that my Spring library is version 3.1.1.
    cglib is 1.1.

    I just tried a direct copy of the link’s code for “Multiple Controllers with Shared Resources” and it threw an exception in the same location. The dump in this case was:

    run:
    Jul 14, 2012 3:13:23 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
    INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@1c4a760: startup date [Sat Jul 14 15:13:23 EDT 2012]; root of context hierarchy
    Exception in Application start method
    java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    at com.javafx.main.Main.launchApp(Main.java:486)
    at com.javafx.main.Main.main(Main.java:638)
    Caused by: java.lang.RuntimeException: Exception in Application start method
    at com.sun.javafx.application.LauncherImpl.launchApplication1(Unknown Source)
    at com.sun.javafx.application.LauncherImpl.access$000(Unknown Source)
    at com.sun.javafx.application.LauncherImpl$1.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:722)
    Caused by: java.lang.IllegalStateException: Cannot load configuration class: com.zenjava.jfxspring.SampleAppFactory
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.enhanceConfigurationClasses(ConfigurationClassPostProcessor.java:346)
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanFactory(ConfigurationClassPostProcessor.java:222)
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:681)
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:620)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:446)
    at org.springframework.context.annotation.AnnotationConfigApplicationContext.(AnnotationConfigApplicationContext.java:73)
    at com.zenjava.jfxspring.SampleApp.start(SampleApp.java:18)
    at com.sun.javafx.application.LauncherImpl$5.run(Unknown Source)
    at com.sun.javafx.application.PlatformImpl$4.run(Unknown Source)
    at com.sun.javafx.application.PlatformImpl$3.run(Unknown Source)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.access$100(Unknown Source)
    at com.sun.glass.ui.win.WinApplication$2$1.run(Unknown Source)
    … 1 more

  • Bob Moore
    July 15, 2012

    I tracked down the problem–a missing class in the SpringSource collection

    org.objectweb.asm.Type

    This is not part of the NetBeans Spring framework plugin. It’s apparently used in the configuration process as an Enum of types. Once included, everything worked well as did the “Multiple Controllers…” project. I also did Guice versions for both of these chapters.

    Thanks again for exploring these topics!

Leave a Comment