Blog

FXML and Google Guice

JavaFX comes with the possibility to define your UI using a declarative XML-Syntax instead of writing Java code. To react on user input the XML-Syntax allows you to call back into programm code. A simple example looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<!-- 
	Do not edit this file it is generated by e(fx)clipse from /at/bestsolution/efxclipse/runtime/examples/guice/guice.fxgraph
-->

<?import java.lang.*?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>

<AnchorPane xmlns:fx="http://javafx.com/fxml" fx:controller="at...LoginController"> 
	<children>
		<Label layoutX="10" layoutY="10"> 
			<text>Username</text>
		</Label>
		<TextField fx:id="username" layoutX="80" layoutY="10"/> 
		<Label layoutX="10" layoutY="40"> 
			<text>Password</text>
		</Label>
		<PasswordField fx:id="password" layoutX="80" layoutY="40"/> 
		<Button layoutX="80" layoutY="70" onAction="#login"> 
			<text>Login</text>
		</Button>
		<Label fx:id="message" layoutX="80" layoutY="100"/> 
	</children>
</AnchorPane>

The controller:

package at.bestsolution.efxclipse.runtime.examples.guice.controller;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import at.bestsolution.efxclipse.runtime.examples.guice.service.ILoginService;

import com.google.inject.Inject;

public class LoginController {
	@FXML
	TextField username;
	
	@FXML
	PasswordField password;
	
	@FXML
	Label message;
	
	@FXML
	public void login(ActionEvent event) {
		// do the login
	}
}

And load the FXML-File like this:

FXMLLoader.load(
  GuiceExample.class.getResource("guice.fxml"), 
  null, 
  new JavaFXBuilderFactory()
);

If you are used to dependency injection - like I am since working on Eclipse 4 - you can't imagine to work without it anymore and so you'd like to make guice inject an ILoginService to the controller.

package at.bestsolution.efxclipse.runtime.examples.guice.controller;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import at.bestsolution.efxclipse.runtime.examples.guice.service.ILoginService;

import com.google.inject.Inject;

public class LoginController {
	@Inject
	ILoginService loginService;
	
	@FXML
	TextField username;
	
	@FXML
	PasswordField password;
	
	@FXML
	Label message;
	
	@FXML
	public void login(ActionEvent event) {
		try {
			long id = loginService.login(username.getText(), password.getText());
			message.setStyle("-fx-text-fill: green;");
			message.setText("Logged in as User: " + id);
		} catch (IllegalArgumentException e) {
			message.setStyle("-fx-text-fill: red;");
			message.setText(e.getMessage());
		}
	}
}

Now the question is how to make the FXMLLoader use Guice to inject stuff. The problem is that you can't pass the loader a delegate so that you can make Guice create the controller instance which means you can't use constructor injection but that's a minor problem for me. So we let the FXMLLoader construct the controller and execute the injection afterwards:

public static <N> N loadFXML(Injector injector, URL url) throws IOException {
	FXMLLoader loader = new FXMLLoader();
	loader.setLocation(url);
	loader.setBuilderFactory(new JavaFXBuilderFactory());
	InputStream in = url.openStream();
	N value = (N) loader.load(in);
	in.close();
		
	if( loader.getController() != null ) {
		injector.injectMembers(loader.getController());
	}
		
	injectLoaders(loader, injector);
		
	return value;
}
	
private static void injectLoaders(FXMLLoader parentLoader, Injector injector) {
	for( FXMLLoader l : parentLoader.getIncludes() ) {
		if( l.getController() != null ) {
			injector.injectMembers(l.getController());
			injectLoaders(l, injector);
		}
	}
}

I've packaged up the code in my runtime modules so you can make use of it quite easy. A similar implementation is available for Eclipse-DI but Eclipse-DI requires an OSGi-Runtime whereas the guice one can be used with or without OSGi.

published on
Previous
Find Classloader for a given OSGi-Bundle or Having fun with FXML in OSGi