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

3

7

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?

Christabelle answered 1/4, 2017 at 7:20 Comment(2)
Without really having deep information about problem and solutions to me comes idea of IntegerField .. see Viritin vaadin.com/directory#!addon/viritin and more from here github.com/viritin/viritin/blob/master/src/main/java/org/vaadin/…Bibliopole
still not working in Vaadin 8.4.0.. so sadHazy
I
5

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 () +
                " }";
    }
}
Infinitesimal answered 1/4, 2017 at 22:10 Comment(9)
@BasilBourque I'm using 8.0.5. Maybe they fixed this in 8.1.0 alpha 3. I'm going to give it a try.Infinitesimal
@BasilBourque 8.1.0 alpha 3 isn't on Maven Central. Do you know a repository from where I can use it. I'd like to avoid to install-file all the artifacts by hand.Infinitesimal
(A) I'm not sure if 8.1.0 Alpha 3 is different in this regard from 8.0.5 as I've not tried my code with 8.0.5. (B) The alphas and betas are listed on the Vaadin Releases page along with the requisite Maven info. Repository: https://maven.vaadin.com/vaadin-prereleasesChristabelle
@BasilBourque Thanks for (B). That's what I was looking for. I was on the releases page before but didn't scroll down to the pre-releases. Is the rule of thumb not applied any more: If it isn't shown on the first page it's maybe not worth showing it at all. ;)Infinitesimal
@BasilBourque That's funny. I tried your code with 8.1.0 alpha 3 here and it worked. Then I switched back to 8.0.5 and it worked with that, as well. My code doesn't work with any of them without the workaround. The only differences I'm aware of atm are, apart from additional methods, etc.: (1) I don't use @PropertyId (my bean properties' and view fields' names are the same) and (2) I use a H2 DB via Hibernate instead of an inline bean object.Infinitesimal
I believe the @PropertyId is required to automatically match a backing property to a UI component like TextField. There is no way for Vaadin to know which bean property to display in a component. Can you cite any documentation about any intended naming convention for the widgets to match up against the bean property?Christabelle
@BasilBourque PropertyId: "The automatic data binding in Binder relies on a naming convention by default: properties of an item are bound to similarly named field components in given a editor object. If you want to map a property with a different name (ID) to a HasValue, you can use this annotation for the member fields, with the name (ID) of the desired property as the parameter."Infinitesimal
Thanks for that quote. So I take that to mean that renaming the TextField from nameField to name to match the bean property name will cause an automatic binding without the need for @PropertyId annotation. I tried that. It does indeed work. But to verify I renamed the TextField to be nameXXX. That causes an error, but about the fact that no remaining instance fields are available to be mapped as bindings. So I altered my example and reposted to this Answer. I added the property honorific (Dr., Ms., Mr., etc.). The implicit field-property binding does indeed work.Christabelle
Vaadin's Binding API in 8 is completely broken in my opinion... I had to add 500 loc in a ExtendedBinder to fulfill our requirements.Nagey
L
2

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)
        )
    }
}
Lynnett answered 1/4, 2017 at 16:50 Comment(9)
This is Groovy. The question is tagged with java explicitely. Not every Java developer is able to convert from one to the other easily.Infinitesimal
@GeroldBroser I'd say, that the problem is clearly a Vaadin API problem and therefor is pretty much agnostic of Java, Groovy, or any other JVM language. I'd even argue, that adding [java] here, was just done to catch the eye of a wider audience.Lynnett
Actually, I added java tag to activate Stack Overflow’s automatic code formatting. I agree Java code is easier to read for most of us including me. But I can usually fake my way through Groovy syntax.Christabelle
@BasilBourque You can activate syntax highlighting with <!-- language: ... --> or <!-- language-all: ... -->, too.Infinitesimal
@GeroldBroser Do you know if there is any policy or Meta-StackOverflow decision about using the tag versus language-all to activate syntax formatting? I remember reading directions to include the tag, but I'm not sure if that is official policy.Christabelle
@BasilBourque I really don't know but my 5 cents are: if this is really an official policy and they don't disable it completely because they don't want to break old Q/As they shouldn't mention it on the help page, at least. Furthermore, they could display a message while editing in realtime or at saving, like many others, if such is detected in a Q/A. If it is there, people use it. That's a simple equation. Not only at SE but in life in general.Infinitesimal
@GeroldBroser Hey Geri! The idiom talks about 2 cents.Infinitesimal
@GeroldBroser Thanks for pointing that out. Apparently I had my generous day.Infinitesimal
@Lynnett Good point. You have +12 reps since yesterday. ;)Infinitesimal
H
1

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
Hazy answered 11/5, 2018 at 2:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.