What Is the Purpose of @Namedarg Annotation in Javafx 8

What is the purpose of @NamedArg annotation in javaFX 8?

The @NamedArg annotation allows an FXMLLoader to instantiate a class that does not have a zero-argument constructor.

Technical Background:

The FXMLLoader creates objects using reflection. Typically, if you use a tag corresponding to a class with a constructor taking no arguments, an object is created from that class by calling Class.newInstance(), which invokes the no-argument constructor.

If a class is defined only with constructors that take parameters, then this is problematic. The main issue is that the Java Language Specification does not require the names of parameters (to methods or constructors) to be retained at runtime. This means there's no direct, guaranteed, way for the FXMLLoader to determine which parameter has a given name.

To make this concrete, suppose we define a Person class as follows:

package application;

import javafx.beans.NamedArg;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Person {
private final StringProperty firstName ;
private final StringProperty lastName ;

public Person(String firstName, String lastName) {
this.firstName = new SimpleStringProperty(this, "firstName", firstName);
this.lastName = new SimpleStringProperty(this, "lastName", lastName);
}

// methods....

}

In FXML we might try to create a Person as follows:

<Person firstName="Jacob" lastName="Smith"/>

This won't work, because the FXML loader has no guarantee that the runtime representation of the Person class retains the information as to which constructor parameter is firstName and which is lastName.

Historical background

Java 2.2 defined "Builder" classes corresponding to each control. These builder classes follow the standard builder pattern. When the FXMLLoader encounters a tag referencing a class with no zero-argument constructor, it would use the corresponding builder to create the instance.

Unfortunately, the implementation of the builder classes was flawed, and they were deprecated in JavaFX 8, and will be removed in a later version (probably JavaFX 9). This left a problem for the FXMLLoader, which would no longer have builder classes to rely on for instantiating classes with no zero-argument constructor. A real example is the Color class, which has no zero-argument constructor and will have its builder class removed.

@NamedArgs

The fix for this was to introduce an annotation that is used to retain a name of a method (or constructor) argument at runtime. By reflection, we can query the parameter list of a constructor/method, and get the type (but not the name) of each parameter. It is also possible to query each parameter for any annotations, and get the value of those annotations. So the @NamedArg annotation was introduced specifically for the purpose of retaining a name of a parameter at runtime.

Example

For an example, use the Person class we introduced above:

package application;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Person {
private final StringProperty firstName ;
private final StringProperty lastName ;

public Person(String firstName, String lastName) {
this.firstName = new SimpleStringProperty(this, "firstName", firstName);
this.lastName = new SimpleStringProperty(this, "lastName", lastName);
}

public final StringProperty firstNameProperty() { return firstName; }
public final String getFirstName() { return firstNameProperty().get(); }
public final void setFirstName(final String firstName) { firstNameProperty().set(firstName); }
public final StringProperty lastNameProperty() { return lastName; }
public final String getLastName() { return lastNameProperty().get(); }
public final void setLastName(final String lastName) { lastNameProperty().set(lastName); }
}

If you try to load this using FXML:

Person.fxml:

<?xml version="1.0" encoding="UTF-8"?>
<?import application.Person?>
<Person firstName="Jacob" lastName="Smith" xmlns:fx="http://javafx.com/fxml/1" />

Main.java:

package application;

import java.io.IOException;
import javafx.fxml.FXMLLoader;

public class Main {

public static void main(String[] args) throws IOException {
Person person = FXMLLoader.load(Main.class.getResource("Person.fxml"));
System.out.println(person.getFirstName()+" "+person.getLastName());
}
}

then you see an error at runtime:

Caused by: java.lang.NoSuchMethodException: application.Person.<init>()

indicating the FXMLLoader is looking for a constructor taking no arguments (Person.<init>()).

In JavaFX 8, you can fix the problem by specifying the name of the parameters with the @NamedArg annotation:

package application;

import javafx.beans.NamedArg;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Person {
private final StringProperty firstName ;
private final StringProperty lastName ;

public Person(@NamedArg("firstName") String firstName, @NamedArg("lastName") String lastName) {
this.firstName = new SimpleStringProperty(this, "firstName", firstName);
this.lastName = new SimpleStringProperty(this, "lastName", lastName);
}

public final StringProperty firstNameProperty() { return firstName; }
public final String getFirstName() { return firstNameProperty().get(); }
public final void setFirstName(final String firstName) { firstNameProperty().set(firstName); }
public final StringProperty lastNameProperty() { return lastName; }
public final String getLastName() { return lastNameProperty().get(); }
public final void setLastName(final String lastName) { lastNameProperty().set(lastName); }
}

