• Oct
    • 26
    • 2011

Generic Controllers

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

Now that we’ve created a few of these Controllers we’re seeing a bit of similarity between them. They all have an attached view and up until now we’ve duplicated the same code in all of them for dealing with these.

Let’s take this opportunity then to clean things up a little and define a generic controller framework. This base framework will be very simple for now, but as we start to head into more complicated areas of JEE application building (such as threading, communicating with servers, and navigating between controllers) this Controller setup is going to provide us with a strong foundation to build on.

Our first step is to create a Controller interface that all controller instances will implement. The only thing we need to know from our controllers at this stage is their attached view, so the interface will provide for this:

import javafx.scene.Node;

public interface Controller
{
    Node getView();
}

Next we can create an abstract base class and we can move the duplicated code from our controllers into this so it’s defined in one nice spot:

import javafx.scene.Node;

public abstract class AbstractController implements Controller
{
    // Problem! This value will not be set by the FXMLLoader since the
    // loader does not detect annotated fields in base classes
    @FXML private Node view;

    public Node getView()
    {
        return view;
    }
}

This looks pretty good, but unfortunately the current version of FXML does not map fields that are defined in a base class of the controller, even if that field is annotated with @FXML. There is a JIRA issue for this, but for the time being we have to find a work around.

Our cheat here is to manually inject our view into our controller when loading it in the factory. Luckily we can get hold of the root of the view from the FXMLLoader and then we can just pass this to our Controller in code.

Our factory load method now looks like this:

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

And we’ve had to expose a setView() method on our controller Interface, and implement this in our AbstractController class. This is not ideal since we don’t want anyone actually ever calling this setView() method, but it does get the job done.

Controller.java

import javafx.scene.Node;

public interface Controller
{
    Node getView();

    /**
     * This method is needed only to allow the loadController() method in SampleAppFactory to
     * inject the view into this controller. Ideally FXML would allow us to specify @FXML on
     * in AbstractController on the 'view' field and this method would not be needed.
     */
    void setView(Node view);
}

AbstractController.java

import javafx.scene.Node;

public abstract class AbstractController implements Controller
{
    private Node view;

    public Node getView()
    {
        return view;
    }

    public void setView(Node view)
    {
        this.view = view;
    }
}

It’s a simple case now to refactor all of our controllers to use these base classes. For more details see the full source code for this post at: http://code.google.com/p/jfxee/source/browse/trunk/jfxee5/

Series Navigation<< Views within Views, Controllers within ControllersClient Server with JavaFX 2 and Hessian (+Guice +FXML) >>

