Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does Vaadin 8 `Binder::bindInstanceFields` only work with String data types?

Using the Vaadin 8 @PropertyId annotation with the Binder::bindInstanceFields is certainly shorter and sweeter than writing a line of code for each field-property binding.

Person person;  // `name` is String, `yearOfBirth` is Integer.
…
@PropertyId ( "name" )
final TextField nameField = new TextField ( "Full name:" ); // Bean property.

@PropertyId ( "yearOfBirth" )
final TextField yearOfBirthField = new TextField ( "Year of Birth:" ); // Bean property.
…
// Binding
Binder < Person > binder = new Binder <> ( Person.class );
binder.bindInstanceFields ( this );
binder.setBean ( person );

But we get an Exception thrown because the yearOfBirth property is an Integer, and this easy-does-it binding approach lacks an converter.

SEVERE:

java.lang.IllegalStateException: Property type 'java.lang.Integer' doesn't match the field type 'java.lang.String'. Binding should be configured manually using converter.

Does that mean Binder::bindInstanceFields can be used only a beans made entirely of properties of String data type?

Is there a way to specify a Converter such as StringToIntegerConverter without having to itemize each and every binding in code?

like image 849
Basil Bourque Avatar asked Apr 01 '17 07:04

Basil Bourque


3 Answers

See Vaadin Framework, Vaadin Data Model, Binding Data to Forms:

Conversions

You can also bind application data to a UI field component even though the types do not match.

Binder#bindInstanceFields() says:

It's not always possible to bind a field to a property because their types are incompatible. E.g. custom converter is required to bind HasValue<String> and Integer property (that would be a case of "age" property). In such case IllegalStateException will be thrown unless the field has been configured manually before calling the bindInstanceFields(Object) method.

[...]: the bindInstanceFields(Object) method doesn't override existing bindings.

[Emphases by me.]

So, AFAIU, this should work:

private final TextField siblingsCount = new TextField( "№ of Siblings" );

...

binder.forField( siblingsCount )
    .withNullRepresentation( "" )
    .withConverter(
        new StringToIntegerConverter( Integer.valueOf( 0 ), "integers only" ) )
    .bind( Child::getSiblingsCount, Child::setSiblingsCount );
binder.bindInstanceFields( this );

But it still throws:

java.lang.IllegalStateException: Property type 'java.lang.Integer' doesn't match the field type 'java.lang.String'. Binding should be configured manually using converter. ... at com.vaadin.data.Binder.bindInstanceFields(Binder.java:2135) ...

Are you kidding me? That's what I did, didn't I? I rather doubt about "doesn't override existing bindings". Or, if not actually overridden, it seems they are ignored in bindInstanceFields(), at least.

The same manual binding configuration works when not using Binder#bindInstanceFields() but the approach with individual bindings for each field.

See also the thread Binding from Integer not working in the Vaadin Framework Data Binding forum and issue #8858 Binder.bindInstanceFields() overwrites existing bindings.

Workaround

Less convoluted than @cfrick's answer:

/** Used for workaround for Vaadin issue #8858
 *  'Binder.bindInstanceFields() overwrites existing bindings'
 *  https://github.com/vaadin/framework/issues/8858
 */
private final Map<String, Component> manualBoundComponents = new HashMap<>();
...
// Commented here and declared local below for workaround for Vaadin issue #8858 
//private final TextField siblingsCount = new TextField( "№ of Siblings" );
...

public ChildView() {
    ...

    // Workaround for Vaadin issue #8858
    // Declared local here to prevent processing by Binder#bindInstanceFields() 
    final TextField siblingsCount = new TextField( "№ of Siblings" );
    manualBoundComponents.put( "siblingsCount", siblingsCount );
    binder.forField( siblingsCount )
            .withNullRepresentation( "" )
            .withConverter( new StringToIntegerConverter( Integer.valueOf( 0 ), "integers only" ) )
            .bind( Child::getSiblingsCount, Child::setSiblingsCount );
    binder.bindInstanceFields( this );

    ...

    // Workaround for Vaadin issue #8858  
    addComponent( manualBoundComponents.get( "siblingsCount" ) );
    //addComponent( siblingsCount );

    ...
}

UPDATE

Fix #8998 Make bindInstanceFields not bind fields already bound using functions.

The source code for that fix appears at least in Vaadin 8.1.0 alpha 4 pre-release (and perhaps others).


Update by Basil Bourque…

Your idea, shown above, to use Binder::bindInstanceFields after a manual binding for the non-compatible (Integer) property does indeed seem to be working for me. You complained that in your experimental code the call to Binder::bindInstanceFields failed to follow the documented behavior where the call “doesn't override existing bindings”.

But it seems to work for me. Here is an example app for Vaadin 8.1.0 alpha 3. First I manually bind yearOfBirth property. Then I use binder.bindInstanceFields to bind the @PropertyId annotated name property. The field for both properties appear populated and respond to user-edits.

Did I miss something or is this working properly as documented? If I made a mistake, please delete this section.

package com.example.vaadin.ex_formatinteger;

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;

