* Make Grid add columns based on bean properties The property set concept used for Binder is slightly generalized and used by Grid as well to support similar functionality. Fixes vaadin/framework8-issues#250tags/8.0.0.beta2
@@ -42,7 +42,7 @@ cell style generator. | |||
[[components.grid.data]] | |||
== Binding to Data | |||
[classname]#Grid# is normally used by binding it to a , | |||
[classname]#Grid# is normally used by binding it to a data provider, | |||
described in | |||
<<dummy/../../../framework/datamodel/datamodel-providers.asciidoc#datamodel.dataproviders,"Showing Many Items in a Listing">>. | |||
By default, it is bound to List of items. You can set the items with the | |||
@@ -96,7 +96,7 @@ grid.setSelectionMode(SelectionMode.MULTI); | |||
grid.addSelectionListener(event -> { | |||
Set<Person> selected = event.getAllSelectedItems(); | |||
Notification.show(selected.size() + " items selected"); | |||
} | |||
}); | |||
---- | |||
Programmatically selecting the value is possible via [methodname]#select(T)#. | |||
@@ -214,7 +214,7 @@ selectionModel.addMultiSelectionListener(event -> { | |||
// Allow deleting only if there's any selected | |||
deleteSelected.setEnabled( | |||
event.getNewSelection().size() > 0); | |||
}; | |||
}); | |||
---- | |||
@@ -241,7 +241,7 @@ and column. | |||
[source, java] | |||
---- | |||
grid.addCellClickListener(event -> | |||
Notification.show("Value: " + event.getItem()); | |||
Notification.show("Value: " + event.getItem())); | |||
---- | |||
The clicked grid cell is also automatically focused. | |||
@@ -255,15 +255,11 @@ well as disable cell focus, in a custom theme. See <<components.grid.css>>. | |||
[[components.grid.columns]] | |||
== Configuring Columns | |||
Columns are normally defined in the container data source. The | |||
[methodname]#addColumn()# method can be used to add columns to [classname]#Grid#. | |||
Column configuration is defined in [classname]#Grid.Column# objects, which can | |||
be obtained from the grid with [methodname]#getColumns()#. | |||
The [methodname]#addColumn()# method can be used to add columns to [classname]#Grid#. | |||
The setter methods in [classname]#Column# have _fluent API_, so you can easily chain | |||
the configuration calls for columns if you want to. | |||
Column configuration is defined in [classname]#Grid.Column# objects, which are returned by `addColumn` and can also be obtained from the grid with [methodname]#getColumns()#. | |||
The setter methods in [classname]#Column# have _fluent API_, so you can easily chain the configuration calls for columns if you want to. | |||
[source, java] | |||
---- | |||
@@ -275,6 +271,22 @@ grid.addColumn(Person:getBirthDate, new DateRenderer()) | |||
In the following, we describe the basic column configuration. | |||
[[components.grid.columns.automatic]] | |||
=== Automatically Adding Columns | |||
You can configure `Grid` to automatically add columns based on the properties in a bean. | |||
To do this, you need to pass the `Class` of the bean type to the constructor when creating a grid. | |||
You can then further configure the columns based on the bean property name. | |||
[source, java] | |||
---- | |||
Grid<Person> grid = new Grid<>(Person.class); | |||
grid.getColumn("birthDate").setWidth("100px"); | |||
grid.setItems(people); | |||
---- | |||
[[components.grid.columns.order]] | |||
=== Column Order | |||
@@ -424,7 +436,7 @@ grid.addColumn(person -> "Delete", | |||
new ButtonRenderer(clickEvent -> { | |||
people.remove(clickEvent.getValue()); | |||
grid.setItems(people); | |||
}); | |||
})); | |||
---- | |||
[classname]#ImageRenderer#:: Renders the cell as an image. |
@@ -32,10 +32,11 @@ import java.util.stream.Stream; | |||
import com.vaadin.data.util.BeanUtil; | |||
import com.vaadin.server.Setter; | |||
import com.vaadin.shared.util.SharedUtil; | |||
import com.vaadin.util.ReflectTools; | |||
/** | |||
* A {@link BinderPropertySet} that uses reflection to find bean properties. | |||
* A {@link PropertySet} that uses reflection to find bean properties. | |||
* | |||
* @author Vaadin Ltd | |||
* | |||
@@ -44,7 +45,7 @@ import com.vaadin.util.ReflectTools; | |||
* @param <T> | |||
* the type of the bean | |||
*/ | |||
public class BeanBinderPropertySet<T> implements BinderPropertySet<T> { | |||
public class BeanPropertySet<T> implements PropertySet<T> { | |||
/** | |||
* Serialized form of a property set. When deserialized, the property set | |||
@@ -52,7 +53,7 @@ public class BeanBinderPropertySet<T> implements BinderPropertySet<T> { | |||
* existing cached instance or creates a new one. | |||
* | |||
* @see #readResolve() | |||
* @see BeanBinderPropertyDefinition#writeReplace() | |||
* @see BeanPropertyDefinition#writeReplace() | |||
*/ | |||
private static class SerializedPropertySet implements Serializable { | |||
private final Class<?> beanType; | |||
@@ -77,7 +78,7 @@ public class BeanBinderPropertySet<T> implements BinderPropertySet<T> { | |||
* definition is then fetched from the property set. | |||
* | |||
* @see #readResolve() | |||
* @see BeanBinderPropertySet#writeReplace() | |||
* @see BeanPropertySet#writeReplace() | |||
*/ | |||
private static class SerializedPropertyDefinition implements Serializable { | |||
private final Class<?> beanType; | |||
@@ -102,14 +103,13 @@ public class BeanBinderPropertySet<T> implements BinderPropertySet<T> { | |||
} | |||
} | |||
private static class BeanBinderPropertyDefinition<T, V> | |||
implements BinderPropertyDefinition<T, V> { | |||
private static class BeanPropertyDefinition<T, V> | |||
implements PropertyDefinition<T, V> { | |||
private final PropertyDescriptor descriptor; | |||
private final BeanBinderPropertySet<T> propertySet; | |||
private final BeanPropertySet<T> propertySet; | |||
public BeanBinderPropertyDefinition( | |||
BeanBinderPropertySet<T> propertySet, | |||
public BeanPropertyDefinition(BeanPropertySet<T> propertySet, | |||
PropertyDescriptor descriptor) { | |||
this.propertySet = propertySet; | |||
this.descriptor = descriptor; | |||
@@ -156,7 +156,12 @@ public class BeanBinderPropertySet<T> implements BinderPropertySet<T> { | |||
} | |||
@Override | |||
public BeanBinderPropertySet<T> getPropertySet() { | |||
public String getCaption() { | |||
return SharedUtil.propertyIdToHumanFriendly(getName()); | |||
} | |||
@Override | |||
public BeanPropertySet<T> getPropertySet() { | |||
return propertySet; | |||
} | |||
@@ -171,21 +176,21 @@ public class BeanBinderPropertySet<T> implements BinderPropertySet<T> { | |||
} | |||
} | |||
private static final ConcurrentMap<Class<?>, BeanBinderPropertySet<?>> instances = new ConcurrentHashMap<>(); | |||
private static final ConcurrentMap<Class<?>, BeanPropertySet<?>> instances = new ConcurrentHashMap<>(); | |||
private final Class<T> beanType; | |||
private final Map<String, BinderPropertyDefinition<T, ?>> definitions; | |||
private final Map<String, PropertyDefinition<T, ?>> definitions; | |||
private BeanBinderPropertySet(Class<T> beanType) { | |||
private BeanPropertySet(Class<T> beanType) { | |||
this.beanType = beanType; | |||
try { | |||
definitions = BeanUtil.getBeanPropertyDescriptors(beanType).stream() | |||
.filter(BeanBinderPropertySet::hasNonObjectReadMethod) | |||
.map(descriptor -> new BeanBinderPropertyDefinition<>(this, | |||
.filter(BeanPropertySet::hasNonObjectReadMethod) | |||
.map(descriptor -> new BeanPropertyDefinition<>(this, | |||
descriptor)) | |||
.collect(Collectors.toMap(BinderPropertyDefinition::getName, | |||
.collect(Collectors.toMap(PropertyDefinition::getName, | |||
Function.identity())); | |||
} catch (IntrospectionException e) { | |||
throw new IllegalArgumentException( | |||
@@ -196,28 +201,28 @@ public class BeanBinderPropertySet<T> implements BinderPropertySet<T> { | |||
} | |||
/** | |||
* Gets a {@link BeanBinderPropertySet} for the given bean type. | |||
* Gets a {@link BeanPropertySet} for the given bean type. | |||
* | |||
* @param beanType | |||
* the bean type to get a property set for, not <code>null</code> | |||
* @return the bean binder property set, not <code>null</code> | |||
* @return the bean property set, not <code>null</code> | |||
*/ | |||
@SuppressWarnings("unchecked") | |||
public static <T> BinderPropertySet<T> get(Class<? extends T> beanType) { | |||
public static <T> PropertySet<T> get(Class<? extends T> beanType) { | |||
Objects.requireNonNull(beanType, "Bean type cannot be null"); | |||
// Cache the reflection results | |||
return (BinderPropertySet<T>) instances.computeIfAbsent(beanType, | |||
BeanBinderPropertySet::new); | |||
return (PropertySet<T>) instances.computeIfAbsent(beanType, | |||
BeanPropertySet::new); | |||
} | |||
@Override | |||
public Stream<BinderPropertyDefinition<T, ?>> getProperties() { | |||
public Stream<PropertyDefinition<T, ?>> getProperties() { | |||
return definitions.values().stream(); | |||
} | |||
@Override | |||
public Optional<BinderPropertyDefinition<T, ?>> getProperty(String name) { | |||
public Optional<PropertyDefinition<T, ?>> getProperty(String name) { | |||
return Optional.ofNullable(definitions.get(name)); | |||
} | |||
@@ -56,7 +56,7 @@ public class BeanValidationBinder<BEAN> extends Binder<BEAN> { | |||
@Override | |||
protected BindingBuilder<BEAN, ?> configureBinding( | |||
BindingBuilder<BEAN, ?> binding, | |||
BinderPropertyDefinition<BEAN, ?> definition) { | |||
PropertyDefinition<BEAN, ?> definition) { | |||
return binding.withValidator( | |||
new BeanValidator(beanType, definition.getName())); | |||
} |
@@ -23,7 +23,6 @@ import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.Collections; | |||
import java.util.HashMap; | |||
import java.util.HashSet; | |||
import java.util.IdentityHashMap; | |||
import java.util.LinkedHashSet; | |||
import java.util.List; | |||
@@ -195,7 +194,7 @@ public class Binder<BEAN> implements Serializable { | |||
/** | |||
* Completes this binding by connecting the field to the property with | |||
* the given name. The getter and setter of the property are looked up | |||
* using a {@link BinderPropertySet}. | |||
* using a {@link PropertySet}. | |||
* <p> | |||
* For a <code>Binder</code> created using the | |||
* {@link Binder#Binder(Class)} constructor, introspection will be used | |||
@@ -217,7 +216,7 @@ public class Binder<BEAN> implements Serializable { | |||
* if the property has no accessible getter | |||
* @throws IllegalStateException | |||
* if the binder is not configured with an appropriate | |||
* {@link BinderPropertySet} | |||
* {@link PropertySet} | |||
* | |||
* @see Binder.BindingBuilder#bind(ValueProvider, Setter) | |||
*/ | |||
@@ -608,7 +607,7 @@ public class Binder<BEAN> implements Serializable { | |||
"Property name cannot be null"); | |||
checkUnbound(); | |||
BinderPropertyDefinition<BEAN, ?> definition = getBinder().propertySet | |||
PropertyDefinition<BEAN, ?> definition = getBinder().propertySet | |||
.getProperty(propertyName) | |||
.orElseThrow(() -> new IllegalArgumentException( | |||
"Could not resolve property name " + propertyName | |||
@@ -627,9 +626,11 @@ public class Binder<BEAN> implements Serializable { | |||
definition); | |||
try { | |||
return ((BindingBuilder) finalBinding).bind(getter, setter); | |||
Binding binding = ((BindingBuilder) finalBinding).bind(getter, | |||
setter); | |||
getBinder().boundProperties.put(propertyName, binding); | |||
return binding; | |||
} finally { | |||
getBinder().boundProperties.add(propertyName); | |||
getBinder().incompleteMemberFieldBindings.remove(getField()); | |||
} | |||
} | |||
@@ -1044,12 +1045,12 @@ public class Binder<BEAN> implements Serializable { | |||
} | |||
} | |||
private final BinderPropertySet<BEAN> propertySet; | |||
private final PropertySet<BEAN> propertySet; | |||
/** | |||
* Property names that have been used for creating a binding. | |||
*/ | |||
private final Set<String> boundProperties = new HashSet<>(); | |||
private final Map<String, Binding<BEAN, ?>> boundProperties = new HashMap<>(); | |||
private final Map<HasValue<?>, BindingBuilder<BEAN, ?>> incompleteMemberFieldBindings = new IdentityHashMap<>(); | |||
@@ -1072,16 +1073,15 @@ public class Binder<BEAN> implements Serializable { | |||
private boolean hasChanges = false; | |||
/** | |||
* Creates a binder using a custom {@link BinderPropertySet} implementation | |||
* for finding and resolving property names for | |||
* Creates a binder using a custom {@link PropertySet} implementation for | |||
* finding and resolving property names for | |||
* {@link #bindInstanceFields(Object)}, {@link #bind(HasValue, String)} and | |||
* {@link BindingBuilder#bind(String)}. | |||
* | |||
* @param propertySet | |||
* the binder property set implementation to use, not | |||
* <code>null</code>. | |||
* the property set implementation to use, not <code>null</code>. | |||
*/ | |||
protected Binder(BinderPropertySet<BEAN> propertySet) { | |||
protected Binder(PropertySet<BEAN> propertySet) { | |||
Objects.requireNonNull(propertySet, "propertySet cannot be null"); | |||
this.propertySet = propertySet; | |||
} | |||
@@ -1094,7 +1094,7 @@ public class Binder<BEAN> implements Serializable { | |||
* the bean type to use, not <code>null</code> | |||
*/ | |||
public Binder(Class<BEAN> beanType) { | |||
this(BeanBinderPropertySet.get(beanType)); | |||
this(BeanPropertySet.get(beanType)); | |||
} | |||
/** | |||
@@ -1106,15 +1106,15 @@ public class Binder<BEAN> implements Serializable { | |||
* {@link #bind(HasValue, String)} or {@link BindingBuilder#bind(String)}. | |||
*/ | |||
public Binder() { | |||
this(new BinderPropertySet<BEAN>() { | |||
this(new PropertySet<BEAN>() { | |||
@Override | |||
public Stream<BinderPropertyDefinition<BEAN, ?>> getProperties() { | |||
public Stream<PropertyDefinition<BEAN, ?>> getProperties() { | |||
throw new IllegalStateException( | |||
"A Binder created with the default constructor doesn't support listing properties."); | |||
} | |||
@Override | |||
public Optional<BinderPropertyDefinition<BEAN, ?>> getProperty( | |||
public Optional<PropertyDefinition<BEAN, ?>> getProperty( | |||
String name) { | |||
throw new IllegalStateException( | |||
"A Binder created with the default constructor doesn't support finding properties by name."); | |||
@@ -1123,8 +1123,8 @@ public class Binder<BEAN> implements Serializable { | |||
} | |||
/** | |||
* Creates a binder using a custom {@link BinderPropertySet} implementation | |||
* for finding and resolving property names for | |||
* Creates a binder using a custom {@link PropertySet} implementation for | |||
* finding and resolving property names for | |||
* {@link #bindInstanceFields(Object)}, {@link #bind(HasValue, String)} and | |||
* {@link BindingBuilder#bind(String)}. | |||
* <p> | |||
@@ -1137,13 +1137,12 @@ public class Binder<BEAN> implements Serializable { | |||
* @see Binder#Binder(Class) | |||
* | |||
* @param propertySet | |||
* the binder property set implementation to use, not | |||
* <code>null</code>. | |||
* the property set implementation to use, not <code>null</code>. | |||
* @return a new binder using the provided property set, not | |||
* <code>null</code> | |||
*/ | |||
public static <BEAN> Binder<BEAN> withPropertySet( | |||
BinderPropertySet<BEAN> propertySet) { | |||
PropertySet<BEAN> propertySet) { | |||
return new Binder<>(propertySet); | |||
} | |||
@@ -1272,7 +1271,7 @@ public class Binder<BEAN> implements Serializable { | |||
/** | |||
* Binds the given field to the property with the given name. The getter and | |||
* setter of the property are looked up using a {@link BinderPropertySet}. | |||
* setter of the property are looked up using a {@link PropertySet}. | |||
* <p> | |||
* For a <code>Binder</code> created using the {@link Binder#Binder(Class)} | |||
* constructor, introspection will be used to find a Java Bean property. If | |||
@@ -1297,7 +1296,7 @@ public class Binder<BEAN> implements Serializable { | |||
* if the property has no accessible getter | |||
* @throws IllegalStateException | |||
* if the binder is not configured with an appropriate | |||
* {@link BinderPropertySet} | |||
* {@link PropertySet} | |||
* | |||
* @see #bind(HasValue, ValueProvider, Setter) | |||
*/ | |||
@@ -1964,7 +1963,7 @@ public class Binder<BEAN> implements Serializable { | |||
/** | |||
* Configures the {@code binding} with the property definition | |||
* {@code definition} before it's being bound. | |||
* | |||
* | |||
* @param binding | |||
* a binding to configure | |||
* @param definition | |||
@@ -1973,7 +1972,7 @@ public class Binder<BEAN> implements Serializable { | |||
*/ | |||
protected BindingBuilder<BEAN, ?> configureBinding( | |||
BindingBuilder<BEAN, ?> binding, | |||
BinderPropertyDefinition<BEAN, ?> definition) { | |||
PropertyDefinition<BEAN, ?> definition) { | |||
return binding; | |||
} | |||
@@ -2217,7 +2216,7 @@ public class Binder<BEAN> implements Serializable { | |||
private void handleProperty(Field field, Object objectWithMemberFields, | |||
BiConsumer<String, Class<?>> propertyHandler) { | |||
Optional<BinderPropertyDefinition<BEAN, ?>> descriptor = getPropertyDescriptor( | |||
Optional<PropertyDefinition<BEAN, ?>> descriptor = getPropertyDescriptor( | |||
field); | |||
if (!descriptor.isPresent()) { | |||
@@ -2225,7 +2224,7 @@ public class Binder<BEAN> implements Serializable { | |||
} | |||
String propertyName = descriptor.get().getName(); | |||
if (boundProperties.contains(propertyName)) { | |||
if (boundProperties.containsKey(propertyName)) { | |||
return; | |||
} | |||
@@ -2237,10 +2236,25 @@ public class Binder<BEAN> implements Serializable { | |||
} | |||
propertyHandler.accept(propertyName, descriptor.get().getType()); | |||
boundProperties.add(propertyName); | |||
assert boundProperties.containsKey(propertyName); | |||
} | |||
/** | |||
* Gets the binding for a property name. Bindings are available by property | |||
* name if bound using {@link #bind(HasValue, String)}, | |||
* {@link BindingBuilder#bind(String)} or indirectly using | |||
* {@link #bindInstanceFields(Object)}. | |||
* | |||
* @param propertyName | |||
* the property name of the binding to get | |||
* @return the binding corresponding to the property name, or an empty | |||
* optional if there is no binding with that property name | |||
*/ | |||
public Optional<Binding<BEAN, ?>> getBinding(String propertyName) { | |||
return Optional.ofNullable(boundProperties.get(propertyName)); | |||
} | |||
private Optional<BinderPropertyDefinition<BEAN, ?>> getPropertyDescriptor( | |||
private Optional<PropertyDefinition<BEAN, ?>> getPropertyDescriptor( | |||
Field field) { | |||
PropertyId propertyIdAnnotation = field.getAnnotation(PropertyId.class); | |||
@@ -2254,8 +2268,7 @@ public class Binder<BEAN> implements Serializable { | |||
String minifiedFieldName = minifyFieldName(propertyId); | |||
return propertySet.getProperties() | |||
.map(BinderPropertyDefinition::getName) | |||
return propertySet.getProperties().map(PropertyDefinition::getName) | |||
.filter(name -> minifyFieldName(name).equals(minifiedFieldName)) | |||
.findFirst().flatMap(propertySet::getProperty); | |||
} |
@@ -21,17 +21,17 @@ import java.util.Optional; | |||
import com.vaadin.server.Setter; | |||
/** | |||
* A property from a {@link BinderPropertySet}. | |||
* A property from a {@link PropertySet}. | |||
* | |||
* @author Vaadin Ltd | |||
* @since | |||
* | |||
* @param <T> | |||
* the type of the binder property set | |||
* the type of the property set | |||
* @param <V> | |||
* the property type | |||
*/ | |||
public interface BinderPropertyDefinition<T, V> extends Serializable { | |||
public interface PropertyDefinition<T, V> extends Serializable { | |||
/** | |||
* Gets the value provider that is used for finding the value of this | |||
* property for a bean. | |||
@@ -62,9 +62,16 @@ public interface BinderPropertyDefinition<T, V> extends Serializable { | |||
public String getName(); | |||
/** | |||
* Gets the {@link BinderPropertySet} that this property belongs to. | |||
* Gets the human readable caption to show for this property. | |||
* | |||
* @return the binder property set, not <code>null</code> | |||
* @return the caption to show, not <code>null</code> | |||
*/ | |||
public BinderPropertySet<T> getPropertySet(); | |||
public String getCaption(); | |||
/** | |||
* Gets the {@link PropertySet} that this property belongs to. | |||
* | |||
* @return the property set, not <code>null</code> | |||
*/ | |||
public PropertySet<T> getPropertySet(); | |||
} |
@@ -20,7 +20,8 @@ import java.util.Optional; | |||
import java.util.stream.Stream; | |||
/** | |||
* Describes a set of properties that can be used with a {@link Binder}. | |||
* Describes a set of properties that can be used for configuration based on | |||
* property names instead of setter and getter callbacks. | |||
* | |||
* @author Vaadin Ltd | |||
* | |||
@@ -29,13 +30,13 @@ import java.util.stream.Stream; | |||
* @param <T> | |||
* the type for which the properties are defined | |||
*/ | |||
public interface BinderPropertySet<T> extends Serializable { | |||
public interface PropertySet<T> extends Serializable { | |||
/** | |||
* Gets all known properties as a stream. | |||
* | |||
* @return a stream of property names, not <code>null</code> | |||
*/ | |||
public Stream<BinderPropertyDefinition<T, ?>> getProperties(); | |||
public Stream<PropertyDefinition<T, ?>> getProperties(); | |||
/** | |||
* Gets the definition for the named property, or an empty optional if there | |||
@@ -46,5 +47,5 @@ public interface BinderPropertySet<T> extends Serializable { | |||
* @return the property definition, or empty optional if property doesn't | |||
* exist | |||
*/ | |||
public Optional<BinderPropertyDefinition<T, ?>> getProperty(String name); | |||
public Optional<PropertyDefinition<T, ?>> getProperty(String name); | |||
} |
@@ -41,10 +41,13 @@ import org.jsoup.nodes.Attributes; | |||
import org.jsoup.nodes.Element; | |||
import org.jsoup.select.Elements; | |||
import com.vaadin.data.BeanPropertySet; | |||
import com.vaadin.data.Binder; | |||
import com.vaadin.data.Binder.Binding; | |||
import com.vaadin.data.HasDataProvider; | |||
import com.vaadin.data.HasValue; | |||
import com.vaadin.data.PropertyDefinition; | |||
import com.vaadin.data.PropertySet; | |||
import com.vaadin.data.ValueProvider; | |||
import com.vaadin.data.provider.DataCommunicator; | |||
import com.vaadin.data.provider.DataProvider; | |||
@@ -1609,7 +1612,9 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, | |||
* a setter that stores the component value in the row item | |||
* @return this column | |||
* | |||
* @see #setEditorBinding(Binding) | |||
* @see Grid#getEditor() | |||
* @see Binder#bind(HasValue, ValueProvider, Setter) | |||
*/ | |||
public <C extends HasValue<V> & Component> Column<T, V> setEditorComponent( | |||
C editorComponent, Setter<T, V> setter) { | |||
@@ -1623,6 +1628,47 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, | |||
return setEditorBinding(binding); | |||
} | |||
/** | |||
* Sets a component to use for editing values of this columns in the | |||
* editor row. This method can only be used if the column has an | |||
* {@link #setId(String) id} and the {@link Grid} has been created using | |||
* {@link Grid#Grid(Class)} or some other way that allows finding | |||
* properties based on property names. | |||
* <p> | |||
* This is a shorthand for use in simple cases where no validator or | |||
* converter is needed. Use {@link #setEditorBinding(Binding)} to | |||
* support more complex cases. | |||
* <p> | |||
* <strong>Note:</strong> The same component cannot be used for multiple | |||
* columns. | |||
* | |||
* @param editorComponent | |||
* the editor component | |||
* @return this column | |||
* | |||
* @see #setEditorBinding(Binding) | |||
* @see Grid#getEditor() | |||
* @see Binder#bind(HasValue, String) | |||
* @see Grid#Grid(Class) | |||
*/ | |||
public <F, C extends HasValue<F> & Component> Column<T, V> setEditorComponent( | |||
C editorComponent) { | |||
Objects.requireNonNull(editorComponent, | |||
"Editor component cannot be null"); | |||
String propertyName = getId(); | |||
if (propertyName == null) { | |||
throw new IllegalStateException( | |||
"setEditorComponent without a setter can only be used if the column has an id. " | |||
+ "Use another setEditorComponent(Component, Setter) or setEditorBinding(Binding) instead."); | |||
} | |||
Binding<T, F> binding = getGrid().getEditor().getBinder() | |||
.bind(editorComponent, propertyName); | |||
return setEditorBinding(binding); | |||
} | |||
/** | |||
* Gets the grid that this column belongs to. | |||
* | |||
@@ -1851,10 +1897,64 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, | |||
private Editor<T> editor; | |||
private final PropertySet<T> propertySet; | |||
/** | |||
* Constructor for the {@link Grid} component. | |||
* Creates a new grid without support for creating columns based on property | |||
* names. Use an alternative constructor, such as {@link Grid#Grid(Class)}, | |||
* to create a grid that automatically sets up columns based on the type of | |||
* presented data. | |||
* | |||
* @see #Grid(Class) | |||
* @see #withPropertySet(PropertySet) | |||
*/ | |||
public Grid() { | |||
this(new PropertySet<T>() { | |||
@Override | |||
public Stream<PropertyDefinition<T, ?>> getProperties() { | |||
// No columns configured by default | |||
return Stream.empty(); | |||
} | |||
@Override | |||
public Optional<PropertyDefinition<T, ?>> getProperty(String name) { | |||
throw new IllegalStateException( | |||
"A Grid created without a bean type class literal or a custom property set" | |||
+ " doesn't support finding properties by name."); | |||
} | |||
}); | |||
} | |||
/** | |||
* Creates a new grid that uses reflection based on the provided bean type | |||
* to automatically set up an initial set of columns. All columns will be | |||
* configured using the same {@link Object#toString()} renderer that is used | |||
* by {@link #addColumn(ValueProvider)}. | |||
* | |||
* @param beanType | |||
* the bean type to use, not <code>null</code> | |||
* @see #Grid() | |||
* @see #withPropertySet(PropertySet) | |||
*/ | |||
public Grid(Class<T> beanType) { | |||
this(BeanPropertySet.get(beanType)); | |||
} | |||
/** | |||
* Creates a grid using a custom {@link PropertySet} implementation for | |||
* configuring the initial columns and resolving property names for | |||
* {@link #addColumn(String)} and | |||
* {@link Column#setEditorComponent(HasValue)}. | |||
* | |||
* @see #withPropertySet(PropertySet) | |||
* | |||
* @param propertySet | |||
* the property set implementation to use, not <code>null</code>. | |||
*/ | |||
protected Grid(PropertySet<T> propertySet) { | |||
Objects.requireNonNull(propertySet, "propertySet cannot be null"); | |||
this.propertySet = propertySet; | |||
registerRpc(new GridServerRpcImpl()); | |||
setDefaultHeaderRow(appendHeaderRow()); | |||
@@ -1882,6 +1982,33 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, | |||
} | |||
} | |||
}); | |||
// Automatically add columns for all available properties | |||
propertySet.getProperties().map(PropertyDefinition::getName) | |||
.forEach(this::addColumn); | |||
} | |||
/** | |||
* Creates a grid using a custom {@link PropertySet} implementation for | |||
* creating a default set of columns and for resolving property names with | |||
* {@link #addColumn(String)} and | |||
* {@link Column#setEditorComponent(HasValue)}. | |||
* <p> | |||
* This functionality is provided as static method instead of as a public | |||
* constructor in order to make it possible to use a custom property set | |||
* without creating a subclass while still leaving the public constructors | |||
* focused on the common use cases. | |||
* | |||
* @see Grid#Grid() | |||
* @see Grid#Grid(Class) | |||
* | |||
* @param propertySet | |||
* the property set implementation to use, not <code>null</code>. | |||
* @return a new grid using the provided property set, not <code>null</code> | |||
*/ | |||
public static <BEAN> Grid<BEAN> withPropertySet( | |||
PropertySet<BEAN> propertySet) { | |||
return new Grid<>(propertySet); | |||
} | |||
/** | |||
@@ -1939,6 +2066,37 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, | |||
userOriginated)); | |||
} | |||
/** | |||
* Adds a new column with the given property name. The property name will be | |||
* used as the {@link Column#getId() column id} and the | |||
* {@link Column#getCaption() column caption} will be set based on the | |||
* property definition. | |||
* <p> | |||
* This method can only be used for a <code>Grid</code> created using | |||
* {@link Grid#Grid(Class)} or {@link #withPropertySet(PropertySet)}. | |||
* | |||
* @param propertyName | |||
* the property name of the new column, not <code>null</code> | |||
* @return the newly added column, not <code>null</code> | |||
*/ | |||
public Column<T, ?> addColumn(String propertyName) { | |||
Objects.requireNonNull(propertyName, "Property name cannot be null"); | |||
if (getColumn(propertyName) != null) { | |||
throw new IllegalStateException( | |||
"There is already a column for " + propertyName); | |||
} | |||
PropertyDefinition<T, ?> definition = propertySet | |||
.getProperty(propertyName) | |||
.orElseThrow(() -> new IllegalArgumentException( | |||
"Could not resolve property name " + propertyName | |||
+ " from " + propertySet)); | |||
return addColumn(definition.getGetter()).setId(definition.getName()) | |||
.setCaption(definition.getCaption()); | |||
} | |||
/** | |||
* Adds a new text column to this {@link Grid} with a value provider. The | |||
* column will use a {@link TextRenderer}. The value is converted to a | |||
@@ -2070,7 +2228,8 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, | |||
* | |||
* @param columnId | |||
* the identifier of the column to get | |||
* @return the column corresponding to the given column identifier | |||
* @return the column corresponding to the given column identifier, or | |||
* <code>null</code> if there is no such column | |||
*/ | |||
public Column<T, ?> getColumn(String columnId) { | |||
return columnIds.get(columnId); | |||
@@ -2983,7 +3142,7 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents, | |||
* @return editor | |||
*/ | |||
protected Editor<T> createEditor() { | |||
return new EditorImpl<>(); | |||
return new EditorImpl<>(propertySet); | |||
} | |||
private void addExtensionComponent(Component c) { |
@@ -27,6 +27,7 @@ import com.vaadin.data.Binder; | |||
import com.vaadin.data.Binder.Binding; | |||
import com.vaadin.data.BinderValidationStatus; | |||
import com.vaadin.data.BinderValidationStatusHandler; | |||
import com.vaadin.data.PropertySet; | |||
import com.vaadin.shared.ui.grid.editor.EditorClientRpc; | |||
import com.vaadin.shared.ui.grid.editor.EditorServerRpc; | |||
import com.vaadin.shared.ui.grid.editor.EditorState; | |||
@@ -112,8 +113,11 @@ public class EditorImpl<T> extends AbstractGridExtension<T> | |||
/** | |||
* Constructor for internal implementation of the Editor. | |||
* | |||
* @param propertySet | |||
* the property set to use for configuring the default binder | |||
*/ | |||
public EditorImpl() { | |||
public EditorImpl(PropertySet<T> propertySet) { | |||
rpc = getRpcProxy(EditorClientRpc.class); | |||
registerRpc(new EditorServerRpc() { | |||
@@ -142,7 +146,7 @@ public class EditorImpl<T> extends AbstractGridExtension<T> | |||
} | |||
}); | |||
setBinder(new Binder<>()); | |||
setBinder(Binder.withPropertySet(propertySet)); | |||
} | |||
@Override |
@@ -32,13 +32,13 @@ import org.junit.Test; | |||
import com.vaadin.data.provider.bov.Person; | |||
import com.vaadin.tests.server.ClassesSerializableTest; | |||
public class BeanBinderPropertySetTest { | |||
public class BeanPropertySetTest { | |||
@Test | |||
public void testSerializeDeserialize_propertySet() throws Exception { | |||
BinderPropertySet<Person> originalPropertySet = BeanBinderPropertySet | |||
PropertySet<Person> originalPropertySet = BeanPropertySet | |||
.get(Person.class); | |||
BinderPropertySet<Person> deserializedPropertySet = ClassesSerializableTest | |||
PropertySet<Person> deserializedPropertySet = ClassesSerializableTest | |||
.serializeAndDeserialize(originalPropertySet); | |||
Assert.assertSame( | |||
@@ -49,7 +49,7 @@ public class BeanBinderPropertySetTest { | |||
@Test | |||
public void testSerializeDeserialize_propertySet_cacheCleared() | |||
throws Exception { | |||
BinderPropertySet<Person> originalPropertySet = BeanBinderPropertySet | |||
PropertySet<Person> originalPropertySet = BeanPropertySet | |||
.get(Person.class); | |||
ByteArrayOutputStream bs = new ByteArrayOutputStream(); | |||
@@ -59,7 +59,7 @@ public class BeanBinderPropertySetTest { | |||
// Simulate deserializing into a different JVM by clearing the instance | |||
// map | |||
Field instancesField = BeanBinderPropertySet.class | |||
Field instancesField = BeanPropertySet.class | |||
.getDeclaredField("instances"); | |||
instancesField.setAccessible(true); | |||
Map<?, ?> instances = (Map<?, ?>) instancesField.get(null); | |||
@@ -67,13 +67,12 @@ public class BeanBinderPropertySetTest { | |||
ObjectInputStream in = new ObjectInputStream( | |||
new ByteArrayInputStream(data)); | |||
BinderPropertySet<Person> deserializedPropertySet = (BinderPropertySet<Person>) in | |||
PropertySet<Person> deserializedPropertySet = (PropertySet<Person>) in | |||
.readObject(); | |||
Assert.assertSame( | |||
"Deserialized instance should be the same as in the cache", | |||
BeanBinderPropertySet.get(Person.class), | |||
deserializedPropertySet); | |||
BeanPropertySet.get(Person.class), deserializedPropertySet); | |||
Assert.assertNotSame( | |||
"Deserialized instance should not be the same as the original", | |||
originalPropertySet, deserializedPropertySet); | |||
@@ -81,11 +80,11 @@ public class BeanBinderPropertySetTest { | |||
@Test | |||
public void testSerializeDeserialize_propertyDefinition() throws Exception { | |||
BinderPropertyDefinition<Person, ?> definition = BeanBinderPropertySet | |||
PropertyDefinition<Person, ?> definition = BeanPropertySet | |||
.get(Person.class).getProperty("born") | |||
.orElseThrow(RuntimeException::new); | |||
BinderPropertyDefinition<Person, ?> deserializedDefinition = ClassesSerializableTest | |||
PropertyDefinition<Person, ?> deserializedDefinition = ClassesSerializableTest | |||
.serializeAndDeserialize(definition); | |||
ValueProvider<Person, ?> getter = deserializedDefinition.getGetter(); | |||
@@ -97,19 +96,17 @@ public class BeanBinderPropertySetTest { | |||
Assert.assertSame( | |||
"Deserialized instance should be the same as in the cache", | |||
BeanBinderPropertySet.get(Person.class).getProperty("born") | |||
BeanPropertySet.get(Person.class).getProperty("born") | |||
.orElseThrow(RuntimeException::new), | |||
deserializedDefinition); | |||
} | |||
@Test | |||
public void properties() { | |||
BinderPropertySet<Person> propertySet = BeanBinderPropertySet | |||
.get(Person.class); | |||
PropertySet<Person> propertySet = BeanPropertySet.get(Person.class); | |||
Set<String> propertyNames = propertySet.getProperties() | |||
.map(BinderPropertyDefinition::getName) | |||
.collect(Collectors.toSet()); | |||
.map(PropertyDefinition::getName).collect(Collectors.toSet()); | |||
Assert.assertEquals(new HashSet<>(Arrays.asList("name", "born")), | |||
propertyNames); |
@@ -16,6 +16,7 @@ | |||
package com.vaadin.data; | |||
import java.util.HashMap; | |||
import java.util.Locale; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.stream.Stream; | |||
@@ -28,7 +29,7 @@ import com.vaadin.ui.TextField; | |||
public class BinderCustomPropertySetTest { | |||
public static class MapPropertyDefinition | |||
implements BinderPropertyDefinition<Map<String, String>, String> { | |||
implements PropertyDefinition<Map<String, String>, String> { | |||
private MapPropertySet propertySet; | |||
private String name; | |||
@@ -65,26 +66,31 @@ public class BinderCustomPropertySetTest { | |||
} | |||
@Override | |||
public BinderPropertySet<Map<String, String>> getPropertySet() { | |||
public PropertySet<Map<String, String>> getPropertySet() { | |||
return propertySet; | |||
} | |||
@Override | |||
public String getCaption() { | |||
return name.toUpperCase(Locale.ENGLISH); | |||
} | |||
} | |||
public static class MapPropertySet | |||
implements BinderPropertySet<Map<String, String>> { | |||
implements PropertySet<Map<String, String>> { | |||
@Override | |||
public Stream<BinderPropertyDefinition<Map<String, String>, ?>> getProperties() { | |||
public Stream<PropertyDefinition<Map<String, String>, ?>> getProperties() { | |||
return Stream.of("one", "two", "three").map(this::createProperty); | |||
} | |||
@Override | |||
public Optional<BinderPropertyDefinition<Map<String, String>, ?>> getProperty( | |||
public Optional<PropertyDefinition<Map<String, String>, ?>> getProperty( | |||
String name) { | |||
return Optional.of(createProperty(name)); | |||
} | |||
private BinderPropertyDefinition<Map<String, String>, ?> createProperty( | |||
private PropertyDefinition<Map<String, String>, ?> createProperty( | |||
String name) { | |||
return new MapPropertyDefinition(this, name); | |||
} |
@@ -17,13 +17,8 @@ package com.vaadin.data.provider.bov; | |||
import java.io.Serializable; | |||
/** | |||
* POJO | |||
* | |||
* @author Vaadin Ltd | |||
*/ | |||
public class Person implements Serializable { | |||
private final String name; | |||
private String name; | |||
private final int born; | |||
public Person(String name, int born) { | |||
@@ -35,6 +30,10 @@ public class Person implements Serializable { | |||
return name; | |||
} | |||
public void setName(String name) { | |||
this.name = name; | |||
} | |||
public int getBorn() { | |||
return born; | |||
} |
@@ -6,25 +6,37 @@ import static org.junit.Assert.assertNotNull; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.HashSet; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.concurrent.atomic.AtomicReference; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.Stream; | |||
import org.easymock.Capture; | |||
import org.junit.Assert; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import com.vaadin.data.Binder.Binding; | |||
import com.vaadin.data.ValidationException; | |||
import com.vaadin.data.ValueProvider; | |||
import com.vaadin.data.provider.GridSortOrder; | |||
import com.vaadin.data.provider.bov.Person; | |||
import com.vaadin.event.selection.SelectionEvent; | |||
import com.vaadin.shared.data.sort.SortDirection; | |||
import com.vaadin.shared.ui.grid.HeightMode; | |||
import com.vaadin.tests.util.MockUI; | |||
import com.vaadin.ui.Grid; | |||
import com.vaadin.ui.Grid.Column; | |||
import com.vaadin.ui.Grid.SelectionMode; | |||
import com.vaadin.ui.TextField; | |||
import com.vaadin.ui.renderers.NumberRenderer; | |||
import elemental.json.Json; | |||
import elemental.json.JsonObject; | |||
public class GridTest { | |||
private Grid<String> grid; | |||
@@ -262,4 +274,83 @@ public class GridTest { | |||
Assert.assertEquals(0, list.size()); | |||
Assert.assertTrue(fired.get()); | |||
} | |||
@Test | |||
public void beanGrid() { | |||
Grid<Person> grid = new Grid<>(Person.class); | |||
Column<Person, ?> nameColumn = grid.getColumn("name"); | |||
Column<Person, ?> bornColumn = grid.getColumn("born"); | |||
Assert.assertNotNull(nameColumn); | |||
Assert.assertNotNull(bornColumn); | |||
Assert.assertEquals("Name", nameColumn.getCaption()); | |||
Assert.assertEquals("Born", bornColumn.getCaption()); | |||
JsonObject json = getRowData(grid, new Person("Lorem", 2000)); | |||
Set<String> values = Stream.of(json.keys()).map(json::getString) | |||
.collect(Collectors.toSet()); | |||
Assert.assertEquals(new HashSet<>(Arrays.asList("Lorem", "2000")), | |||
values); | |||
} | |||
@Test | |||
public void beanGrid_editor() throws ValidationException { | |||
Grid<Person> grid = new Grid<>(Person.class); | |||
Column<Person, ?> nameColumn = grid.getColumn("name"); | |||
TextField nameField = new TextField(); | |||
nameColumn.setEditorComponent(nameField); | |||
Optional<Binding<Person, ?>> maybeBinding = grid.getEditor().getBinder() | |||
.getBinding("name"); | |||
Assert.assertTrue(maybeBinding.isPresent()); | |||
Binding<Person, ?> binding = maybeBinding.get(); | |||
Assert.assertSame(nameField, binding.getField()); | |||
Person person = new Person("Lorem", 2000); | |||
grid.getEditor().getBinder().setBean(person); | |||
Assert.assertEquals("Lorem", nameField.getValue()); | |||
nameField.setValue("Ipsum"); | |||
Assert.assertEquals("Ipsum", person.getName()); | |||
} | |||
@Test(expected = IllegalStateException.class) | |||
public void oneArgSetEditor_nonBeanGrid() { | |||
Grid<Person> grid = new Grid<>(); | |||
Column<Person, String> nameCol = grid.addColumn(Person::getName) | |||
.setId("name"); | |||
nameCol.setEditorComponent(new TextField()); | |||
} | |||
@Test(expected = IllegalStateException.class) | |||
public void addExistingColumnById_throws() { | |||
Grid<Person> grid = new Grid<>(Person.class); | |||
grid.addColumn("name"); | |||
} | |||
private static <T> JsonObject getRowData(Grid<T> grid, T row) { | |||
JsonObject json = Json.createObject(); | |||
if (grid.getColumns().isEmpty()) { | |||
return json; | |||
} | |||
// generateData only works if Grid is attached | |||
new MockUI().setContent(grid); | |||
grid.getColumns().forEach(column -> column.generateData(row, json)); | |||
// Detach again | |||
grid.getUI().setContent(null); | |||
return json.getObject("d"); | |||
} | |||
} |