Loading & Saving Business Objects
Form state is loaded from business objects, and can be saved normally after validation. Once bindings are set up, you’re ready to fill the bound UI components with data from your business objects. Changes can be written automatically or manually to business objects.
Reading & Writing Automatically
Writing automatically to business objects when the user makes changes in the UI is usually the most convenient option. You can bind the values directly to an instance by allowing Binder
to save automatically values from the fields.
In the example here, field values are saved automatically:
Binder<Person> binder = new Binder<>();
// Field binding configuration omitted.
// It should be done here.
Person person = new Person("John Doe", 1957);
// Loads the values from the person instance.
// Sets person to be updated when any bound field
// is updated.
binder.setBean(person);
Button saveButton = new Button("Save", event -> {
if (binder.validate().isOk()) {
// Person is always up-to-date as long as
// there are no validation errors.
MyBackend.updatePersonInDatabase(person);
}
});
The validate()
call ensures that bean-level validators are checked when saving automatically.
Warning
|
When you use the setBean() method, the business object instance updates whenever the user changes the value of a bound field. If another part of the application simultaneously uses the same instance, that part could display changes before the user saves. You can prevent this, though, by using a copy of the edited object, or by manually updating the object only when the user saves.
|
Reading Manually
You can use the readBean()
method to read manually values from a business object instance into the UI components.
This example uses the readBean
method:
Person person = new Person("John Doe", 1957);
binder.readBean(person);
The example above assumes that binder
has been configured with a TextField
bound to the name property. The value "John Doe" displays in the field.
Validating & Writing Manually
To prevent displaying multiple errors to the user, validation errors only display after the user has edited each field and submitted (i.e., loaded) the form.
You can explicitly validate the form or try to save the values to a business object, even if the user hasn’t edited a field.
In this example, it’s explicitly validating a form:
// This makes all current validation errors visible.
BinderValidationStatus<Person> status =
binder.validate();
if (status.hasErrors()) {
notifyValidationErrors(status.getValidationErrors());
}
Writing the field values to a business object fails if any of the bound fields contain an invalid value. You can handle invalid values in many ways. The following are some examples:
try {
binder.writeBean(person);
MyBackend.updatePersonInDatabase(person);
} catch (ValidationException e) {
notifyValidationErrors(e.getValidationErrors());
}
boolean saved = binder.writeBeanIfValid(person);
if (saved) {
MyBackend.updatePersonInDatabase(person);
} else {
notifyValidationErrors(binder.validate()
.getValidationErrors());
}
try {
// Writes values from bindings that have changed, as tracked by Binder.
binder.writeChangedBindingsToBean(person);
// Alternatively, this writes values of given set of bindings to the bean.
Collection<Binding<Person, ?>> bindingsToWrite = ...; // define a set of bindings
binder.writeBean(person, bindingsToWrite);
MyBackend.updatePersonInDatabase(person);
} catch (ValidationException e) {
notifyValidationErrors(e.getValidationErrors());
}
binder.withValidator(
p -> p.getYearOfMarriage() > p.getYearOfBirth(),
"Marriage year must be bigger than birth year.");
The withValidator(Validator)
method runs on the bound bean after the values of the bound fields have been updated.
Bean-level validators also run as part of writeBean(Object)
, writeBeanIfValid(Object)
, and validate(Object)
— if the content passes all field-level validators.
Note
|
For bean-level validators, the bean must be updated before the validator runs. If a bean-level validator fails in writeBean(Object) or writeBeanIfValid(Object) , the bean reverts to the state it was in before it returns from the method. Remember to check your getters/setters to ensure there are no unwanted side effects.
|
Writing as a Draft
In addition to other means of writing field values to a business object, there’s also a way to write the bean as a draft. This means that it’s not required for all field validations to pass: bean-level validation isn’t run at all.
binder.writeBeanAsDraft(person); // (1)
binder.writeBeanAsDraft(person, true); // (2)
-
This will write all values which pass conversion and field-level validation to person bean.
-
This will write all values which pass conversion to person bean, ignoring field-level validation.
Tracking Binding Status
Binder
tracks which bindings have been updated by the user and which are in an invalid state. It fires an event when there are binding status changes. You can use this event to enable and disable the form buttons, depending on the current status of the form.
This example enables the Save and Reset buttons when changes are detected:
binder.addStatusChangeListener(event -> {
boolean isValid = event.getBinder().isValid();
boolean hasChanges = event.getBinder().hasChanges();
saveButton.setEnabled(hasChanges && isValid);
resetButton.setEnabled(hasChanges);
});
Tracking Binding Value Changes
Binder
can also track which bindings have changes by comparing them to the initial values of the fields, values that are set and stored when the readBean
is invoked.
By default, any change to a binding’s value marks that binding as changed, even if the initial value is restored for the binding. However, if setChangeDetectionEnabled(true)
is called, the initial value of the binding is compared to the new value.
The hasChanges()
method returns true
only if the new value doesn’t match the initial value. Otherwise, the binding is not marked as changed. This ensures that changedBindings
only includes entries where the current value differs from the initial value.
When this feature is enabled, Binder
will use equals
to check the equality of values. This can be overridden with a custom equality predicate for each binding via withEqualityPredicate(SerializableBiPredicate<TARGET, TARGET> equalityPredicate)
.
When there are status changes, the Binder
fires a StatusChangeEvent
. You can use this event to enable and disable the form buttons, depending on the current status of the Binder
.
The following example enables the Submit button when changes of initial values are detected:
binder.setChangeDetectionEnabled(true);
binder.addValueChangeListener(event ->
submitButton.setEnabled(binder.hasChanges());
);
Person person = new Person("John", "Doe");
binder.readBean(person);
To set the values with which the Binder compares the changes, the readBean
method should be invoked again:
submitButton.addClickListener(event -> {
binder.writeBeanIfValid(person); // stores the bean
binder.readBean(person); // updates initial values
event.getSource().setEnabled(false);
});
Using Java Records with Binder
Instead of using Java Beans, it’s possible to use the Binder
with Java Records. Since Java Records are immutable, only manual reading and writing can be used when the datatype is a record. This means that methods relying on bean datatypes — such as writeBeanAsDraft
, writeBean
, and setBean
— throw an exception when called for a Binder with a record datatype. Additionally, since records can only be read via readBean
, the refreshFields
method clears all of the Binder fields.
For reading a record, the readRecord
method should be used as shown in the example below.
public record Person(String firstName, String lastName) {}
binder.readRecord(new Person("John", "Doe"));
This method throws an exception, if used with beans.
For writing a record, the writeRecord
method should be used. Calling this method applies field and binder level validators, and either returns a new record instance with the current state of the binder, or throws a ValidationException
.
Person editedPerson = binder.writeRecord();
33EBA0BC-10B8-4DB4-922C-71AA8B0A446C