FXML builders detection

When you’re in the custom control business, you need hooks into the main framework to get the controls supported well. One of the features of JavaFX is the FXML UI notation format, it allows the coder to define (parts of) a scene with more ease and readability. FXML already is a pretty flexible and open technology, using reflection and the Java Bean standard (aka setters and getters) to try and automatically setup a control according to the values in the FXML file. But there are always situations that don’t fit.

One of those situation is the JFXtras CalendarTextField control and then specifically the date format properties. In the Java API these properties take instances of DateFormat, either a single one, or even a whole list. FXML at the moment does not know how to convert a string to DateFormat. So the dateFormat and dateFormats attributes below result in errors.

<?import javafx.scene.control.*>
<?import javafx.scene.layout.*?>
<?import jfxtras.labs.scene.control.*?>

<VBox xmlns:fx="http://javafx.com/fxml">
    <children>
        <CalendarTextField dateFormat="yyyy-MM-dd HH:mm:ss" dateFormats="yyyy-MM-dd, yyyy-MM, yyyy"/>
    </children>
</VBox>

The FXML parser discovers that there is no setDateFormat(String) in the control, and no build-in converter to convert the String to DateFormat, and consequentially fails. One possible solution would be to add setters that take a string as a parameter to the control, but that not only results in problems because the setter requires an associated getter, which then conflicts with the existing getDateFormat(), but it also pollutes the Java API with stuff needed for FXML. Another approach could extend the control in a special FXML class, adding all FXML stuff there thus keeping the controls API clean, but that still does not solve the property naming / type conflicts. These approaches simply do not lead to nice implementations.

FXML has thought of that and the FXMLLoader supports a BuilderFactory. This builder factory is asked if it knows of a builder to build a certain class. The value strings in the FXML file are then written to the builder instead of the actual control, and then the builder is asked to “build()” the control. This allows to have setters with the correct name, taking string as the parameter and convert them to the appropriate type. For example the builder for CalendarTextField looks like this:

public class CalendarTextFieldBuilder implements Builder<CalendarTextField>
{
	/** DateFormat */
	public String getDateFormat() { return null; } // dummy, just to make it Java Bean compatible
	public void setDateFormat(String value) { iDateFormat = new SimpleDateFormat(value); }
	private SimpleDateFormat iDateFormat = null;

	/** Locale */
	public String getLocale() { return null; } // dummy, just to make it Java Bean compatible
	public void setLocale(String value) { iLocale = Locale.forLanguageTag(value); }
	private Locale iLocale = null;

	/** PromptText */
	public String getPromptText() { return null; } // dummy, just to make it Java Bean compatible
	public void setPromptText(String value) { iPromptText = value; }
	private String iPromptText = null;

	/** DateFormats */
	public String getDateFormats() { return null; } // dummy, just to make it Java Bean compatible
	public void setDateFormats(String value)
	{
		String[] lParts = value.split(",");
		iDateFormats = FXCollections.observableArrayList();
		for (String lPart : lParts)
		{
			iDateFormats.add( new SimpleDateFormat(lPart.trim()) );
		}
	}
	private ObservableList<DateFormat> iDateFormats = null;

	/**
	 * Implementation of Builder interface
	 */
	@Override
	public CalendarTextField build()
	{
		CalendarTextField lCalendarTextField = new CalendarTextField();
		if (iDateFormat != null) lCalendarTextField.setDateFormat(iDateFormat);
		if (iLocale != null) lCalendarTextField.setLocale(iLocale);
		if (iPromptText != null) lCalendarTextField.setPromptText(iPromptText);
		if (iDateFormats != null) lCalendarTextField.setDateFormats(iDateFormats);
		return lCalendarTextField;
	}
}

Now, this is all nice and well, but if you write a custom builder like the one above, you need to inform the BuilderFactory that it exists. The default JavaFXBuilderFactory can’t do that, it just knows how to build the build-in types, but you can’t register custom builders. One workaround is to write your own BuilderFactory implementation and wrap the default builder factory, like so:

public class CustomBuilderFactory implements BuilderFactory {

    private BuilderFactory baseFactory;

    public CustomBuilderFactory() {
        baseFactory = new JavaFXBuilderFactory();
    }

    @Override
    public Builder<?> getBuilder(Class<?> aClass) {
        if (CalendarTextField.class.equals(aClass)) {
            return new CalendarTextFieldBuilder();
        } else {
            return baseFactory.getBuilder(aClass);
        }
    }
}

This will work, but it can become tedious if you start using multiple jars files containing controls; you need to add code for each control, you need to know how the builder class is called, and new releases may have new controls that are not adopted automatically. It simply is not an elegant solution. But do no despair, Java has a elegant solution readily available: ServiceLoader.

The specifics can be found in the Javadoc, but in essence it comes down to that you can define an interface, and then specify in the META-INF directory which implementations of that interface a jar file provides, these implementations are then automatically detected. JFXtraBuilderFactory uses this technique to automatically detect all builders that are available on the classpath.

So, these are the steps to take:

Implement the jfxtras.fxml.BuilderService interface instead of the javafx.util.Builder interface on all builder implementations. (BuilderService extends the Builder interface and adds one additional method.)
Create a file in your project / jar called “META-INF/services/jfxtras.fxml.BuilderService”
In that file specify the full class name of all builders that you want to make auto-discoverable, each on a single line.
Use the JFXtraBuilderFactory instead of the default, like so: FXMLLoader.load(url, null, new JFXtrasBuilderFactory());
So, this is what needs to be changed to the CalendarTextFieldBuilder file:

public class CalendarTextFieldBuilder implements BuilderService<CalendarTextField>
{
	...

	/**
	 * Implementation of BuilderService interface
	 */
	@Override
	public boolean isBuilderFor(Class<?> clazz)
	{
		return CalendarTextField.class.isAssignableFrom(clazz);
	}
}

And then the file “META-INF/services/jfxtras.fxml.BuilderService” must be created with one line in it:

jfxtras.labs.fxml.CalendarTextFieldBuilder


Done!

This Post Has 2 Comments

  1. Andy

    There is an issue in JavaFX where builders returned by a custom BuilderFactory are expected to implement Map or use traditional setters (with the “set” prefix), rather than use the conventional builder naming scheme (without the “set” prefix).

    However, it’s possible to use the conventional builder naming scheme by having your BuilderFactory wrap your Builder in a Map.

    It turns out you can also just not use a custom BuilderFactory and JavaFXBuilderFactory will find your Builder using reflection as long as it’s in the same package, is named as expected, e.g. MyChartBuilder for MyChart, and has a static create method.

    Note none of this is documented though, and builders are now being deprecated, so it’s unclear as to what you can rely on in the future.

    Please see my comment on the following JavaFX JIRA ticket for more information:

    https://javafx-jira.kenai.com/browse/RT-35522?focusedCommentId=389511&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-389511

    1. tbeernot

      Since basically all I do is marry JavaFX’s BuilderFactory with Java’s ServiceLoader, I don’t think any of the point you make are JFXtras related?

Leave a Reply to tbeernot Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.