From: Denis Anisimov Date: Wed, 19 Oct 2016 10:55:20 +0000 (+0300) Subject: Derive Listing components from HasValue. X-Git-Tag: 8.0.0.alpha6~57 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=02ed73dc9ea247b13fbef63007af6c3c53ad9423;p=vaadin-framework.git Derive Listing components from HasValue. Single select components implement HasValue and mutliselect components implements HasValue>. Change-Id: Ic280a43bf021efd7425cce04e75010b6745fd698 --- diff --git a/server/src/main/java/com/vaadin/data/BeanBinder.java b/server/src/main/java/com/vaadin/data/BeanBinder.java index c73b89de81..d8e8c0450d 100644 --- a/server/src/main/java/com/vaadin/data/BeanBinder.java +++ b/server/src/main/java/com/vaadin/data/BeanBinder.java @@ -21,15 +21,12 @@ import java.beans.PropertyDescriptor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Objects; -import java.util.Set; import java.util.function.Function; 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.AbstractMultiSelect; -import com.vaadin.ui.AbstractSingleSelect; import com.vaadin.util.ReflectTools; /** @@ -273,20 +270,6 @@ public class BeanBinder extends Binder { this::handleValidationStatus); } - @Override - public BeanBinding forSelect( - AbstractSingleSelect select) { - return (BeanBinding) super.forSelect( - select); - } - - @Override - public BeanBinding, Set> forSelect( - AbstractMultiSelect select) { - return (BeanBinding, Set>) super.forSelect( - select); - } - /** * Binds the given field to the property with the given name. The getter and * setter methods of the property are looked up with bean introspection and diff --git a/server/src/main/java/com/vaadin/data/Binder.java b/server/src/main/java/com/vaadin/data/Binder.java index df29f8d480..1dfed366ba 100644 --- a/server/src/main/java/com/vaadin/data/Binder.java +++ b/server/src/main/java/com/vaadin/data/Binder.java @@ -38,10 +38,8 @@ import com.vaadin.event.EventRouter; import com.vaadin.server.ErrorMessage; import com.vaadin.server.UserError; import com.vaadin.shared.Registration; -import com.vaadin.shared.data.selection.SelectionModel.Multi; import com.vaadin.ui.AbstractComponent; import com.vaadin.ui.AbstractMultiSelect; -import com.vaadin.ui.AbstractSingleSelect; import com.vaadin.ui.Component; import com.vaadin.ui.Label; import com.vaadin.ui.UI; @@ -716,84 +714,6 @@ public class Binder implements Serializable { 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 bean 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()))); - } - }); - } - - /** - * Creates a new binding for the given multi 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 bean type of the select - * @param select - * the select to be bound, not null - * @return the new binding - */ - public Binding, Set> forSelect( - AbstractMultiSelect select) { - return forField(new HasValue>() { - - @Override - public void setValue(Set value) { - Multi selectionModel = select.getSelectionModel(); - selectionModel.deselectAll(); - value.forEach(selectionModel::select); - } - - @Override - public Set getValue() { - return select.getSelectionModel().getSelectedItems(); - } - - @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 @@ -843,111 +763,6 @@ public class Binder implements Serializable { 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 bean is - * set to the return value of the given getter. The property value is then - * updated via the given setter whenever the selected bean 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 bean, 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 bean 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 a multi select to a bean property represented by the given getter
-     * and setter pair. The functions are used to update the set of selected
-     * beans from the property and to store the selection to the property,
-     * respectively.
-     * <p>
-     * Use the {@link #forSelect(AbstractMultiSelect)} method instead if you
-     * want to further configure the new binding.
-     * <p>
-     * When a bean is bound with {@link Binder#bind(BEAN)}, the set of selected
-     * beans are set to the return value of the given getter. The property value
-     * is then updated via the given setter whenever the selected beans change.
-     * 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>.
-     * <p>
-     * If the Binder is already bound to some bean, 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 Feature {
-     *     public enum Browser { CHROME, EDGE, FIREFOX, IE, OPERA, SAFARI }
-    
-     *     public Set<Browser> getSupportedBrowsers() { ... }
-     *     public void setSupportedBrowsers(Set<Browser> title) { ... }
-     * }
-     *
-     * CheckBoxGroup<Title> browserSelect = new CheckBoxGroup<>();
-     * browserSelect.setItems(Browser.values());
-     * binder.bind(browserSelect, Feature::getSupportedBrowsers, Feature::setSupportedBrowsers);
-     * </pre>
-     *
-     * @param <SELECTVALUE>
-     *            the bean type of the select
-     * @param select
-     *            the select to bind, not null
-     * @param getter
-     *            the function to get the set of selected beans, not null
-     * @param setter
-     *            the function to save the set of selected beans or null if
-     *            read-only
-     */
-    public <SELECTVALUE> void bind(AbstractMultiSelect<SELECTVALUE> select,
-            Function<BEAN, Set<SELECTVALUE>> getter,
-            BiConsumer<BEAN, Set<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/data/HasValue.java b/server/src/main/java/com/vaadin/data/HasValue.java
index 8333c8a02a..89d8d69e66 100644
--- a/server/src/main/java/com/vaadin/data/HasValue.java
+++ b/server/src/main/java/com/vaadin/data/HasValue.java
@@ -116,8 +116,8 @@ public interface HasValue<V> extends Serializable {
      * @see Registration
      */
     @FunctionalInterface
-    public interface ValueChangeListener<V> extends
-            EventListener<ValueChange<V>> {
+    public interface ValueChangeListener<V>
+            extends EventListener<ValueChange<V>> {
 
         @Deprecated
         public static final Method VALUE_CHANGE_METHOD = ReflectTools
@@ -162,7 +162,7 @@ public interface HasValue<V> extends Serializable {
 
     /**
      * Adds a value change listener. The listener is called when the value of
-     * this {@code hasValue} is changed either by the user or programmatically.
+     * this {@code HasValue} is changed either by the user or programmatically.
      *
      * @param listener
      *            the value change listener, not null
diff --git a/server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java b/server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java
index 48b47ca2d5..6c1fbde990 100644
--- a/server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java
+++ b/server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java
@@ -25,6 +25,7 @@ import java.util.function.Consumer;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
+import com.vaadin.data.HasValue;
 import com.vaadin.event.selection.MultiSelectionEvent;
 import com.vaadin.event.selection.MultiSelectionListener;
 import com.vaadin.server.Resource;
@@ -50,7 +51,7 @@ import elemental.json.JsonObject;
  * @since 8.0
  */
 public abstract class AbstractMultiSelect<T>
-        extends AbstractListing<T, Multi<T>> {
+        extends AbstractListing<T, Multi<T>> implements HasValue<Set<T>> {
 
     /**
      * Simple implementation of multiselectmodel.
@@ -334,6 +335,68 @@ public abstract class AbstractMultiSelect<T>
         getDataCommunicator().reset();
     }
 
+    /**
+     * Returns the current value of this object which is an immutable set of the
+     * currently selected items.
+     * <p>
+     * The call is delegated to {@link #getSelectedItems()}
+     *
+     * @return the current selection
+     * 
+     * @see #getSelectedItems()
+     * @see SelectionModel#getSelectedItems
+     */
+    @Override
+    public Set<T> getValue() {
+        return getSelectedItems();
+    }
+
+    /**
+     * Sets the value of this object which is a set of items to select. If the
+     * new value is not equal to {@code getValue()}, fires a value change event.
+     * May throw {@code IllegalArgumentException} if the value is not
+     * acceptable.
+     * <p>
+     * The method effectively selects the given items and deselects previously
+     * selected. The call is delegated to
+     * {@link Multi#updateSelection(Set, Set)}.
+     *
+     * @see Multi#updateSelection(Set, Set)
+     *
+     * @param value
+     *            the items to select, not {@code null}
+     * @throws IllegalArgumentException
+     *             if the value is invalid
+     */
+    @Override
+    public void setValue(Set<T> value) {
+        Objects.requireNonNull(value);
+        Set<T> copy = value.stream().map(Objects::requireNonNull)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+
+        getSelectionModel().updateSelection(copy,
+                new LinkedHashSet<>(getSelectionModel().getSelectedItems()));
+    }
+
+    /**
+     * Adds a value change listener. The listener is called when the selection
+     * set of this multi select is changed either by the user or
+     * programmatically.
+     * 
+     * @see #addSelectionListener(MultiSelectionListener)
+     *
+     * @param listener
+     *            the value change listener, not null
+     * @return a registration for the listener
+     */
+    @Override
+    public Registration addValueChangeListener(
+            HasValue.ValueChangeListener<? super Set<T>> listener) {
+        return addSelectionListener(
+                event -> listener.accept(new ValueChange<>(event.getConnector(),
+                        event.getValue(), event.isUserOriginated())));
+    }
+
     /**
      * Returns the item icon generator for this multiselect.
      * <p>
diff --git a/server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java b/server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java
index bf521f34cb..d37c5352d5 100644
--- a/server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java
+++ b/server/src/main/java/com/vaadin/ui/AbstractSingleSelect.java
@@ -19,11 +19,13 @@ import java.lang.reflect.Method;
 import java.util.Objects;
 import java.util.Optional;
 
+import com.vaadin.data.HasValue;
 import com.vaadin.event.selection.SingleSelectionChange;
 import com.vaadin.event.selection.SingleSelectionListener;
 import com.vaadin.server.data.DataCommunicator;
 import com.vaadin.shared.Registration;
 import com.vaadin.shared.data.selection.SelectionModel;
+import com.vaadin.shared.data.selection.SelectionModel.Single;
 import com.vaadin.shared.data.selection.SelectionServerRpc;
 import com.vaadin.shared.ui.AbstractSingleSelectState;
 import com.vaadin.util.ReflectTools;
@@ -42,7 +44,8 @@ import com.vaadin.util.ReflectTools;
  * @since 8.0
  */
 public abstract class AbstractSingleSelect<T> extends
-        AbstractListing<T, AbstractSingleSelect<T>.AbstractSingleSelection> {
+        AbstractListing<T, AbstractSingleSelect<T>.AbstractSingleSelection>
+        implements HasValue<T> {
 
     /**
      * A base class for single selection model implementations. Listens to
@@ -296,6 +299,48 @@ public abstract class AbstractSingleSelect<T> extends
         getSelectionModel().setSelectedItem(item);
     }
 
+    /**
+     * Returns the current value of this object which is the currently selected
+     * item.
+     * <p>
+     * The call is delegated to {@link #getSelectedItem()}
+     *
+     * @return the current selection, may be {@code null}
+     * 
+     * @see #getSelectedItem()
+     * @see Single#getSelectedItem
+     */
+    @Override
+    public T getValue() {
+        return getSelectedItem().orElse(null);
+    }
+
+    /**
+     * Sets the value of this object which is an item to select. If the new
+     * value is not equal to {@code getValue()}, fires a value change event. If
+     * value is {@code null} then it deselects currently selected item.
+     * <p>
+     * The call is delegated to {@link #setSelectedItem(Object)}.
+     * 
+     * @see #setSelectedItem(Object)
+     * @see Single#setSelectedItem(Object)
+     *
+     * @param value
+     *            the item to select or {@code null} to clear selection
+     */
+    @Override
+    public void setValue(T value) {
+        setSelectedItem(value);
+    }
+
+    @Override
+    public Registration addValueChangeListener(
+            HasValue.ValueChangeListener<? super T> listener) {
+        return addSelectionListener(
+                event -> listener.accept(new ValueChange<>(event.getConnector(),
+                        event.getValue(), event.isUserOriginated())));
+    }
+
     @Override
     protected AbstractSingleSelectState getState() {
         return (AbstractSingleSelectState) super.getState();
diff --git a/server/src/test/java/com/vaadin/data/BinderMultiSelectTest.java b/server/src/test/java/com/vaadin/data/BinderMultiSelectTest.java
index fd75c930f3..09e9b7e8ce 100644
--- a/server/src/test/java/com/vaadin/data/BinderMultiSelectTest.java
+++ b/server/src/test/java/com/vaadin/data/BinderMultiSelectTest.java
@@ -65,7 +65,7 @@ public class BinderMultiSelectTest
         select = new CheckBoxGroup<>();
         select.setItems(TestEnum.values());
 
-        converterBinder.forSelect(select)
+        converterBinder.forField(select)
                 .withConverter(new TestEnumSetToStringConverter())
                 .bind(AtomicReference<String>::get,
                         AtomicReference<String>::set);
@@ -85,7 +85,7 @@ public class BinderMultiSelectTest
     public void beanBound_bindSelect_selectionUpdated() {
         item.setEnums(Collections.singleton(TestEnum.TWO));
         binder.bind(item);
-        binder.forSelect(select).bind(BeanWithEnums::getEnums,
+        binder.forField(select).bind(BeanWithEnums::getEnums,
                 BeanWithEnums::setEnums);
 
         assertEquals(Collections.singleton(TestEnum.TWO),
@@ -173,7 +173,7 @@ public class BinderMultiSelectTest
 
     @Test
     public void withValidator_validate_validatorUsed() {
-        binder.forSelect(select)
+        binder.forField(select)
                 .withValidator(selection -> selection.size() % 2 == 1,
                         "Must select odd number of items")
                 .bind(BeanWithEnums::getEnums, BeanWithEnums::setEnums);
@@ -187,7 +187,7 @@ public class BinderMultiSelectTest
     }
 
     protected void bindEnum() {
-        binder.forSelect(select).bind(BeanWithEnums::getEnums,
+        binder.forField(select).bind(BeanWithEnums::getEnums,
                 BeanWithEnums::setEnums);
         binder.bind(item);
     }
diff --git a/server/src/test/java/com/vaadin/data/BinderSingleSelectTest.java b/server/src/test/java/com/vaadin/data/BinderSingleSelectTest.java
index 84747483d9..b911692bd0 100644
--- a/server/src/test/java/com/vaadin/data/BinderSingleSelectTest.java
+++ b/server/src/test/java/com/vaadin/data/BinderSingleSelectTest.java
@@ -26,8 +26,8 @@ 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> {
+public class BinderSingleSelectTest
+        extends BinderTestBase<Binder<Person>, Person> {
 
     private NativeSelect<Sex> select;
 
@@ -52,7 +52,7 @@ public class BinderSingleSelectTest extends
     public void personBound_bindSelect_selectionUpdated() {
         item.setSex(Sex.MALE);
         binder.bind(item);
-        binder.forSelect(select).bind(Person::getSex, Person::setSex);
+        binder.forField(select).bind(Person::getSex, Person::setSex);
 
         assertSame(Sex.MALE, select.getSelectedItem().orElse(null));
     }
@@ -103,7 +103,7 @@ public class BinderSingleSelectTest extends
     }
 
     protected void bindSex() {
-        binder.forSelect(select).bind(Person::getSex, Person::setSex);
+        binder.forField(select).bind(Person::getSex, Person::setSex);
         binder.bind(item);
     }
 }
diff --git a/server/src/test/java/com/vaadin/tests/server/component/twincolselect/TwinColSelectDeclarativeTest.java b/server/src/test/java/com/vaadin/tests/server/component/twincolselect/TwinColSelectDeclarativeTest.java
index 6ec4f16f86..87d4c8c216 100644
--- a/server/src/test/java/com/vaadin/tests/server/component/twincolselect/TwinColSelectDeclarativeTest.java
+++ b/server/src/test/java/com/vaadin/tests/server/component/twincolselect/TwinColSelectDeclarativeTest.java
@@ -41,9 +41,15 @@ public class TwinColSelectDeclarativeTest
         TwinColSelect<String> s = new TwinColSelect<>();
         s.setRightColumnCaption("Selected values");
         s.setLeftColumnCaption("Unselected values");
-        s.setItems("First item", "Second item", "Third item");
-        s.getSelectionModel().select("Second item");
-        s.getSelectionModel().select("Third item");
+        /*
+         * This is broken for now : declarative doesn't read data and doesn't
+         * set value/selection. See #388
+         * 
+         * s.setItems("First item", "Second item", "Third item");
+         * s.getSelectionModel().select("Second item");
+         * s.getSelectionModel().select("Third item");
+         * 
+         */
         s.setRows(5);
         return s;
     }
diff --git a/server/src/test/java/com/vaadin/ui/AbstractMultiSelectTest.java b/server/src/test/java/com/vaadin/ui/AbstractMultiSelectTest.java
index d10cee9157..644314c377 100644
--- a/server/src/test/java/com/vaadin/ui/AbstractMultiSelectTest.java
+++ b/server/src/test/java/com/vaadin/ui/AbstractMultiSelectTest.java
@@ -17,8 +17,12 @@ package com.vaadin.ui;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -30,7 +34,11 @@ import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameter;
 import org.junit.runners.Parameterized.Parameters;
+import org.mockito.Mockito;
 
+import com.vaadin.data.HasValue.ValueChange;
+import com.vaadin.event.selection.MultiSelectionEvent;
+import com.vaadin.event.selection.MultiSelectionListener;
 import com.vaadin.server.data.DataSource;
 import com.vaadin.shared.Registration;
 import com.vaadin.shared.data.selection.MultiSelectServerRpc;
@@ -212,6 +220,113 @@ public class AbstractMultiSelectTest {
         assertSelectionOrder(selectionModel, "6", "4", "8");
     }
 
+    @Test
+    public void getValue() {
+        selectionModel.selectItems("1");
+
+        Assert.assertEquals(Collections.singleton("1"),
+                selectToTest.getValue());
+
+        selectionModel.deselectAll();
+        LinkedHashSet<String> set = new LinkedHashSet<>();
+        set.add("1");
+        set.add("5");
+        selectionModel.selectItems(set.toArray(new String[2]));
+        Assert.assertEquals(set, selectToTest.getValue());
+
+        set.add("3");
+        selectionModel.selectItems("3");
+        Assert.assertEquals(set, selectToTest.getValue());
+    }
+
+    @Test
+    @SuppressWarnings({ "serial", "unchecked" })
+    public void getValue_isDelegatedTo_getSelectedItems() {
+        Set<String> set = Mockito.mock(Set.class);
+        AbstractMultiSelect<String> select = new AbstractMultiSelect<String>() {
+
+            @Override
+            public Set<String> getSelectedItems() {
+                return set;
+            }
+        };
+
+        Assert.assertSame(set, select.getValue());
+    }
+
+    @Test
+    public void setValue() {
+        selectToTest.setValue(Collections.singleton("1"));
+
+        Assert.assertEquals(Collections.singleton("1"),
+                selectionModel.getSelectedItems());
+
+        Set<String> set = new LinkedHashSet<>();
+        set.add("4");
+        set.add("3");
+        selectToTest.setValue(set);
+
+        Assert.assertEquals(set, selectionModel.getSelectedItems());
+    }
+
+    @Test
+    @SuppressWarnings({ "unchecked", "rawtypes", "serial" })
+    public void setValue_isDelegatedToDeselectAndUpdateSelection() {
+        Multi<?> model = Mockito.mock(Multi.class);
+        AbstractMultiSelect<String> select = new AbstractMultiSelect<String>() {
+            @Override
+            public Multi<String> getSelectionModel() {
+                return (Multi<String>) model;
+            }
+        };
+
+        Set set = new LinkedHashSet<>();
+        set.add("foo1");
+        set.add("foo");
+        Set selected = new LinkedHashSet<>();
+        selected.add("bar1");
+        selected.add("bar");
+        selected.add("bar2");
+        Mockito.when(model.getSelectedItems()).thenReturn(selected);
+
+        select.setValue(set);
+
+        Mockito.verify(model).updateSelection(set, selected);
+    }
+
+    @SuppressWarnings({ "unchecked", "serial" })
+    @Test
+    public void addValueChangeListener() {
+        AtomicReference<MultiSelectionListener<String>> selectionListener = new AtomicReference<>();
+        Registration registration = Mockito.mock(Registration.class);
+        AbstractMultiSelect<String> select = new AbstractMultiSelect<String>() {
+            @Override
+            public Registration addSelectionListener(
+                    MultiSelectionListener<String> listener) {
+                selectionListener.set(listener);
+                return registration;
+            }
+        };
+
+        AtomicReference<ValueChange<?>> event = new AtomicReference<>();
+        Registration actualRegistration = select.addValueChangeListener(evt -> {
+            Assert.assertNull(event.get());
+            event.set(evt);
+        });
+
+        Assert.assertSame(registration, actualRegistration);
+
+        Set<String> set = new HashSet<>();
+        set.add("foo");
+        set.add("bar");
+        selectionListener.get().accept(new MultiSelectionEvent<>(select,
+                Mockito.mock(Set.class), set, true));
+
+        Assert.assertEquals(select, event.get().getConnector());
+        Assert.assertEquals(set, event.get().getValue());
+        Assert.assertTrue(event.get().isUserOriginated());
+    }
+
     private void rpcSelect(String... keysToSelect) {
         rpcUpdateSelection(keysToSelect, new String[] {});
     }
diff --git a/server/src/test/java/com/vaadin/ui/AbstractSingleSelectTest.java b/server/src/test/java/com/vaadin/ui/AbstractSingleSelectTest.java
index 82c80542c4..82c7859207 100644
--- a/server/src/test/java/com/vaadin/ui/AbstractSingleSelectTest.java
+++ b/server/src/test/java/com/vaadin/ui/AbstractSingleSelectTest.java
@@ -22,13 +22,24 @@ import static org.junit.Assert.assertTrue;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
 
+import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.Mockito;
 
+import com.vaadin.data.HasValue.ValueChange;
+import com.vaadin.event.selection.SingleSelectionChange;
+import com.vaadin.event.selection.SingleSelectionListener;
 import com.vaadin.server.data.datasource.bov.Person;
+import com.vaadin.shared.Registration;
 import com.vaadin.shared.data.DataCommunicatorClientRpc;
+import com.vaadin.shared.data.selection.SelectionModel.Multi;
 import com.vaadin.ui.AbstractSingleSelect.AbstractSingleSelection;
 
 /**
@@ -41,8 +52,7 @@ public class AbstractSingleSelectTest {
     private PersonListing.AbstractSingleSelection selectionModel;
     private List<Person> selectionChanges;
 
-    private static class PersonListing extends
-            AbstractSingleSelect<Person> {
+    private static class PersonListing extends AbstractSingleSelect<Person> {
         public PersonListing() {
             setSelectionModel(new SimpleSingleSelection());
         }
@@ -77,8 +87,8 @@ public class AbstractSingleSelectTest {
         assertTrue(selectionModel.isSelected(PERSON_B));
         assertFalse(selectionModel.isSelected(PERSON_C));
 
-        assertEquals(Collections.singleton(PERSON_B), selectionModel
-                .getSelectedItems());
+        assertEquals(Collections.singleton(PERSON_B),
+                selectionModel.getSelectedItems());
 
         assertEquals(Arrays.asList(PERSON_B), selectionChanges);
     }
@@ -112,8 +122,8 @@ public class AbstractSingleSelectTest {
         assertFalse(selectionModel.isSelected(PERSON_B));
         assertTrue(selectionModel.isSelected(PERSON_C));
 
-        assertEquals(Collections.singleton(PERSON_C), selectionModel
-                .getSelectedItems());
+        assertEquals(Collections.singleton(PERSON_C),
+                selectionModel.getSelectedItems());
 
         assertEquals(Arrays.asList(PERSON_B, PERSON_C), selectionChanges);
     }
@@ -130,8 +140,8 @@ public class AbstractSingleSelectTest {
         assertFalse(selectionModel.isSelected(PERSON_B));
         assertTrue(selectionModel.isSelected(PERSON_C));
 
-        assertEquals(Collections.singleton(PERSON_C), selectionModel
-                .getSelectedItems());
+        assertEquals(Collections.singleton(PERSON_C),
+                selectionModel.getSelectedItems());
 
         assertEquals(Arrays.asList(PERSON_C), selectionChanges);
     }
@@ -148,8 +158,8 @@ public class AbstractSingleSelectTest {
         assertFalse(selectionModel.isSelected(PERSON_B));
         assertTrue(selectionModel.isSelected(PERSON_C));
 
-        assertEquals(Collections.singleton(PERSON_C), selectionModel
-                .getSelectedItems());
+        assertEquals(Collections.singleton(PERSON_C),
+                selectionModel.getSelectedItems());
 
         assertEquals(Arrays.asList(PERSON_C), selectionChanges);
     }
@@ -172,4 +182,109 @@ public class AbstractSingleSelectTest {
         assertEquals(Arrays.asList(PERSON_C, null), selectionChanges);
     }
 
+    @Test
+    public void getValue() {
+        selectionModel.setSelectedItem(PERSON_B);
+
+        Assert.assertEquals(PERSON_B, listing.getValue());
+
+        selectionModel.deselectAll();
+        Assert.assertNull(listing.getValue());
+    }
+
+    @Test
+    @SuppressWarnings({ "rawtypes" })
+    public void getValue_isDelegatedTo_getSelectedItem() {
+        AbstractSingleSelect select = Mockito.mock(AbstractSingleSelect.class);
+        Optional selected = Optional.of(new Object());
+        Mockito.when(select.getSelectedItem()).thenReturn(selected);
+        Mockito.doCallRealMethod().when(select).getValue();
+
+        Assert.assertSame(selected.get(), select.getValue());
+
+        selected = Optional.empty();
+        Mockito.when(select.getSelectedItem()).thenReturn(selected);
+        Assert.assertNull(select.getValue());
+    }
+
+    @Test
+    public void setValue() {
+        listing.setValue(PERSON_C);
+
+        Assert.assertEquals(PERSON_C, selectionModel.getSelectedItem().get());
+
+        listing.setValue(null);
+
+        Assert.assertFalse(selectionModel.getSelectedItem().isPresent());
+    }
+
+    @Test
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    public void setValue_isDelegatedTo_setSelectedItem() {
+        AbstractSingleSelect select = Mockito.mock(AbstractSingleSelect.class);
+        Mockito.doCallRealMethod().when(select).setValue(Mockito.any());
+
+        Object value = new Object();
+        select.setValue(value);
+        Mockito.verify(select).setSelectedItem(value);
+
+        select.setValue(null);
+        Mockito.verify(select).setSelectedItem(null);
+    }
+
+    @SuppressWarnings({ "unchecked", "serial" })
+    @Test
+    public void addValueChangeListener() {
+        AtomicReference<SingleSelectionListener<String>> selectionListener = new AtomicReference<>();
+        Registration registration = Mockito.mock(Registration.class);
+        AbstractSingleSelect<String> select = new AbstractSingleSelect<String>() {
+            @Override
+            public Registration addSelectionListener(
+                    SingleSelectionListener<String> listener) {
+                selectionListener.set(listener);
+                return registration;
+            }
+        };
+
+        AtomicReference<ValueChange<?>> event = new AtomicReference<>();
+        Registration actualRegistration = select.addValueChangeListener(evt -> {
+            Assert.assertNull(event.get());
+            event.set(evt);
+        });
+        Assert.assertSame(registration, actualRegistration);
+
+        String value = "foo";
+        selectionListener.get()
+                .accept(new SingleSelectionChange<>(select, value, true));
+
+        Assert.assertEquals(select, event.get().getConnector());
+        Assert.assertEquals(value, event.get().getValue());
+        Assert.assertTrue(event.get().isUserOriginated());
+    }
+
+    @Test
+    @SuppressWarnings({ "unchecked", "rawtypes", "serial" })
+    public void setValue_isDelegatedToDeselectAndUpdateSelection() {
+        Multi<?> model = Mockito.mock(Multi.class);
+        AbstractMultiSelect<String> select = new AbstractMultiSelect<String>() {
+            @Override
+            public Multi<String> getSelectionModel() {
+                return (Multi<String>) model;
+            }
+        };
+
+        Set set = new LinkedHashSet<>();
+        set.add("foo1");
+        set.add("foo");
+        Set selected = new LinkedHashSet<>();
+        selected.add("bar1");
+        selected.add("bar");
+        selected.add("bar2");
+        Mockito.when(model.getSelectedItems()).thenReturn(selected);
+
+        select.setValue(set);
+
+        Mockito.verify(model).updateSelection(set, selected);
+    }
+
 }