When using a Binder
in Vaadin 8 with a bean having a read-only calculated property whose value derives from another property, how can I get a TextField
to automatically update the display of the derived calculation result when the driving property has its value changed?
In the following example, how do I get the “age” field to update its calculation when the user changes the “year of birth” field?
Complete working example for Vaadin 8.
package com.example.val;
import com.vaadin.annotations.Theme;
import com.vaadin.annotations.VaadinServletConfiguration;
import com.vaadin.data.Binder;
import com.vaadin.data.converter.StringToIntegerConverter;
import com.vaadin.server.VaadinRequest;
import com.vaadin.server.VaadinServlet;
import com.vaadin.ui.*;
import javax.servlet.annotation.WebServlet;
import java.time.LocalDate;
import java.time.ZoneId;
/**
* This UI is the application entry point. A UI may either represent a browser window
* (or tab) or some part of a html page where a Vaadin application is embedded.
* <p>
* The UI is initialized using {@link #init(VaadinRequest)}. This method is intended to be
* overridden to add component to the user interface and initialize non-component functionality.
*/
@Theme ( "mytheme" )
public class MyUI extends UI {
Person person;
Binder < Person > binder;
@Override
protected void init ( VaadinRequest vaadinRequest ) {
// Data model
this.person = new Person ( "Jean-Luc", 1955 );
// Widgets
final TextField nameField = new TextField ( "Type the person’s name here:" );
final TextField yearOfBirthField = new TextField ( "Type the year of birth here:" );
final TextField ageField = new TextField ( "Approximate age:" );
ageField.setReadOnly ( true );
final Label beanToString = new Label ( );
// Binder
this.binder = new Binder <> ( );
binder.forField ( nameField )
.bind ( Person:: getName, Person:: setName );
binder.forField ( yearOfBirthField )
.withConverter ( new StringToIntegerConverter ( "Input must be Integer" ) )
.bind ( Person:: getYearOfBirth, Person:: setYearOfBirth );
binder.forField ( ageField )
.withConverter ( new StringToIntegerConverter ( "" ) )
.bind ( Person:: getAge, null );
binder.setBean ( this.person );
final Button button = new Button ( "Save" );
button.addClickListener ( event -> {
if ( binder.validate ( ).isOk ( ) ) {
// With `setBear`, the Person object is always up-to-date as long as there are no validation errors.
// MyBackend.updatePersonInDatabase(person);
beanToString.setValue ( this.person.toString ( ) );
} else { // Else bean flunks validation.
beanToString.setValue ( "The Person bean has invalid state." );
}
} );
this.setContent ( new VerticalLayout ( nameField, yearOfBirthField, ageField, button, beanToString ) );
}
@WebServlet ( urlPatterns = "/*", name = "MyUIServlet", asyncSupported = true )
@VaadinServletConfiguration ( ui = MyUI.class, productionMode = false )
public static class MyUIServlet extends VaadinServlet {
}
}
class Person {
private Integer yearOfBirth;
private String name;
public Person ( String name_, Integer yearOfBirth_ ) {
this.name = name_;
this.yearOfBirth = yearOfBirth_;
}
public String getName ( ) {
return name;
}
public void setName ( String name ) {
this.name = name;
}
public Integer getYearOfBirth ( ) {
return yearOfBirth;
}
public void setYearOfBirth ( Integer yearOfBirth ) {
this.yearOfBirth = yearOfBirth;
}
// Read-only property 'age', calculated rather than stored.
public Integer getAge ( ) {
LocalDate today = LocalDate.now ( ZoneId.systemDefault ( ) );
Integer years = ( today.getYear ( ) - this.yearOfBirth );
return years;
}
@Override
public String toString ( ) {
return "Person{ " +
"yearOfBirth=" + yearOfBirth +
", age='" + this.getAge ( ) + "'" +
", name='" + name + "'" +
" }";
}
}
The simplest way I found is to modify the binding code of the yearOfBirthField
in the following way:
binder.forField(yearOfBirthField)
.withConverter(new StringToIntegerConverter("Input must be Integer"))
.bind(Person::getYearOfBirth, (Setter<Person, Integer>) (person1, integer) -> {
person1.setYearOfBirth(integer);
ageField.setValue(person1.getAge().toString());
});
This effectively binds the yearOfBirthField
to the yearOfBirth
property and updates the age
property of the person bean.
Altering the members of the person bean in any way for example by calling person.setYearOfBirth(1977)
has no immediate effect on the fields. The update mechanism of Vaadin works only in one direction. Field modifications are transferred to the bean but not vice versa.
Edit
In Vaadin 8 there is no built-in way to automatically update a Field when a bean attribute has changed. When the yearOfBirthField
's value changed, the new value is propagated to the person bean and the person's yearOfBirth
member is altered automatically through the binding. But afterwards Vaadin does not fetch the current values for all the bound fields of the bean. So the ageField
is not updated and does not reflect the current value.
So to make the ageField
display the updated value you have to update the ageField
programmatically. You could set the whole bean again on the binder which would cause the all the getters being called, but the easiest way is to just set the ageField's value after the yearOfBirth
has been set. This is done in the suggested modification of the setter binding.