diff options
5 files changed, 259 insertions, 24 deletions
diff --git a/server/src/main/java/com/vaadin/data/BeanBinder.java b/server/src/main/java/com/vaadin/data/BeanBinder.java index dd5790b8b6..64436c26a8 100644 --- a/server/src/main/java/com/vaadin/data/BeanBinder.java +++ b/server/src/main/java/com/vaadin/data/BeanBinder.java @@ -20,7 +20,6 @@ import java.beans.IntrospectionException; import java.beans.PropertyDescriptor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Locale; import java.util.Objects; import java.util.function.Function; import java.util.function.Predicate; @@ -28,8 +27,6 @@ import java.util.function.Predicate; import com.vaadin.data.util.BeanUtil; import com.vaadin.data.util.converter.Converter; import com.vaadin.data.validator.BeanValidator; -import com.vaadin.ui.Component; -import com.vaadin.ui.UI; /** * A {@code Binder} subclass specialized for binding <em>beans</em>: classes @@ -177,7 +174,7 @@ public class BeanBinder<BEAN> extends Binder<BEAN> { if (BeanValidator.checkBeanValidationAvailable()) { finalBinding = finalBinding.withValidator(new BeanValidator( - getBinder().beanType, propertyName, getLocale())); + getBinder().beanType, propertyName, findLocale())); } PropertyDescriptor descriptor = getDescriptor(propertyName); @@ -242,20 +239,6 @@ public class BeanBinder<BEAN> extends Binder<BEAN> { throw new RuntimeException(exception); }); } - - private Locale getLocale() { - Locale l = null; - if (getField() instanceof Component) { - l = ((Component) getField()).getLocale(); - } - if (l == null && UI.getCurrent() != null) { - l = UI.getCurrent().getLocale(); - } - if (l == null) { - l = Locale.getDefault(); - } - return l; - } } private final Class<? extends BEAN> beanType; diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index 0a7e33c556..88549e12c3 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -38,7 +38,10 @@ import com.vaadin.server.ErrorMessage; import com.vaadin.server.UserError; import com.vaadin.shared.Registration; import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.AbstractSingleSelect; +import com.vaadin.ui.Component; import com.vaadin.ui.Label; +import com.vaadin.ui.UI; /** * Connects one or more {@code Field} components to properties of a backing data @@ -497,6 +500,25 @@ public class Binder<BEAN> implements Serializable { } } + /** + * Finds an appropriate locale to be used in conversion and validation. + * + * @return the found locale, not null + */ + protected Locale findLocale() { + Locale l = null; + if (getField() instanceof Component) { + l = ((Component) getField()).getLocale(); + } + if (l == null && UI.getCurrent() != null) { + l = UI.getCurrent().getLocale(); + } + if (l == null) { + l = Locale.getDefault(); + } + return l; + } + private void bind(BEAN bean) { setFieldValue(bean); onValueChange = getField() @@ -521,7 +543,7 @@ public class Binder<BEAN> implements Serializable { private ValidationStatus<TARGET> doValidation() { FIELDVALUE fieldValue = field.getValue(); Result<TARGET> dataValue = converterValidatorChain.convertToModel( - fieldValue, ((AbstractComponent) field).getLocale()); + fieldValue, findLocale()); return new ValidationStatus<>(this, dataValue); } @@ -543,8 +565,7 @@ public class Binder<BEAN> implements Serializable { private FIELDVALUE convertDataToFieldType(BEAN bean) { return converterValidatorChain.convertToPresentation( - getter.apply(bean), - ((AbstractComponent) getField()).getLocale()); + getter.apply(bean), findLocale()); } /** @@ -673,6 +694,8 @@ public class Binder<BEAN> implements Serializable { * @param field * the field to be bound, not null * @return the new binding + * + * @see #bind(HasValue, Function, BiConsumer) */ public <FIELDVALUE> Binding<BEAN, FIELDVALUE, FIELDVALUE> forField( HasValue<FIELDVALUE> field) { @@ -686,6 +709,45 @@ public class Binder<BEAN> implements Serializable { } /** + * Creates a new binding for the given single select component. The returned + * binding may be further configured before invoking + * {@link Binding#bind(Function, BiConsumer) Binding.bind} which completes + * the binding. Until {@code Binding.bind} is called, the binding has no + * effect. + * + * @param <SELECTVALUE> + * the item type of the select + * @param select + * the select to be bound, not null + * @return the new binding + * + * @see #bind(AbstractSingleSelect, Function, BiConsumer) + */ + public <SELECTVALUE> Binding<BEAN, SELECTVALUE, SELECTVALUE> forSelect( + AbstractSingleSelect<SELECTVALUE> select) { + return forField(new HasValue<SELECTVALUE>() { + + @Override + public void setValue(SELECTVALUE value) { + select.setSelectedItem(value); + } + + @Override + public SELECTVALUE getValue() { + return select.getSelectedItem().orElse(null); + } + + @Override + public Registration addValueChangeListener( + ValueChangeListener<? super SELECTVALUE> listener) { + return select.addSelectionListener(e -> listener.accept( + new ValueChange<>(select, getValue(), e + .isUserOriginated()))); + } + }); + } + + /** * Binds a field to a bean property represented by the given getter and * setter pair. The functions are used to update the field value from the * property and to store the field value to the property, respectively. @@ -735,6 +797,59 @@ public class Binder<BEAN> implements Serializable { } /** + * Binds a single select to a bean property represented by the given getter + * and setter pair. The functions are used to update the selection from the + * property and to store the selection to the property, respectively. + * <p> + * Use the {@link #forSelect(AbstractSingleSelect)} method instead if you + * want to further configure the new binding. + * <p> + * When a bean is bound with {@link Binder#bind(BEAN)}, the selected item is + * set to the return value of the given getter. The property value is then + * updated via the given setter whenever the selected item changes. The + * setter may be null; in that case the property value is never updated and + * the binding is said to be <i>read-only</i>. A null property value + * corresponds to no selection and vice versa. + * <p> + * If the Binder is already bound to some item, the newly bound select is + * associated with the corresponding bean property as described above. + * <p> + * The getter and setter can be arbitrary functions, for instance + * implementing user-defined conversion or validation. However, in the most + * basic use case you can simply pass a pair of method references to this + * method as follows: + * + * <pre> + * class Person { + * public enum Title { MR, MS, MISS, MRS, DR, PROF }; + * + * public Title getTitle() { ... } + * public void setTitle(Title title) { ... } + * } + * + * NativeSelect<Title> titleSelect = new NativeSelect<>(); + * titleSelect.setItems(Title.values()); + * binder.bind(titleSelect, Person::getTitle, Person::setTitle); + * </pre> + * + * @param <SELECTVALUE> + * the item type of the select + * @param select + * the select to bind, not null + * @param getter + * the function to get the value of the property to the + * selection, not null + * @param setter + * the function to save the selection to the property or null if + * read-only + */ + public <SELECTVALUE> void bind(AbstractSingleSelect<SELECTVALUE> select, + Function<BEAN, SELECTVALUE> getter, + BiConsumer<BEAN, SELECTVALUE> setter) { + forSelect(select).bind(getter, setter); + } + + /** * Binds the given bean to all the fields added to this Binder. To remove * the binding, call {@link #unbind()}. * <p> diff --git a/server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java b/server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java index 823650f443..81c22cffda 100644 --- a/server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java +++ b/server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java @@ -254,6 +254,17 @@ public abstract class AbstractSingleSelect<T> extends return getSelectionModel().getSelectedItem(); } + /** + * Sets the current selection to the given item or clears selection if given + * {@code null}. + * + * @param item + * the item to select or {@code null} to clear selection + */ + public void setSelectedItem(T item) { + getSelectionModel().setSelectedItem(item); + } + @Override protected AbstractSingleSelectState getState() { return (AbstractSingleSelectState) super.getState(); diff --git a/server/src/test/java/com/vaadin/data/BinderSingleSelectTest.java b/server/src/test/java/com/vaadin/data/BinderSingleSelectTest.java new file mode 100644 index 0000000000..84747483d9 --- /dev/null +++ b/server/src/test/java/com/vaadin/data/BinderSingleSelectTest.java @@ -0,0 +1,109 @@ +/* + * Copyright 2000-2016 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.data; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.tests.data.bean.Person; +import com.vaadin.tests.data.bean.Sex; +import com.vaadin.ui.NativeSelect; + +public class BinderSingleSelectTest extends + BinderTestBase<Binder<Person>, Person> { + + private NativeSelect<Sex> select; + + @Before + public void setUp() { + binder = new Binder<>(); + item = new Person(); + select = new NativeSelect<>(); + select.setItems(Sex.values()); + } + + @Test + public void personBound_bindSelectByShortcut_selectionUpdated() { + item.setSex(Sex.FEMALE); + binder.bind(item); + binder.bind(select, Person::getSex, Person::setSex); + + assertSame(Sex.FEMALE, select.getSelectedItem().orElse(null)); + } + + @Test + public void personBound_bindSelect_selectionUpdated() { + item.setSex(Sex.MALE); + binder.bind(item); + binder.forSelect(select).bind(Person::getSex, Person::setSex); + + assertSame(Sex.MALE, select.getSelectedItem().orElse(null)); + } + + @Test + public void selectBound_bindPersonWithNullSex_selectedItemNotPresent() { + bindSex(); + + assertFalse(select.getSelectedItem().isPresent()); + } + + @Test + public void selectBound_bindPerson_selectionUpdated() { + item.setSex(Sex.FEMALE); + bindSex(); + + assertSame(Sex.FEMALE, select.getSelectedItem().orElse(null)); + } + + @Test + public void bound_setSelection_beanValueUpdated() { + bindSex(); + + select.select(Sex.MALE); + + assertSame(Sex.MALE, item.getSex()); + } + + @Test + public void bound_deselect_beanValueUpdatedToNull() { + item.setSex(Sex.MALE); + bindSex(); + + select.deselect(Sex.MALE); + + assertNull(item.getSex()); + } + + @Test + public void unbound_changeSelection_beanValueNotUpdated() { + item.setSex(Sex.UNKNOWN); + bindSex(); + binder.unbind(); + + select.select(Sex.FEMALE); + + assertSame(Sex.UNKNOWN, item.getSex()); + } + + protected void bindSex() { + binder.forSelect(select).bind(Person::getSex, Person::setSex); + binder.bind(item); + } +} diff --git a/shared/src/main/java/com/vaadin/shared/data/selection/SelectionModel.java b/shared/src/main/java/com/vaadin/shared/data/selection/SelectionModel.java index 115c43013b..8711d6a9c8 100644 --- a/shared/src/main/java/com/vaadin/shared/data/selection/SelectionModel.java +++ b/shared/src/main/java/com/vaadin/shared/data/selection/SelectionModel.java @@ -58,11 +58,28 @@ public interface SelectionModel<T> extends Serializable { public Optional<T> getSelectedItem(); /** + * Sets the current selection to the given item, or clears selection if + * given {@code null}. + * + * @param item + * the item to select or {@code null} to clear selection + */ + public default void setSelectedItem(T item) { + if (item != null) { + select(item); + } else { + deselectAll(); + } + } + + /** * Returns a singleton set of the currently selected item or an empty * set if no item is selected. * * @return a singleton set of the selected item if any, an empty set * otherwise + * + * @see #getSelectedItem() */ @Override default Set<T> getSelectedItems() { @@ -85,13 +102,13 @@ public interface SelectionModel<T> extends Serializable { */ @Override public void select(T item); - } /** - * Returns an immutable set of the currently selected items. + * Returns an immutable set of the currently selected items. It is safe to + * invoke other {@code SelectionModel} methods while iterating over the set. * <p> - * <i>Implementation note:</i> the iteration order of the items in the + * <em>Implementation note:</em> the iteration order of the items in the * returned set should be well-defined and documented by the implementing * class. * |