From 2a0c590bfae9b630838ae8f85637543c2a7d160f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Tue, 13 Sep 2016 16:48:39 +0300 Subject: [PATCH] Implement support for binding single-select components Change-Id: I340e802e5c8e6e036b54f81ec46beeb5e1c34329 --- .../main/java/com/vaadin/data/BeanBinder.java | 19 +-- .../src/main/java/com/vaadin/data/Binder.java | 121 +++++++++++++++++- .../com/vaadin/ui/AbstractSingleSelect.java | 11 ++ .../vaadin/data/BinderSingleSelectTest.java | 109 ++++++++++++++++ .../shared/data/selection/SelectionModel.java | 23 +++- 5 files changed, 259 insertions(+), 24 deletions(-) create mode 100644 server/src/test/java/com/vaadin/data/BinderSingleSelectTest.java diff --git a/server/src/main/java/com/vaadin/data/BeanBinder.java b/server/src/main/java/com/vaadin/data/BeanBinder.java index dd5790b8b62..64436c26a8b 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.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 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 beans: classes @@ -177,7 +174,7 @@ public void bind(String propertyName) { if (BeanValidator.checkBeanValidationAvailable()) { finalBinding = finalBinding.withValidator(new BeanValidator( - getBinder().beanType, propertyName, getLocale())); + getBinder().beanType, propertyName, findLocale())); } PropertyDescriptor descriptor = getDescriptor(propertyName); @@ -242,20 +239,6 @@ private Converter createConverter() { 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 beanType; diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index 0a7e33c5560..88549e12c38 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.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 @@ protected void checkUnbound() { } } + /** + * 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 ValidationStatus validate() { private ValidationStatus doValidation() { FIELDVALUE fieldValue = field.getValue(); Result dataValue = converterValidatorChain.convertToModel( - fieldValue, ((AbstractComponent) field).getLocale()); + fieldValue, findLocale()); return new ValidationStatus<>(this, dataValue); } @@ -543,8 +565,7 @@ private void setFieldValue(BEAN bean) { private FIELDVALUE convertDataToFieldType(BEAN bean) { return converterValidatorChain.convertToPresentation( - getter.apply(bean), - ((AbstractComponent) getField()).getLocale()); + getter.apply(bean), findLocale()); } /** @@ -673,6 +694,8 @@ public Optional getBean() { * @param field * the field to be bound, not null * @return the new binding + * + * @see #bind(HasValue, Function, BiConsumer) */ public Binding forField( HasValue field) { @@ -685,6 +708,45 @@ public Binding forField( this::handleValidationStatus); } + /** + * 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 + * 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 Binding forSelect( + AbstractSingleSelect select) { + return forField(new HasValue() { + + @Override + public void setValue(SELECTVALUE value) { + select.setSelectedItem(value); + } + + @Override + public SELECTVALUE getValue() { + return select.getSelectedItem().orElse(null); + } + + @Override + public Registration addValueChangeListener( + ValueChangeListener 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 @@ -734,6 +796,59 @@ public void bind(HasValue field, forField(field).bind(getter, setter); } + /** + * 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. + *

+ * Use the {@link #forSelect(AbstractSingleSelect)} method instead if you + * want to further configure the new binding. + *

+ * 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 read-only. A null property value + * corresponds to no selection and vice versa. + *

+ * If the Binder is already bound to some item, the newly bound select is + * associated with the corresponding bean property as described above. + *

+ * 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: + * + *

+     * class Person {
+     *     public enum Title { MR, MS, MISS, MRS, DR, PROF };
+     *
+     *     public Title getTitle() { ... }
+     *     public void setTitle(Title title) { ... }
+     * }
+     *
+     * NativeSelect 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()}.
diff --git a/server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java b/server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java
index 823650f443d..81c22cffdaa 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 Optional<T> getSelectedItem() {
         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 00000000000..84747483d9b
--- /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 115c43013b4..8711d6a9c80 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
@@ -57,12 +57,29 @@ public interface Single<T> extends SelectionModel<T> {
          */
         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 Multi<T> extends SelectionModel<T> {
          */
         @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.
      *