Работает ли Vaadin 8 ' Binder:: bindInstanceFields` только со строковыми типами данных?

С помощью фреймворк Vaadin 8 @PropertyId аннотация с Binder::bindInstanceFields конечно короче и слаще, чем писать строку кода для каждой привязки свойства поля.

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 );

но мы получаем исключение, потому что yearOfBirth свойство является целым числом, и этот простой в привязке подход не имеет преобразователя.

тяжелые:

java.ленг.IllegalStateException: тип недвижимости 'Ява.ленг.Целое число "не соответствует типу поля" java.ленг.Струна". Привязка должна быть настроена вручную с помощью конвертера.

значит Binder::bindInstanceFields можно использовать только бобы, сделанные полностью из свойств String тип данных?

есть ли способ указать Converter например StringToIntegerConverter без необходимости детализировать каждую привязку в коде?

3 ответов


посмотреть Vaadin Framework, модель данных Vaadin, привязка данных к формам:

преобразование

вы также можете привязать данные приложения к компоненту поля пользовательского интерфейса, даже если типы не совпадают.

Binder#bindInstanceFields() говорит:

не всегда возможно привязать поле к свойству, поскольку их типы несовместимы. Е. Г. пользовательский конвертер, необходимое для связывания HasValue<String> и Integer свойство (это был бы случай свойства "возраста"). В таком случае IllegalStateException будет брошен если поле не было настроено вручную перед вызовом bindInstanceFields(Object) метод.

[...]: the bindInstanceFields(Object) метод не переопределяет существующие привязки.

[выделено мной.]

Итак, АФАЮ, это должно сработать:

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 );

но он все равно бросает:

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) ...

ты шутишь? Вот что я сделал, не так ли? Я сильно сомневаюсь в "не переопределяет существующие привязки". или, если на самом деле не переопределены, кажется, что они игнорируются в bindInstanceFields(), по крайней мере.

та же конфигурация ручной привязки работает, когда не используется Binder#bindInstanceFields() но подход с индивидуальными привязками для каждого поля.

см. Также нити привязка из целого числа не работа в форуме привязки данных Vaadin Framework и выпуск #8858 Binder.bindInstanceFields () перезаписывает существующие привязки.

решение

менее запутанный, чем ответ @cfrick:

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

    ...
}

обновление

исправить #8998 сделать bindInstanceFields не связывать поля уже связаны с помощью функций.

на исходный код для этого исправления появляется по крайней мере в фреймворк Vaadin 8.1.0 alpha 4 pre-release (и, возможно, другие).


обновление от Basil Bourque...

ваша идея, показанная выше, чтобы использовать Binder::bindInstanceFields после ручной привязки для несовместимого (целого) свойства действительно, похоже, работает для меня. Вы жаловались, что в вашем экспериментальном коде вызов Binder::bindInstanceFields не удалось выполнить задокументированное поведение, когда вызов "не переопределяет существующие привязки".

но это, кажется, работает для меня. Вот пример приложения для Vaadin 8.1.0 alpha 3. Сначала я вручную связываю yearOfBirth собственность. Тогда я использую binder.bindInstanceFields обязать @PropertyId аннотированный name собственность. Поле для обоих свойств отображается заполненным и отвечает на пользовательские изменения.

я что-то пропустил или это работает должным образом, как описано? Если я ошибся, пожалуйста, удалите этот раздел.

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 {
    }
}

и просто Person класса.

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 () +
                " }";
    }
}

до сих пор лучший способ справиться с этим для меня, это написать специальное поле для тип ввода (обратите внимание, что этот "шаблон" также работает для написания композита полы.)

посмотреть полный пример здесь. The IntegerField - поле для обертывания целого числа еще в другом связующем через Боб держать фактическое значение.

// 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)
        )
    }
}

проблема все еще существует в Vaadin 8.4.0, конвертер не распознается и продолжает бросать IllegalStateException. Но есть простой обходной путь к этой ужасной ошибке:

binder.bind(id, obj -> obj.getId() + "", null); //the ValueProvider "getter" could consider that getId returns null