]> source.dussan.org Git - vaadin-framework.git/commitdiff
Implement support for binding multi-select components
authorLeif Åstrand <leif@vaadin.com>
Wed, 14 Sep 2016 10:24:28 +0000 (13:24 +0300)
committerVaadin Code Review <review@vaadin.com>
Wed, 14 Sep 2016 12:25:36 +0000 (12:25 +0000)
Also updates ComponentTest.getRpcProxy to use an approach that doesn't
require the component to be attached to a UI.

Change-Id: Iab4603a7818cd0fd2a3410660b90a2a839fb8a76

server/src/main/java/com/vaadin/data/Binder.java
server/src/main/java/com/vaadin/event/selection/MultiSelectionEvent.java
server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java [new file with mode: 0644]
server/src/main/java/com/vaadin/ui/CheckBoxGroup.java
server/src/test/java/com/vaadin/data/BinderMultiSelectTest.java [new file with mode: 0644]
server/src/test/java/com/vaadin/tests/data/bean/BeanWithEnums.java [new file with mode: 0644]
server/src/test/java/com/vaadin/ui/CheckBoxGroupTest.java
server/src/test/java/com/vaadin/ui/ComponentTest.java

index 88549e12c383e4d5e90b4e37c49bd3197d7952ab..d2114e81208c9324ea4dc2808a813be6d556187c 100644 (file)
@@ -37,7 +37,9 @@ import com.vaadin.data.util.converter.StringToIntegerConverter;
 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;
@@ -747,6 +749,45 @@ public class Binder<BEAN> implements Serializable {
         });
     }
 
