Vaadin 8 `Binder::bindInstanceFields` 是否仅适用于 String 数据类型?
Does Vaadin 8 `Binder::bindInstanceFields` only work with String data types?
使用 Vaadin 8 @PropertyId
annotation with the 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
属性 是一个整数,而这种简单易行的绑定方法缺少转换器。
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.
这是否意味着 Binder::bindInstanceFields
只能用于完全由 String
数据类型的属性组成的 bean?
有没有一种方法可以指定 Converter
such as StringToIntegerConverter
而不必逐项列出代码中的每个绑定?
到目前为止,对我来说处理这个问题的最好方法是为
输入的类型(注意这个 "pattern" 也适用于编写复合
字段)。
请在此处查看完整示例。 IntegerField
是包装 Integer 的字段
在另一个活页夹中通过一个 bean 来保存实际值。
// 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 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()
说:
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.
[我强调的。]
所以,AFAIU,这应该有效:
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)
...
你在开玩笑吗?我就是这么做的,不是吗?我相当怀疑 "doesn't override existing bindings"。 或者,如果实际上没有被覆盖,它们似乎至少在 bindInstanceFields()
中被忽略了。
相同的手动绑定配置在不使用 Binder#bindInstanceFields()
时有效,但每个字段使用单独绑定的方法。
另请参阅线程 Binding from Integer not working in the Vaadin Framework Data Binding forum and issue #8858 Binder.bindInstanceFields() overwrites existing bindings。
解决方法
比@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 );
...
}
更新
Fix #8998 Make bindInstanceFields not bind fields already bound using functions.
source code for that fix appears at least in 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
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 () +
" }";
}
}
问题在Vaadin 8.4.0 中依然存在,Converter 无法识别并不断抛出IllegalStateException。
但是这个可怕的错误有一个简单的解决方法:
binder.bind(id, obj -> obj.getId() + "", null); //the ValueProvider "getter" could consider that getId returns null
使用 Vaadin 8 @PropertyId
annotation with the 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
属性 是一个整数,而这种简单易行的绑定方法缺少转换器。
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.
这是否意味着 Binder::bindInstanceFields
只能用于完全由 String
数据类型的属性组成的 bean?
有没有一种方法可以指定 Converter
such as StringToIntegerConverter
而不必逐项列出代码中的每个绑定?
到目前为止,对我来说处理这个问题的最好方法是为 输入的类型(注意这个 "pattern" 也适用于编写复合 字段)。
请在此处查看完整示例。 IntegerField
是包装 Integer 的字段
在另一个活页夹中通过一个 bean 来保存实际值。
// 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 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()
说:
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>
andInteger
property (that would be a case of "age" property). In such caseIllegalStateException
will be thrown unless the field has been configured manually before calling thebindInstanceFields(Object)
method.[...]: the
bindInstanceFields(Object)
method doesn't override existing bindings.
[我强调的。]
所以,AFAIU,这应该有效:
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) ...
你在开玩笑吗?我就是这么做的,不是吗?我相当怀疑 "doesn't override existing bindings"。 或者,如果实际上没有被覆盖,它们似乎至少在 bindInstanceFields()
中被忽略了。
相同的手动绑定配置在不使用 Binder#bindInstanceFields()
时有效,但每个字段使用单独绑定的方法。
另请参阅线程 Binding from Integer not working in the Vaadin Framework Data Binding forum and issue #8858 Binder.bindInstanceFields() overwrites existing bindings。
解决方法
比@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 );
...
}
更新
Fix #8998 Make bindInstanceFields not bind fields already bound using functions.
source code for that fix appears at least in 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
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 () +
" }";
}
}
问题在Vaadin 8.4.0 中依然存在,Converter 无法识别并不断抛出IllegalStateException。 但是这个可怕的错误有一个简单的解决方法:
binder.bind(id, obj -> obj.getId() + "", null); //the ValueProvider "getter" could consider that getId returns null