This will allow the FXMLLoader to load the class as required.

Note that you can also fix the issue by defining a builder class, and that this also works in JavaFX 2.0 and later. The JavaFX team decided (probably correctly) that using this approach in a way that didn't suffer from the bugs that existed in the initial implementation of the builders would add too much bloat to the codebase of the framework.

package application;

public class PersonBuilder {

private String firstName ;
private String lastName ;

private PersonBuilder() { }

public static PersonBuilder create() {
return new PersonBuilder();
}

public PersonBuilder firstName(String firstName) {
this.firstName = firstName ;
return this ;
}

public PersonBuilder lastName(String lastName) {
this.lastName = lastName ;
return this ;
}

public Person build() {
return new Person(firstName, lastName);
}
}

Clearly if you are using JavaFX 8, the constructor annotation approach is much less work.

References:

  • Proposal to deprecate builders
  • Tweak request to add constructor annotations
  • Builder pattern
  • FXML documentation (discusses builders, but not @NamedArg)
  • Request to add documentation on @NamedArgs to "Introduction to FXML" document

JavaFX - custom object in FXML file

To allow the FXMLLoader to instantiate classes without no-arg constructors, you need to annotate the constructor parameters. (The reason for this is that in Java parameter names are not guaranteed to be retained at runtime, so a mechanism is needed to reflectively match unordered values to parameters based on names at runtime.) See What is the purpose of @NamedArg annotation in javaFX 8? for more information.

So your Tile and Coordinates classes now look like:

import javafx.beans.NamedArg;
import javafx.scene.shape.Rectangle;

public class Tile extends Rectangle {
private Coordinates coordinates;

public Tile(
@NamedArg("width") double width,
@NamedArg("height") double height,
@NamedArg("coordinates") Coordinates coordinates) {
super(width, height);
this.coordinates = coordinates;
}

public Coordinates getCoordinates() {
return coordinates;
}
}
import java.util.Objects;

import javafx.beans.NamedArg;

public class Coordinates {
private int xCoordinate;
private int yCoordinate;

public Coordinates(
@NamedArg("xCoordinate") int xCoordinate,
@NamedArg("yCoordinate") int yCoordinate) {
this(xCoordinate, yCoordinate, 10, false);
}

public Coordinates(
@NamedArg("xCoordinate") int xCoordinate,
@NamedArg("yCoordinate") int yCoordinate,
@NamedArg("max") int max) {
this(xCoordinate, yCoordinate, max, false);
}

public Coordinates(
@NamedArg("xCoordinate") int xCoordinate,
@NamedArg("yCoordinate") int yCoordinate,
@NamedArg("max") int max,
@NamedArg("allowedZero") boolean allowedZero) {
if (allowedZero) {
if ((xCoordinate >= 0 && yCoordinate >= 0) && (xCoordinate <= max && yCoordinate <= max)) {
this.xCoordinate = xCoordinate;
this.yCoordinate = yCoordinate;
} else {
throw new IllegalArgumentException(String.format("Either X or Y has set to value <= 0, or > %d", max));
}
} else {
if ((xCoordinate > 0 && yCoordinate > 0) && (xCoordinate <= max && yCoordinate <= max)) {
this.xCoordinate = xCoordinate;
this.yCoordinate = yCoordinate;
} else {
throw new IllegalArgumentException(String.format("Either X or Y has set to value <= 0, or > %d", max));
}
}

}

// remaining code unaltered...
}

To use this in FXML, you can do:

<Tile width="100.0" height="100.0">
<coordinates>
<Coordinates xCoordinate="1" yCoordinate="1"/>
</coordinates>
</Tile>

or

<fx:define>
<Coordinates fx:id="tileCoordinates" xCoordinate="1" yCoordinate="1" />
</fx:define>
<Tile width="100.0" height="100.0" coordinates="$tileCoordinates" />

The choice between these is mostly just a choice of style; however note that the latter options gives you the opportunity for multiple Tiles to share the same Coordinates instance (probably not applicable in this particular use case, but in general it can be useful).

Is it possible to refer to converters in JavaFX through FXML?

An FXMLLoader instantiates types via reflection. This means the class must either have a constructor whose parameters are annotated with @NamedArg or a constructor with zero parameters. The @NamedArg solves the issue of parameter names not necessarily being saved in the byte code, as better explained by this answer. The remaining attributes will be set on the object according to the rules specified in this section of Introduction to FXML. To summarize, the FXML attribute/element must have a corresponding property setter, read-only List property, or read-only Map property in the target class.