/**
 * 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;

    //@PropertyId ( "honorific" )
    final TextField honorific = new TextField ( "Honorific:" ); // Bean property.

    //@PropertyId ( "name" )
    final TextField name = new TextField ( "Full name:" ); // Bean property.

    // Manually bind property to field.
    final TextField yearOfBirthField = new TextField ( "Year of Birth:" ); // Bean property.

    final Label spillTheBeanLabel = new Label ( ); // Debug. Not a property.

    @Override
    protected void init ( VaadinRequest vaadinRequest ) {
        this.person = new Person ( "Ms.", "Margaret Hamilton", Integer.valueOf ( 1936 ) );

        Button button = new Button ( "Spill" );
        button.addClickListener ( ( Button.ClickEvent e ) -> {
            spillTheBeanLabel.setValue ( person.toString ( ) );
        } );

        // Binding
        Binder < Person > binder = new Binder <> ( Person.class );
        binder.forField ( this.yearOfBirthField )
              .withNullRepresentation ( "" )
              .withConverter ( new StringToIntegerConverter ( Integer.valueOf ( 0 ), "integers only" ) )
              .bind ( Person:: getYearOfBirth, Person:: setYearOfBirth );
        binder.bindInstanceFields ( this );
        binder.setBean ( person );


        setContent ( new VerticalLayout ( honorific, name, yearOfBirthField, button, spillTheBeanLabel ) );
    }

    @WebServlet ( urlPatterns = "/*", name = "MyUIServlet", asyncSupported = true )
    @VaadinServletConfiguration ( ui = MyUI.class, productionMode = false )
    public static class MyUIServlet extends VaadinServlet {
    }
}

And simple Person class.

package com.example.vaadin.ex_formatinteger;

import java.time.LocalDate;
import java.time.ZoneId;

/**
 * Created by Basil Bourque on 2017-03-31.
 */
public class Person {

    private String honorific ;
    private String name;
    private Integer yearOfBirth;

    // Constructor
    public Person ( String honorificArg , String nameArg , Integer yearOfBirthArg ) {
        this.honorific = honorificArg;
        this.name = nameArg;
        this.yearOfBirth = yearOfBirthArg;
    }

    public String getHonorific ( ) {
        return honorific;
    }

    public void setHonorific ( String honorific ) {
        this.honorific = honorific;
    }

    // name property
    public String getName ( ) {
        return name;
    }

    public void setName ( String nameArg ) {
        this.name = nameArg;
    }

    // yearOfBirth property
    public Integer getYearOfBirth ( ) {
        return yearOfBirth;
    }

    public void setYearOfBirth ( Integer yearOfBirth ) {
        this.yearOfBirth = yearOfBirth;
    }

    // age property. Calculated, so getter only, no setter.
    public Integer getAge ( ) {
        int age = ( LocalDate.now ( ZoneId.systemDefault ( ) )
                             .getYear ( ) - this.yearOfBirth );
        return age;
    }

    @Override
    public String toString ( ) {
        return "Person{ " +
                "honorific='" + this.getHonorific () + '\'' +
                ", name='" + this.getName ()  +
                ", yearOfBirth=" + this.yearOfBirth +
                ", age=" + this.getAge () +
                " }";
    }
}
like image 151
Gerold Broser Avatar answered Nov 10 '22 11:11

Gerold Broser


So far the best way to handle this for me, is to write a dedicated field for the type of input (note that this "pattern" also works for writing composite fields).

See the complete example here. The IntegerField is the field to wrap the Integer yet in another binder via a bean to hold the actual value.

// run with `spring run --watch <file>.groovy`
@Grab('com.vaadin:vaadin-spring-boot-starter:2.0.1')

import com.vaadin.ui.*
import com.vaadin.annotations.*
import com.vaadin.shared.*
import com.vaadin.data.*
import com.vaadin.data.converter.*

class IntegerField extends CustomField<Integer> {
    final Binder<Bean> binder
    final wrappedField = new TextField()
    IntegerField() {
        binder = new BeanValidationBinder<IntegerField.Bean>(IntegerField.Bean)
        binder.forField(wrappedField)
            .withNullRepresentation('')
            .withConverter(new StringToIntegerConverter("Only numbers"))
            .bind('value')
        doSetValue(null)
    }
    IntegerField(String caption) {
        this()
        setCaption(caption)
    }
    Class<Integer> getType() {
        Integer
    }
    com.vaadin.ui.Component initContent() {
        wrappedField
    }
    Registration addValueChangeListener(HasValue.ValueChangeListener<Integer> listener) {
        binder.addValueChangeListener(listener)
    }
    protected void doSetValue(Integer value) {
        binder.bean = new IntegerField.Bean(value)
    }
    Integer getValue() {
       binder.bean?.value
    }
    @groovy.transform.Canonical
    static class Bean {
        Integer value
    }
}

@groovy.transform.Canonical
class Person {
    @javax.validation.constraints.Min(value=18l)
    Integer age
}

class PersonForm extends FormLayout {
    @PropertyId('age')
    IntegerField ageField = new IntegerField("Age")
    PersonForm() {
        addComponents(ageField)
    }
}

@com.vaadin.spring.annotation.SpringUI
@com.vaadin.annotations.Theme("valo")
class MyUI extends UI {
    protected void init(com.vaadin.server.VaadinRequest request) {
        def form = new PersonForm()
        def binder = new BeanValidationBinder<Person>(Person)
        binder.bindInstanceFields(form)
        binder.bean = new Person()
        content = new VerticalLayout(
            form, 
            new Button("Submit", {
                Notification.show(binder.bean.toString())
            } as Button.ClickListener)
        )
    }
}
like image 39
cfrick Avatar answered Nov 10 '22 11:11

cfrick


the problem still exists in Vaadin 8.4.0, Converter is not recognized and keep throwing the IllegalStateException. But there is a simple workaround to this horrible bug:

binder.bind(id, obj -> obj.getId() + "", null); //the ValueProvider "getter" could consider that getId returns null
like image 1
FiruzzZ Avatar answered Nov 10 '22 11:11

FiruzzZ