3

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?

enter image description here

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 + "'" +
                " }";
    }
}
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • You could use a [`StatusChangeListener`](https://vaadin.com/api/8.0.4/com/vaadin/data/StatusChangeListener.html) where you pretty much put the same code as in the _Save_ button click listener, changing the check to `if (binder.isValid())`. Am I missing something? – Morfic Mar 27 '17 at 14:11
  • @Morfic Perhaps. In that listener, how do you make the bound field for the derived/calculated bean property freshen its display on-screen? In the example, after changing the year-of-birth, how do we make `ageField` fetch and display the new age value? – Basil Bourque Mar 27 '17 at 19:07
  • No need, you added yourself a comment in your code (perhaps from the [docs](https://vaadin.com/docs/-/part/framework/datamodel/datamodel-forms.html#datamodel.forms) if i'm not mistaking) that the bean is always up to date when using `binder.setBean` (automatic saving). Thus, if the bean has changed, the listener is triggered, and in the listener the status label text is updated with the latest value. If you think it's worth, I can add a proper answer with code and screenshots tomorrow, as now i'm on my mobile. – Morfic Mar 27 '17 at 20:34
  • @Morfic The bean’s internal values have changed, but the *display* of the calculated `age` property has *not* been updated to reflect the fresh value. That's my Question. – Basil Bourque Mar 27 '17 at 20:41
  • That's what the binder's `StatusChangeListener` for, and that's why i'm asking if i missed something. I have a feeling i may have not entirely understood your question – Morfic Mar 27 '17 at 20:44
  • I'm looking for the binder to automatically update the display of all the properties bound to fields. The calculated property is not automatically updating its display. What I want may not be reasonable. Sounds like you are suggesting replacing my field bound to calculated property with a label or field whose value I set programmatically in the binder’s status change listener. Certainly doable but certainly not automatic. – Basil Bourque Mar 27 '17 at 20:54
  • So I was indeed missing the fact that the `age` field is calculated based on the `year`. The screenshot made me focus only on the label, apologies. As far as I know there's currently no automatic `refresh` on the bound bean. As a workaround if you want to manually refresh in the button click listener, you could call `binder.setBean(this.person)` again. If you want it to happen when the field changes as I was suggesting with the `StatusChangeListener`, you'll need to unregister the listener, call the method, register the listener again, because on set it will get triggered => StackOverflow – Morfic Mar 28 '17 at 08:56

2 Answers2

3

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.

Axel Meier
  • 1,105
  • 2
  • 18
  • 28
  • Thanks for the suggestion. Not only does this help with my particular issue, it also makes a nice practical example of using the `Setter` interface. I had wondered what kind of use-case might make that useful. This is one such case. – Basil Bourque Mar 31 '17 at 03:20
  • Can you edit your Answer to make clear that you are suggesting (a) what I want (auto-update) is not currently possible, and perhaps not reasonable, and (b) You are suggesting that I disconnect the field widget from binding to the bean. Instead I should programmatically update its value when the driving data (yearOfBirth) is modified as you show in your code. That would make the Answer more clear, and I could mark it accepted. – Basil Bourque Apr 01 '17 at 05:30
  • @BasilBourque Thank you for you feedback and your tips how to improve my answer. I've just elaborated on my answer and hope that my answer is better this way. – Axel Meier Apr 03 '17 at 08:35
1

Apparently, the behavior changed with the Binder API and, after a quick glance at the source code, such a refresh feature doesn't seem to be supported. Having said that, the Vaadin guys are considering this case...

I would say the cleanest / safest way is to add a listener on the field itself and not rely on some behavior of this new Binding API. As it is, well... "new", it might be enhanced in the upcoming Vaadin releases. What's more, the update to the UI field might be instantaneous.

EDIT

At a second thought, the minimum you can do is to do a re-read of the bean upon successful validation (which is not much different from what @Morfic suggested by setting the bean again):

if (binder.validate().isOk()) {

  // Force update of the Fields...
  binder.readBean(binder.getBean());

  beanToString.setValue(person.toString());
} else {
  beanToString.setValue("The Person bean has invalid state.");
}
Octavian Theodor
  • 537
  • 6
  • 14