What this all means for your question is:

  1. It depends on the implementation of StringConverter.

    • It must have a no-argument constructor or have @NamedArg annotated constructor parameters.
  2. It depends on the object you're setting the StringConverter on.

    • It must either have a suitably annotated constructor parameter or the property must have a setter. Otherwise the FXML loader won't know how to set the converter.

Looking at the source code it doesn't look like any of the StringConverter implementations use the @NamedArg annotation. They often do provide a no-argument constructor, however. This will allow you to use them but you may lose the ability to customize them. For instance, when using LocalDateStringConverter you won't be able to customize the format.

That said, you could always create a factory class and use fx:factory in the FXML file.

public class Converters {

public static StringConverter<LocalDate> myAwesomeConverter() {
DateTimeFormatter formatter = ...;
return new LocalDateStringConverter(formatter, formatter);
}

}
<DatePicker>
<converter>
<Converters fx:factory="myAwesomeConverter"/>
</converter>
</DatePicker>

Or if you don't want a custom formatter, you can just use the converter's no-arg constructor.

<DatePicker>
<converter>
<LocalDateStringConverter/>
</converter>
</DatePicker>

Component constructor arguments fxml javafx

Overview

Although OP already selected an answer as best, from the comments it appears that there was some confusion and the selected answer did not outright answer their question and is missing information so I am posting what I believe to be the full/complete answer:

When creating a custom component in JavaFX and you'd like to provide one or more default values to the component upon it's creation, you cannot simply add a parameter to the default constructor as it is used behind the scenes when JavaFX is doing it's FXML loading thing.

To handle this conundrum we're provided with the ability to add parameters to the default constructor only if they are annotated with the @NamedArg("name of parameter here") annotation. This allows one to provide custom components with values at run-time when they are constructed either programmatically or through FXML.

Details and Examples

Custom Component Controller Constructor

public MyCustomComponent(@NamedArg("foo") String foo) { }

Behind the scenes, JavaFX recognizes that the component's constructor now has a parameter annotated with @NamedArg and this will be reflected wherever the component is currently being used. Find or create a FXML document and put your component into the text. You should then be able to set the named argument from the FXML and your IDE's intellisense should support this as well.

Adding the value to the component via FXML

<MyCustomComponent foo="myStringValue"/>

You can set the parameter programmatically:

MyCustomComponent myComp = new MyCustomComponent("myStringValue");

Now, the current selected answer to OP's question does not mention this, but as far as I know, there is one more step you have to take care of or none of this will work. The controller class must have a property that matches the named argument. I believe this is required for initializing the FXML behind the scenes but I could be wrong. Properties in Java, for the most part, look like so:

private StringProperty foo = new SimpleStringProperty();

public String getFoo()
{
return foo.get();
}

public void setFoo(String value)
{
this.foo.set(value);
}

public StringProperty fooProperty()
{
return foo;
}

Conclusion

I wanted to add this here because the previous answer does answer the question but not completely and while it does appear that OP was able to figure out the issue, the answer makes no mention of what OP realized/changed to get it to work because at first it did not solve the issue. I know 100% that this will function as intended.

How to properly add @DefaultProperty annotation to custom class that uses @NamedArg

There are a few options here, but the more simple one is stated by @Slaw in a comment:

Use a String URL in the constructor instead of the Image

So this should work:

public ImageButton(@NamedArg("url") String url, @NamedArg("text") String text) {
setEffect(new ImageInput(new Image(url)));
setText(text);
}

with the FXML like:

<ImageButton fx:id="imageButton" text="Click Me!" url="@icon.png"/>

Let's explore now the use of <image /> combined with @DefaultProperty.

ImageButton control

First of all, let's define our control. For the sake of simplicity (and also because these can't be overridden), I won't include width and height:

public class ImageButton extends Button {

public ImageButton(@NamedArg("image") Image image, @NamedArg("text") String text) {
setImage(image);
setText(text);
}

private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image") {
@Override
protected void invalidated() {
// setGraphic(new ImageView(get()));
setEffect(new ImageInput(get()));
}
};

public final ObjectProperty<Image> imageProperty() {
return image;
}

public final Image getImage() {
return image.get();
}

public final void setImage(Image value) {
image.set(value);
}
}

And:

<ImageButton fx:id="imageButton" text="Click Me!">
<image>
<Image url="@icon.png"/>
</image>
</ImageButton>

will work perfectly fine. However, the purpose is to remove the <image> tag.

DefaultProperty

The theory says you could do:

@DefaultProperty("image")
public class ImageButton extends Button {
...
}

and

<ImageButton fx:id="imageButton" text="Click Me!">
<Image url="@icon.png"/>
</ImageButton>

However, an exception is thrown:

Caused by: javafx.fxml.LoadException: Element does not define a default property.

For more details, why this exception happens, see the linked question.

Basically, as discussed within comments, @DefaultProperty and @NamedArg don't work together: In order to extend the FXML attributes of a given class, @NamedArg provide new constructors to this class, which require the use of ProxyBuilder, so FXMLLoader will use instances of ProxyBuilder instead, and these don't have included the @DefaultProperty annotation.

Builders

Though builder design pattern was used in JavaFX 2.0, and it was deprecated a long time ago (in Java 8, removed in Java 9, link), there are still some builders in the current JavaFX code.

In fact, FXMLLoader makes use of JavaFXBuilderFactory, as default builder factory, that will call this ProxyBuilder if NamedArg annotations are found in the class constructor, among other builders like JavaFXImageBuilder.

There is some description about builders here.

Builder implementation

How can we add our own builder factory? FXMLLoader has a way: setBuilderFactory.

Can we extend JavaFXBuilderFactory? No, it's final, we can't extend it, we have to create one from the scratch.

ImageButtonBuilderFactory

Let's create it:

import javafx.util.Builder;
import javafx.util.BuilderFactory;

public class ImageButtonBuilderFactory implements BuilderFactory {

@Override
public Builder<?> getBuilder(Class<?> type) {
if (type == null) {
throw new NullPointerException();
}
if (type == ImageButton.class) {
return ImageButtonBuilder.create();
}
return null;
}
}

Now let's add the builder:

ImageButtonBuilder

import javafx.scene.image.Image;
import javafx.util.Builder;

import java.util.AbstractMap;
import java.util.HashSet;
import java.util.Set;

public class ImageButtonBuilder extends AbstractMap<String, Object> implements Builder<ImageButton> {

private String text = "";
private Image image;

private ImageButtonBuilder() {}

@Override
public Set<Entry<String, Object>> entrySet() {
return new HashSet<>();
}

public static ImageButtonBuilder create() {
return new ImageButtonBuilder();
}

@Override
public Object put(String key, Object value) {
if (value != null) {
String str = value.toString();

if ("image".equals(key)) {
image = (Image) value;
} else if ("text".equals(key)) {
text = str;
} else {
throw new IllegalArgumentException("Unknown property: " + key);
}
}

return null;
}

@Override
public ImageButton build() {
return new ImageButton(image, text);
}

}

Note that ImageButton is the same class as above (without DefaultProperty annotation).

Using the custom builder

Now we could use our custom builder:

FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("imagebutton.fxml"));
loader.setBuilderFactory(new ImageButtonBuilderFactory());
Parent root = loader.load();
Scene scene = new Scene(root);
...

where the FXML is:

<StackPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11" xmlns:fx="http://javafx.com/fxml/1">
<ImageButton fx:id="imageButton" text="Click Me!">
<image>
<Image url="@icon.png"/>
</image>
</ImageButton>
</StackPane>

If we run this now, it should work. We have verified that our new builder works. If we comment out the setBuilderFactory call, it will work as well (using NamedArg and ProxyBuilder). With the custom builder factory, it won't use ProxyBuilder but our custom builder.

Final step

Finally, we can make use of DefaultProperty to get rid of the <image> tag.

And we'll add the annotation to the builder class, not to the control!

So now we have:


@DefaultProperty("image")
public class ImageButtonBuilder extends AbstractMap<String, Object> implements Builder<ImageButton> {
...
}

and finally we can remove the <image> tag from the FXML file:

<StackPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11" xmlns:fx="http://javafx.com/fxml/1">
<ImageButton fx:id="imageButton" text="Click Me!">
<Image url="@icon.png"/>
</ImageButton>
</StackPane>

and it will work!

Getting a custom class to be recognized as an fxml tag

The error raised due to the FXMLLoader is (by default) tried to instantiate the given class using its a no-arg constructor. If you didn't define, try it.

Instantiating using a constructor having some arguments, you need to use @NamedArgs annotation. See this comprehensive answer for more details.



Related Topics



Leave a reply



Submit