7 Comments

  • macalase
    November 11, 2011

    Hey, some great posts again!

    I thought I posted this but can’t seem to find it. I have been playing around with JavaFX last week or 2, and considering using it for my next project. Was working with swing previously.

    I have loaded the jfxee5 project into Netbeans and everything worked as described above

    I tried this:
    1. I created a new file test.fxml and it only has simple GridPane inside, also new controller for it which extends your Controller
    3. I wanted to use test.fxml as an include inside the Main.fxml

    However when trying to load the app with
    inside Main.fxml I get the following error

    javafx.fxml.LoadException: Base location is undefined.
    at javafx.fxml.FXMLLoader$IncludeElement.constructValue(FXMLLoader.java:692)
    at javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:371)
    at javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:1522)
    at javafx.fxml.FXMLLoader.load(FXMLLoader.java:1410)
    at tellerfx.TellerFXFactory.loadController(TellerFXFactory.java:54)
    at tellerfx.TellerFXFactory.tellerFXController(TellerFXFactory.java:24)
    at tellerfx.TellerFXFactory$$EnhancerByCGLIB$$1710e705.CGLIB$tellerFXController$0()
    at tellerfx.TellerFXFactory$$EnhancerByCGLIB$$1710e705$$FastClassByCGLIB$$cf0c15a6.invoke()
    at net.sf.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:215)
    at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:210)
    at tellerfx.TellerFXFactory$$EnhancerByCGLIB$$1710e705.tellerFXController()
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:145)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:570)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:983)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:879)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:485)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:456)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:293)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:290)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:192)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:585)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:895)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:425)
    at org.springframework.context.annotation.AnnotationConfigApplicationContext.(AnnotationConfigApplicationContext.java:65)
    at tellerfx.TellerFX.start(TellerFX.java:22)
    at com.sun.javafx.application.LauncherImpl$5.run(LauncherImpl.java:298)
    at com.sun.javafx.application.PlatformImpl$4.run(PlatformImpl.java:136)
    at com.sun.javafx.application.PlatformImpl$3.run(PlatformImpl.java:108)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.access$100(WinApplication.java:29)
    at com.sun.glass.ui.win.WinApplication$2$1.run(WinApplication.java:62)
    at java.lang.Thread.run(Thread.java:662)
    Nov 11, 2011 2:04:25 PM org.springframework.beans.factory.support.DefaultSingletonBeanRegistry destroySingletons
    INFO: Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@9e5c73: defining beans [org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,tellerFXFactory,tellerFXController,logonController,depositController,customerSearchController,menuController]; root of factory hierarchy
    Exception in Application start method
    java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at com.javafx.main.Main.launchApp(Main.java:354)
    at com.javafx.main.Main.main(Main.java:435)
    Caused by: java.lang.RuntimeException: Exception in Application start method
    at com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:378)
    at com.sun.javafx.application.LauncherImpl.access$000(LauncherImpl.java:27)
    at com.sun.javafx.application.LauncherImpl$1.run(LauncherImpl.java:97)
    at java.lang.Thread.run(Thread.java:662)
    Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘tellerFXController’ defined in class tellerfx.TellerFXFactory: Instantiation of bean failed; nested exception is org.springframework.beans.factory.BeanDefinitionStoreException: Factory method [public tellerfx.TellerFXController tellerfx.TellerFXFactory.tellerFXController() throws java.io.IOException] threw exception; nested exception is java.io.IOException: javafx.fxml.LoadException: Base location is undefined.
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:581)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:983)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:879)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:485)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:456)
    at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:293)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:290)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:192)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:585)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:895)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:425)
    at org.springframework.context.annotation.AnnotationConfigApplicationContext.(AnnotationConfigApplicationContext.java:65)
    at tellerfx.TellerFX.start(TellerFX.java:22)
    at com.sun.javafx.application.LauncherImpl$5.run(LauncherImpl.java:298)
    at com.sun.javafx.application.PlatformImpl$4.run(PlatformImpl.java:136)
    at com.sun.javafx.application.PlatformImpl$3.run(PlatformImpl.java:108)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.access$100(WinApplication.java:29)
    at com.sun.glass.ui.win.WinApplication$2$1.run(WinApplication.java:62)
    … 1 more
    Caused by: org.springframework.beans.factory.BeanDefinitionStoreException: Factory method [public tellerfx.TellerFXController tellerfx.TellerFXFactory.tellerFXController() throws java.io.IOException] threw exception; nested exception is java.io.IOException: javafx.fxml.LoadException: Base location is undefined.
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:157)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:570)
    … 20 more
    Caused by: java.io.IOException: javafx.fxml.LoadException: Base location is undefined.
    at tellerfx.TellerFXFactory.loadController(TellerFXFactory.java:60)
    at tellerfx.TellerFXFactory.tellerFXController(TellerFXFactory.java:24)
    at tellerfx.TellerFXFactory$$EnhancerByCGLIB$$1710e705.CGLIB$tellerFXController$0()
    at tellerfx.TellerFXFactory$$EnhancerByCGLIB$$1710e705$$FastClassByCGLIB$$cf0c15a6.invoke()
    at net.sf.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:215)
    at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:210)
    at tellerfx.TellerFXFactory$$EnhancerByCGLIB$$1710e705.tellerFXController()
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:145)
    … 21 more
    Caused by: javafx.fxml.LoadException: Base location is undefined.
    at javafx.fxml.FXMLLoader$IncludeElement.constructValue(FXMLLoader.java:692)
    at javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:371)
    at javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:1522)
    at javafx.fxml.FXMLLoader.load(FXMLLoader.java:1410)
    at tellerfx.TellerFXFactory.loadController(TellerFXFactory.java:54)

    Any idea if includes work properly with this approach, or why I am getting that error?

    Thanks,
    Mac

  • macalase
    November 11, 2011

    Ok, I found the problem, sorry to bother you. FYI, to fix it, I add the setLocation call in loadController and my version looks like this

    protected Controller loadController(String url) throws IOException {
    InputStream fxmlStream = null;
    try {
    fxmlStream = getClass().getResourceAsStream(url);
    FXMLLoader loader = new FXMLLoader();
    loader.setResources(ResourceBundle.getBundle(“labels”));
    loader.setLocation(getClass().getResource(url));
    Node view = (Node) loader.load(fxmlStream);
    Controller controller = (Controller) loader.getController();
    controller.setView(view);
    return controller;
    } catch (Exception e ){
    e.printStackTrace();
    throw new IOException(e);
    }finally {
    if (fxmlStream != null) {
    fxmlStream.close();
    }
    }
    }

  • Ha Ta
    November 14, 2011

    I have some problems with the controller class, please give me some advice about them:)

    I have the first scene as Login.fxml with LoginController, when the login is success the user will be injected by @Autowire to Spring Container and then the second scene is display.

    In the second scene as Profile.fxml with ProfileController that implement Initializable interface for post-processing.
    by initialize method.

    The problem is the ProfileController with initialize method is loaded by Spring Container when application start up, in that time, the user information is not created by LoginController, so that I can’t get the user information by @Autowire from Spring Container to set value for control ui in the ProfileController. The post-processing with initialize method is only loaded 1 times by Spring Container and I can not initial data for ProfileController when the scene display.

    If you have any suggestion for me about post-processing in controller class with Spring framework, please let me know!

    Thanks so much

  • zonski Author
    November 14, 2011

    Good point – the Initializable.initialize() method is called when the FXMLLoader finishes. Your FXML bindings will be set but Spring will not have had a chance yet to inject the @Autowired fields.

    What you can do is create your own activation method (instead of using Initializable) and annotate this with the @PostConstruct annotation. Spring will then call this method after it has autowired anything.

    Something like:

    @PostConstruct
    private void init() // you can call the method anything you like
    {
    … your custom initialisation code here …
    }

  • EHE
    December 25, 2011

    Thanks for your sharing, I have practice these examples it is quite a good design pattern to integrate Spring DI and JavaFX. But I met the issue when working with Image. The problem is, when I use the FXMLLoader object’s load method to load the FXML file from inputstream, the Image can not be instantiated.
    I am not sure if you have met the same issue, when I try the static load method of FXMLLoader to load the fxml file, the image can correctly be instantiated without any exception but if I use this method, how can I get the controller from FXML?
    Maybe you can have a try to work with Image and give me some hint on how to resolve this issue.

    Thanks & Best Regards!

Leave a Comment