Issue
I have two methods in my JavaFX-code for creating TextFields with Bindings. I have to bind ReadOnlyProperty
or StringBinding
. So I created two methods with different method signatures but with the same code in the block.
How can I simplify my code (with generics?) to handle all the different Properties (String, Long, Object etc.)?
createTextField(knkFile.idProperty().asString(), 1, 0); // ReadOnlyIntegerProperty
createTextField(knkFile.dateProperty().asString(), 1, 1); // ReadOnlyObjectProperty
createTextField(knkFile.fileNameProperty(), 1, 2); // ReadOnlyStringProperty
createTextField(knkFile.lastModifiedProperty().asString(), 1, 5); // ReadOnlyLongProperty
private void createTextField(ReadOnlyProperty property, int column, int row) {
TextField textField = new TextField();
textField.textProperty().bind(property);
this.add(textField, column, row); // add to GridPane
}
private void createTextField(StringBinding binding, int column, int row) {
TextField textField = new TextField();
textField.textProperty().bind(binding);
this.add(textField, column, row);
}
Edit: own answer (deprecated - see accepted answer)
I just saw that StringBinding
and ReadOnlyProperty
implement ObservableValue
. So I only need this method now:
private void createTextField(ObservableValue property, int column, int row) {
TextField textField = new TextField();
textField.textProperty().bind(property);
this.add(textField, column, row);
}
Solution
Solution: Use StringExpression
All of the values passed to your createTextField
methods are StringExpressions, so you can create a single method that takes a StringExpression as a parameter.
You can write your example method like this:
private void createTextField(
StringExpression property,
int column,
int row
) {
TextField textField = new TextField();
textField.textProperty().bind(property);
form.add(textField, column, row);
}
Using a StringExpression
is preferable to using an ObservableValue
as defined in your updated question.
Explanation
A text field must be bound to an ObservableValue which is a string (type ObservableValue<? extends String>
).
If you don't use generics in the type specification for your parameter and just use a raw type (without the part including <>
), then you lose type safety, so compile-time type checks are not made. For example, the compiler would allow you to pass an Integer-based rather than String-based ObservableValue into the method. If you did that, on execution, you would get a runtime error due to a type mismatch because an Integer cannot be directly treated as a String.
A StringExpression
implements ObservableValue<String>
, so that means that a text field can bind to it directly and type safety at compile time is retained.
Executable example
import javafx.application.Application;
import javafx.beans.binding.StringExpression;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;
import java.time.Instant;
public class BindSample extends Application {
private final FileModel fileModel = new FileModel(
1, Instant.now(), "test.txt", Instant.now().toEpochMilli()
);
private final GridPane form = new GridPane();
@Override
public void start(Stage stage) {
form.setHgap(5);
form.setVgap(5);
form.setPadding(new Insets(10));
form.addColumn(0,
new Label("Id"),
new Label("Date"),
new Label("FileName"),
new Label(),
new Label(),
new Label("Last Modified")
);
createTextField(fileModel.idProperty().asString(), 1, 0); // ReadOnlyIntegerProperty
createTextField(fileModel.dateProperty().asString(), 1, 1); // ReadOnlyObjectProperty
createTextField(fileModel.fileNameProperty(), 1, 2); // ReadOnlyStringProperty
createTextField(fileModel.lastModifiedProperty().asString(), 1, 5); // ReadOnlyLongProperty
stage.setScene(new Scene(form));
stage.show();
}
private void createTextField(
StringExpression property,
int column,
int row
) {
TextField textField = new TextField();
textField.textProperty().bind(property);
form.add(textField, column, row);
}
public static void main(String[] args) {
launch();
}
private static class FileModel {
private final ReadOnlyIntegerWrapper id;
private final ReadOnlyObjectWrapper<Instant> date;
private final ReadOnlyStringWrapper fileName;
private final ReadOnlyLongWrapper lastModified;
public FileModel(int id, Instant date, String fileName, long lastModified) {
this.id = new ReadOnlyIntegerWrapper(id);
this.date = new ReadOnlyObjectWrapper<>(date);
this.fileName = new ReadOnlyStringWrapper(fileName);
this.lastModified = new ReadOnlyLongWrapper(lastModified);
}
public int getId() {
return id.get();
}
public ReadOnlyIntegerProperty idProperty() {
return id.getReadOnlyProperty();
}
public Instant getDate() {
return date.get();
}
public ReadOnlyObjectProperty<Instant> dateProperty() {
return date.getReadOnlyProperty();
}
public String getFileName() {
return fileName.get();
}
public ReadOnlyStringProperty fileNameProperty() {
return fileName.getReadOnlyProperty();
}
public long getLastModified() {
return lastModified.get();
}
public ReadOnlyLongProperty lastModifiedProperty() {
return lastModified.getReadOnlyProperty();
}
}
}
Minor implementation notes
The example uses ReadOnlyPropertys when demonstrating the binding to match your question sample code. But for this simple example, if the model object were immutable, then a Record without Bindings could be used instead. This can simplify the logic if you know the data never changes.
In the example, I use an
Instant
to represent a date as a precise moment in time, but you could use aZonedDateTime
for better date representation (orLocalDateTime
orLocalDate
if the ambiguity was OK), see Java date representations if you want to understand this.
Answered By - jewelsea
Answer Checked By - Clifford M. (JavaFixing Volunteer)