+    /**
+     * 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 <SELECTVALUE>
+     *            the item type of the select
+     * @param select
+     *            the select to be bound, not null
+     * @return the new binding
+     */
+    public <SELECTVALUE> Binding<BEAN, Set<SELECTVALUE>, Set<SELECTVALUE>> forSelect(
+            AbstractMultiSelect<SELECTVALUE> select) {
+        return forField(new HasValue<Set<SELECTVALUE>>() {
+
+            @Override
+            public void setValue(Set<SELECTVALUE> value) {
+                Multi<SELECTVALUE> selectionModel = select.getSelectionModel();
+                selectionModel.deselectAll();
+                value.forEach(selectionModel::select);
+            }
+
+            @Override
+            public Set<SELECTVALUE> getValue() {
+                return select.getSelectionModel().getSelectedItems();
+            }
+
+            @Override
+            public Registration addValueChangeListener(
+                    ValueChangeListener<? super Set<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
@@ -849,6 +890,58 @@ public class Binder<BEAN> implements Serializable {
         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
+     * items 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
+     * items are set to the return value of the given getter. The property value
+     * is then updated via the given setter whenever the selected items 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>.
+     * <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 Feature {
+     *     public enum Browser { CHROME, EDGE, FIREFOX, IE, OPERA, SAFARI }
+
+     *     public Set&lt;Browser> getSupportedBrowsers() { ... }
+     *     public void setSupportedBrowsers(Set&lt;Browser> title) { ... }
+     * }
+     *
+     * CheckBoxGroup<Title> browserSelect = new CheckBoxGroup<>();
+     * browserSelect.setItems(Browser.values());
+     * binder.bind(browserSelect, Feature::getSupportedBrowsers, Feature::setSupportedBrowsers);
+     * </pre>
+     *
+     * @param <SELECTVALUE>
+     *            the item type of the select
+     * @param select
+     *            the select to bind, not null
+     * @param getter
+     *            the function to get the set of selected items, not null
+     * @param setter
+     *            the function to save the set of selected items 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()}.
index 81c8478721abbb69cb8be20d5e5be7742c7b636a..1de911f3538ff707ee385f4b1d69bb9581c5b6f4 100644 (file)
@@ -18,7 +18,7 @@ package com.vaadin.event.selection;
 import java.util.Collections;
 import java.util.Set;
 
-import com.vaadin.event.ConnectorEvent;
+import com.vaadin.data.HasValue.ValueChange;
 import com.vaadin.shared.data.selection.SelectionModel;
 import com.vaadin.ui.AbstractListing;
 
@@ -33,10 +33,9 @@ import com.vaadin.ui.AbstractListing;
  * @param <T>
  *            the data type of the selection model
  */
-public class MultiSelectionEvent<T> extends ConnectorEvent {
+public class MultiSelectionEvent<T> extends ValueChange<Set<T>> {
 
-    private Set<T> oldSelection;
-    private Set<T> newSelection;
+    private final Set<T> oldSelection;
 
     /**
      * Creates a new event.
@@ -47,13 +46,16 @@ public class MultiSelectionEvent<T> extends ConnectorEvent {
      *            the old set of selected items
      * @param newSelection
      *            the new set of selected items
+     * @param userOriginated
+     *            {@code true} if this event originates from the client,
+     *            {@code false} otherwise.
      */
     public MultiSelectionEvent(
             AbstractListing<T, SelectionModel.Multi<T>> source,
-            Set<T> oldSelection, Set<T> newSelection) {
-        super(source);
+            Set<T> oldSelection, Set<T> newSelection, boolean userOriginated) {
+        super(source, Collections.unmodifiableSet(newSelection),
+                userOriginated);
         this.oldSelection = oldSelection;
-        this.newSelection = newSelection;
     }
 
     /**
@@ -62,7 +64,7 @@ public class MultiSelectionEvent<T> extends ConnectorEvent {
      * @return a set of items selected after the selection was changed
      */
     public Set<T> getNewSelection() {
-        return Collections.unmodifiableSet(newSelection);
+        return getValue();
     }
 
     /**
@@ -73,5 +75,4 @@ public class MultiSelectionEvent<T> extends ConnectorEvent {
     public Set<T> getOldSelection() {
         return Collections.unmodifiableSet(oldSelection);
     }
-
 }
diff --git a/server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java b/server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java
new file mode 100644 (file)
index 0000000..1d17841
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * 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.ui;
+
+import java.lang.reflect.Method;
+import java.util.Objects;
+
+import com.vaadin.event.selection.MultiSelectionEvent;
+import com.vaadin.event.selection.MultiSelectionListener;
+import com.vaadin.shared.Registration;
+import com.vaadin.shared.data.selection.SelectionModel.Multi;
+import com.vaadin.util.ReflectTools;
+
+/**
+ * Base class for listing components that allow selecting multiple items.
+ *
+ * @param <T>
+ *            item type
+ * @author Vaadin Ltd
+ * @since 8.0
+ */
+public abstract class AbstractMultiSelect<T>
+        extends AbstractListing<T, Multi<T>> {
+
+    @Deprecated
+    private static final Method SELECTION_CHANGE_METHOD = ReflectTools
+            .findMethod(MultiSelectionListener.class, "accept",
+                    MultiSelectionEvent.class);
+
+    /**
+     * Creates a new multi select with an empty data source.
+     */
+    protected AbstractMultiSelect() {
+        super();
+    }
+
+    /**
+     * Adds a selection listener that will be called when the selection is
+     * changed either by the user or programmatically.
+     *
+     * @param listener
+     *            the value change listener, not <code>null</code>
+     * @return a registration for the listener
+     */
+    public Registration addSelectionListener(
+            MultiSelectionListener<T> listener) {
+        Objects.requireNonNull(listener, "listener cannot be null");
+        addListener(MultiSelectionEvent.class, listener,
+                SELECTION_CHANGE_METHOD);
+        return () -> removeListener(MultiSelectionEvent.class, listener);
+    }
+
+}
\ No newline at end of file
index 8687aa2b92b2b9832cbfc2aecba71bfa3f55da5a..8525dc6cbb7f13b319dfd0a3057015ef02256e52 100644 (file)
@@ -16,7 +16,6 @@
 
 package com.vaadin.ui;
 
-import java.lang.reflect.Method;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedHashSet;
@@ -29,17 +28,14 @@ import java.util.function.Predicate;
 
 import com.vaadin.data.Listing;
 import com.vaadin.event.selection.MultiSelectionEvent;
-import com.vaadin.event.selection.MultiSelectionListener;
 import com.vaadin.server.Resource;
 import com.vaadin.server.ResourceReference;
 import com.vaadin.server.data.DataGenerator;
 import com.vaadin.server.data.DataSource;
-import com.vaadin.shared.Registration;
 import com.vaadin.shared.data.selection.SelectionModel;
 import com.vaadin.shared.data.selection.SelectionServerRpc;
 import com.vaadin.shared.ui.optiongroup.CheckBoxGroupConstants;
 import com.vaadin.shared.ui.optiongroup.CheckBoxGroupState;
-import com.vaadin.util.ReflectTools;
 
 import elemental.json.JsonObject;
 
@@ -52,8 +48,7 @@ import elemental.json.JsonObject;
  * @author Vaadin Ltd
  * @since 8.0
  */
-public class CheckBoxGroup<T>
-        extends AbstractListing<T, SelectionModel.Multi<T>> {
+public class CheckBoxGroup<T> extends AbstractMultiSelect<T> {
 
     private final class SimpleMultiSelectModel
             implements SelectionModel.Multi<T> {
@@ -62,11 +57,16 @@ public class CheckBoxGroup<T>
 
         @Override
         public void select(T item) {
+            // Not user originated
+            select(item, false);
+        }
+
+        private void select(T item, boolean userOriginated) {
             if (selection.contains(item)) {
                 return;
             }
 
-            updateSelection(set -> set.add(item));
+            updateSelection(set -> set.add(item), userOriginated);
         }
 
         @Override
@@ -76,11 +76,16 @@ public class CheckBoxGroup<T>
 
         @Override
         public void deselect(T item) {
+            // Not user originated
+            deselect(item, false);
+        }
+
+        private void deselect(T item, boolean userOriginated) {
             if (!selection.contains(item)) {
                 return;
             }
 
-            updateSelection(set -> set.remove(item));
+            updateSelection(set -> set.remove(item), userOriginated);
         }
 
         @Override
@@ -89,16 +94,17 @@ public class CheckBoxGroup<T>
                 return;
             }
 
-            updateSelection(Set::clear);
+            updateSelection(Set::clear, false);
         }
 
-        private void updateSelection(Consumer<Set<T>> handler) {
+        private void updateSelection(Consumer<Set<T>> handler,
+                boolean userOriginated) {
             LinkedHashSet<T> oldSelection = new LinkedHashSet<>(selection);
             handler.accept(selection);
             LinkedHashSet<T> newSelection = new LinkedHashSet<>(selection);
 
             fireEvent(new MultiSelectionEvent<>(CheckBoxGroup.this,
-                    oldSelection, newSelection));
+                    oldSelection, newSelection, userOriginated));
 
             getDataCommunicator().reset();
         }
@@ -109,11 +115,6 @@ public class CheckBoxGroup<T>
         }
     }
 
-    @Deprecated
-    private static final Method SELECTION_CHANGE_METHOD = ReflectTools
-            .findMethod(MultiSelectionListener.class, "accept",
-                    MultiSelectionEvent.class);
-
     private Function<T, Resource> itemIconProvider = item -> null;
 
     private Function<T, String> itemCaptionProvider = String::valueOf;
@@ -172,14 +173,14 @@ public class CheckBoxGroup<T>
 
             @Override
             public void select(String key) {
-                getItemForSelectionChange(key)
-                        .ifPresent(getSelectionModel()::select);
+                getItemForSelectionChange(key).ifPresent(
+                        item -> getSelectionModel().select(item, true));
             }
 
             @Override
             public void deselect(String key) {
-                getItemForSelectionChange(key)
-                        .ifPresent(getSelectionModel()::deselect);
+                getItemForSelectionChange(key).ifPresent(
+                        item -> getSelectionModel().deselect(item, true));
             }
 
             private Optional<T> getItemForSelectionChange(String key) {
@@ -190,6 +191,11 @@ public class CheckBoxGroup<T>
 
                 return Optional.of(item);
             }
+
+            private SimpleMultiSelectModel getSelectionModel() {
+                return (SimpleMultiSelectModel) CheckBoxGroup.this
+                        .getSelectionModel();
+            }
         });
 
         addDataGenerator(new DataGenerator<T>() {
@@ -330,20 +336,4 @@ public class CheckBoxGroup<T>
         Objects.nonNull(itemEnabledProvider);
         this.itemEnabledProvider = itemEnabledProvider;
     }
-
-    /**
-     * Adds a selection listener that will be called when the selection is
-     * changed either by the user or programmatically.
-     *
-     * @param listener
-     *            the value change listener, not <code>null</code>
-     * @return a registration for the listener
-     */
-    public Registration addSelectionListener(
-            MultiSelectionListener<T> listener) {
-        Objects.requireNonNull(listener, "listener cannot be null");
-        addListener(MultiSelectionEvent.class, listener,
-                SELECTION_CHANGE_METHOD);
-        return () -> removeListener(MultiSelectionEvent.class, listener);
-    }
 }
diff --git a/server/src/test/java/com/vaadin/data/BinderMultiSelectTest.java b/server/src/test/java/com/vaadin/data/BinderMultiSelectTest.java
new file mode 100644 (file)
index 0000000..93f4ccd
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * 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.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collections;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.tests.data.bean.BeanWithEnums;
+import com.vaadin.tests.data.bean.TestEnum;
+import com.vaadin.ui.CheckBoxGroup;
+
+public class BinderMultiSelectTest
+        extends BinderTestBase<Binder<BeanWithEnums>, BeanWithEnums> {
+    public class TestEnumSetToStringConverter
+            implements Converter<Set<TestEnum>, String> {
+        @Override
+        public Result<String> convertToModel(Set<TestEnum> value,
+                Locale locale) {
+            return Result.ok(value.stream().map(TestEnum::name)
+                    .collect(Collectors.joining(",")));
+        }
+
+        @Override
+        public Set<TestEnum> convertToPresentation(String value,
+                Locale locale) {
+            return Stream.of(value.split(","))
+                    .filter(string -> !string.isEmpty()).map(TestEnum::valueOf)
+                    .collect(Collectors.toSet());
+        }
+    }
+
+    private Binder<AtomicReference<String>> converterBinder = new Binder<>();
+
+    private CheckBoxGroup<TestEnum> select;
+
+    @Before
+    public void setUp() {
+        binder = new Binder<>();
+        item = new BeanWithEnums();
+        select = new CheckBoxGroup<>();
+        select.setItems(TestEnum.values());
+
+        converterBinder.forSelect(select)
+                .withConverter(new TestEnumSetToStringConverter())
+                .bind(AtomicReference<String>::get,
+                        AtomicReference<String>::set);
+    }
+
+    @Test
+    public void beanBound_bindSelectByShortcut_selectionUpdated() {
+        item.setEnums(Collections.singleton(TestEnum.ONE));
+        binder.bind(item);
+        binder.bind(select, BeanWithEnums::getEnums, BeanWithEnums::setEnums);
+
+        assertEquals(Collections.singleton(TestEnum.ONE),
+                select.getSelectedItems());
+    }
+
+    @Test
+    public void beanBound_bindSelect_selectionUpdated() {
+        item.setEnums(Collections.singleton(TestEnum.TWO));
+        binder.bind(item);
+        binder.forSelect(select).bind(BeanWithEnums::getEnums,
+                BeanWithEnums::setEnums);
+
+        assertEquals(Collections.singleton(TestEnum.TWO),
+                select.getSelectedItems());
+    }
+
+    @Test
+    public void selectBound_bindBeanWithoutEnums_selectedItemNotPresent() {
+        bindEnum();
+
+        assertTrue(select.getSelectedItems().isEmpty());
+    }
+
+    @Test
+    public void selectBound_bindBean_selectionUpdated() {
+        item.setEnums(Collections.singleton(TestEnum.ONE));
+        bindEnum();
+
+        assertEquals(Collections.singleton(TestEnum.ONE),
+                select.getSelectedItems());
+    }
+
+    @Test
+    public void bound_setSelection_beanValueUpdated() {
+        bindEnum();
+
+        select.select(TestEnum.TWO);
+
+        assertEquals(Collections.singleton(TestEnum.TWO), item.getEnums());
+    }
+
+    @Test
+    public void bound_deselect_beanValueUpdatedToNull() {
+        item.setEnums(Collections.singleton(TestEnum.ONE));
+        bindEnum();
+
+        select.deselect(TestEnum.ONE);
+
+        assertTrue(item.getEnums().isEmpty());
+    }
+
+    @Test
+    public void unbound_changeSelection_beanValueNotUpdated() {
+        item.setEnums(Collections.singleton(TestEnum.ONE));
+        bindEnum();
+        binder.unbind();
+
+        select.select(TestEnum.TWO);
+
+        assertEquals(Collections.singleton(TestEnum.ONE), item.getEnums());
+    }
+
+    @Test
+    public void withConverter_load_selectUpdated() {
+        converterBinder.load(new AtomicReference<>("TWO"));
+
+        assertEquals(Collections.singleton(TestEnum.TWO),
+                select.getSelectionModel().getSelectedItems());
+    }
+
+    @Test
+    public void withConverter_save_referenceUpdated() {
+        select.select(TestEnum.ONE);
+        select.select(TestEnum.TWO);
+
+        AtomicReference<String> reference = new AtomicReference<>("");
+        converterBinder.saveIfValid(reference);
+
+        assertEquals("ONE,TWO", reference.get());
+    }
+
+    @Test
+    public void withValidator_validate_validatorUsed() {
+        binder.forSelect(select)
+                .withValidator(selection -> selection.size() % 2 == 1,
+                        "Must select odd number of items")
+                .bind(BeanWithEnums::getEnums, BeanWithEnums::setEnums);
+        binder.bind(item);
+
+        assertFalse(binder.validate().isOk());
+
+        select.select(TestEnum.TWO);
+
+        assertTrue(binder.validate().isOk());
+    }
+
+    protected void bindEnum() {
+        binder.forSelect(select).bind(BeanWithEnums::getEnums,
+                BeanWithEnums::setEnums);
+        binder.bind(item);
+    }
+}
diff --git a/server/src/test/java/com/vaadin/tests/data/bean/BeanWithEnums.java b/server/src/test/java/com/vaadin/tests/data/bean/BeanWithEnums.java
new file mode 100644 (file)
index 0000000..b0b612c
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * 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.tests.data.bean;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class BeanWithEnums {
+    private Set<TestEnum> enums = new HashSet<>();
+
+    public Set<TestEnum> getEnums() {
+        return enums;
+    }
+
+    public void setEnums(Set<TestEnum> enums) {
+        this.enums = enums;
+    }
+}
index 0ad1801e8b606915a3a5a66c44291bcb9a6d1109..192dcb3d5258337c7a99eec13db80bdd4ad96749 100644 (file)
@@ -17,22 +17,31 @@ package com.vaadin.ui;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.junit.Assert;
+import org.junit.Before;
 import org.junit.Test;
 
 import com.vaadin.server.data.DataSource;
 import com.vaadin.shared.data.selection.SelectionModel.Multi;
+import com.vaadin.shared.data.selection.SelectionServerRpc;
 
 public class CheckBoxGroupTest {
-    @Test
-    public void stableSelectionOrder() {
-        CheckBoxGroup<String> checkBoxGroup = new CheckBoxGroup<>();
+    private CheckBoxGroup<String> checkBoxGroup;
+    private Multi<String> selectionModel;
+
+    @Before
+    public void setUp() {
+        checkBoxGroup = new CheckBoxGroup<>();
         // Intentional deviation from upcoming selection order
         checkBoxGroup
                 .setDataSource(DataSource.create("Third", "Second", "First"));
-        Multi<String> selectionModel = checkBoxGroup.getSelectionModel();
+        selectionModel = checkBoxGroup.getSelectionModel();
+    }
 
+    @Test
+    public void stableSelectionOrder() {
         selectionModel.select("First");
         selectionModel.select("Second");
         selectionModel.select("Third");
@@ -46,6 +55,48 @@ public class CheckBoxGroupTest {
         assertSelectionOrder(selectionModel, "Second", "Third", "First");
     }
 
+    @Test
+    public void apiSelectionChange_notUserOriginated() {
+        AtomicInteger listenerCount = new AtomicInteger(0);
+
+        checkBoxGroup.addSelectionListener(event -> {
+            listenerCount.incrementAndGet();
+            Assert.assertFalse(event.isUserOriginated());
+        });
+
+        checkBoxGroup.select("First");
+        checkBoxGroup.select("Second");
+
+        checkBoxGroup.deselect("Second");
+        checkBoxGroup.getSelectionModel().deselectAll();
+
+        Assert.assertEquals(4, listenerCount.get());
+    }
+
+    @Test
+    public void rpcSelectionChange_userOriginated() {
+        AtomicInteger listenerCount = new AtomicInteger(0);
+
+        checkBoxGroup.addSelectionListener(event -> {
+            listenerCount.incrementAndGet();
+            Assert.assertTrue(event.isUserOriginated());
+        });
+
+        SelectionServerRpc rpc = ComponentTest.getRpcProxy(checkBoxGroup,
+                SelectionServerRpc.class);
+
+        rpc.select(getItemKey("First"));
+        rpc.select(getItemKey("Second"));
+        rpc.deselect(getItemKey("Second"));
+
+        Assert.assertEquals(3, listenerCount.get());
+    }
+
+    private String getItemKey(String dataObject) {
+        return checkBoxGroup.getDataCommunicator().getKeyMapper()
+                .key(dataObject);
+    }
+
     private static void assertSelectionOrder(Multi<String> selectionModel,
             String... selectionOrder) {
         Assert.assertEquals(Arrays.asList(selectionOrder),
index 8cd9afa776f19af3fa4f4bac03f8bc5c780cba15..cc58dbbf9393e5a7534d0d015c22fab69ad05b25 100644 (file)
  */
 package com.vaadin.ui;
 
-import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
-import java.lang.reflect.Proxy;
 
 import com.vaadin.server.ClientConnector;
-import com.vaadin.server.ServerRpcMethodInvocation;
+import com.vaadin.server.ServerRpcManager;
 import com.vaadin.shared.communication.ServerRpc;
 
 /**
@@ -67,32 +65,26 @@ public class ComponentTest {
     }
 
     /**
-     * Gets a proxy object which invokes ServerRpc methods.
+     * Gets the server rpc handler registered for a component.
      *
      * @param component
      *            the component which listens to the RPC
      * @param serverRpcClass
      *            the server RPC class
-     * @return a proxy which can be used to invoke RPC methods
+     * @return the server RPC handler
      */
-    @SuppressWarnings("unchecked")
     public static <T extends ServerRpc> T getRpcProxy(Component component,
             Class<T> serverRpcClass) {
-        return (T) Proxy.newProxyInstance(component.getClass().getClassLoader(),
-                new Class[] { serverRpcClass }, new InvocationHandler() {
-
-                    @Override
-                    public Object invoke(Object proxy, Method method,
-                            Object[] args) throws Throwable {
-                        ServerRpcMethodInvocation invocation = new ServerRpcMethodInvocation(
-                                component.getConnectorId(), serverRpcClass,
-                                method.getName(), args.length);
-                        invocation.setParameters(args);
-                        component.getRpcManager(serverRpcClass.getName())
-                                .applyInvocation(invocation);
-                        return null;
-                    }
-                });
+        try {
+            ServerRpcManager<?> rpcManager = component
+                    .getRpcManager(serverRpcClass.getName());
+            Method method = ServerRpcManager.class
+                    .getDeclaredMethod("getImplementation");
+            method.setAccessible(true);
+            return serverRpcClass.cast(method.invoke(rpcManager));
+        } catch (ReflectiveOperationException e) {
+            throw new RuntimeException(e);
+        }
     }
 
 }