diff options
author | Henrik Paul <henrik@vaadin.com> | 2013-12-17 11:08:25 +0200 |
---|---|---|
committer | Henrik Paul <henrik@vaadin.com> | 2013-12-17 11:08:25 +0200 |
commit | 7670a020c7447d7ae2fe2c3e1fb1fa966b138e65 (patch) | |
tree | 039d1fddc555ac9ddb34cd36c42d4a231e3a47c3 /server/src | |
parent | 887eead6bfa56de25f5b6514b2eafe698b3e9947 (diff) | |
download | vaadin-framework-7670a020c7447d7ae2fe2c3e1fb1fa966b138e65.tar.gz vaadin-framework-7670a020c7447d7ae2fe2c3e1fb1fa966b138e65.zip |
Grid supports data set changes (#12645)
Change-Id: I5ceb52dea079f48b0065c1b2dbdc35b30fe8c4ee
Diffstat (limited to 'server/src')
-rw-r--r-- | server/src/com/vaadin/data/RpcDataProviderExtension.java | 71 | ||||
-rw-r--r-- | server/src/com/vaadin/ui/components/grid/Grid.java | 362 |
2 files changed, 420 insertions, 13 deletions
diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java index 48f03b98c0..b22e6a209b 100644 --- a/server/src/com/vaadin/data/RpcDataProviderExtension.java +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -18,6 +18,7 @@ package com.vaadin.data; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import com.vaadin.data.Container.Indexed; @@ -67,22 +68,24 @@ public class RpcDataProviderExtension extends AbstractExtension { Collection<?> propertyIds = container.getContainerPropertyIds(); List<String[]> rows = new ArrayList<String[]>(itemIds.size()); for (Object itemId : itemIds) { - Item item = container.getItem(itemId); - String[] row = new String[propertyIds.size()]; - - int i = 0; - for (Object propertyId : propertyIds) { - Object value = item.getItemProperty(propertyId).getValue(); - String stringValue = String.valueOf(value); - row[i++] = stringValue; - } - - rows.add(row); + rows.add(getRowData(propertyIds, itemId)); } - getRpcProxy(DataProviderRpc.class).setRowData(firstRow, rows); } + private String[] getRowData(Collection<?> propertyIds, Object itemId) { + Item item = container.getItem(itemId); + String[] row = new String[propertyIds.size()]; + + int i = 0; + for (Object propertyId : propertyIds) { + Object value = item.getItemProperty(propertyId).getValue(); + String stringValue = String.valueOf(value); + row[i++] = stringValue; + } + return row; + } + @Override protected DataProviderState getState() { return (DataProviderState) super.getState(); @@ -98,4 +101,48 @@ public class RpcDataProviderExtension extends AbstractExtension { super.extend(component); } + /** + * Informs the client side that new rows have been inserted into the data + * source. + * + * @param index + * the index at which new rows have been inserted + * @param count + * the number of rows inserted at <code>index</code> + */ + public void insertRowData(int index, int count) { + getState().containerSize += count; + getRpcProxy(DataProviderRpc.class).insertRowData(index, count); + } + + /** + * Informs the client side that rows have been removed from the data source. + * + * @param firstIndex + * the index of the first row removed + * @param count + * the number of rows removed + */ + public void removeRowData(int firstIndex, int count) { + getState().containerSize -= count; + getRpcProxy(DataProviderRpc.class).removeRowData(firstIndex, count); + } + + /** + * Informs the client side that data of a row has been modified in the data + * source. + * + * @param index + * the index of the row that was updated + */ + public void updateRowData(int index) { + /* + * TODO: ignore duplicate requests for the same index during the same + * roundtrip. + */ + Object itemId = container.getIdByIndex(index); + String[] row = getRowData(container.getContainerPropertyIds(), itemId); + getRpcProxy(DataProviderRpc.class).setRowData(index, + Collections.singletonList(row)); + } } diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java index 1fb0692104..08685874c1 100644 --- a/server/src/com/vaadin/ui/components/grid/Grid.java +++ b/server/src/com/vaadin/ui/components/grid/Grid.java @@ -26,15 +26,28 @@ import java.util.List; import java.util.Map; import com.vaadin.data.Container; +import com.vaadin.data.Container.Indexed.ItemAddEvent; +import com.vaadin.data.Container.Indexed.ItemRemoveEvent; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Container.ItemSetChangeNotifier; import com.vaadin.data.Container.PropertySetChangeEvent; import com.vaadin.data.Container.PropertySetChangeListener; import com.vaadin.data.Container.PropertySetChangeNotifier; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.data.Property.ValueChangeNotifier; import com.vaadin.data.RpcDataProviderExtension; import com.vaadin.server.KeyMapper; import com.vaadin.shared.ui.grid.ColumnGroupRowState; import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.Range; import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.Component; /** * Data grid component @@ -57,6 +70,282 @@ import com.vaadin.ui.AbstractComponent; public class Grid extends AbstractComponent { /** + * A helper class that handles the client-side Escalator logic relating to + * making sure that whatever is currently visible to the user, is properly + * initialized and otherwise handled on the server side (as far as + * requried). + * <p> + * This bookeeping includes, but is not limited to: + * <ul> + * <li>listening to the currently visible {@link Property Properties'} value + * changes on the server side and sending those back to the client; and + * <li>attaching and detaching {@link Component Components} from the Vaadin + * Component hierarchy. + * </ul> + */ + private final class ActiveRowHandler { + /** + * A map from itemId to the value change listener used for all of its + * properties + */ + private final Map<Object, GridValueChangeListener> valueChangeListeners = new HashMap<Object, GridValueChangeListener>(); + + /** + * The currently active range. Practically, it's the range of row + * indices being displayed currently. + */ + private Range activeRange = Range.withLength(0, 0); + + /** + * A hook for making sure that appropriate data is "active". All other + * rows should be "inactive". + * <p> + * "Active" can mean different things in different contexts. For + * example, only the Properties in the active range need + * ValueChangeListeners. Also, whenever a row with a Component becomes + * active, it needs to be attached (and conversely, when inactive, it + * needs to be detached). + * + * @param firstActiveRow + * the first active row + * @param activeRowCount + * the number of active rows + */ + public void setActiveRows(int firstActiveRow, int activeRowCount) { + + final Range newActiveRange = Range.withLength(firstActiveRow, + activeRowCount); + + // TODO [[Components]] attach and detach components + + /*- + * Example + * + * New Range: [3, 4, 5, 6, 7] + * Old Range: [1, 2, 3, 4, 5] + * Result: [1, 2][3, 4, 5] [] + */ + final Range[] depractionPartition = activeRange + .partitionWith(newActiveRange); + removeValueChangeListeners(depractionPartition[0]); + removeValueChangeListeners(depractionPartition[2]); + + /*- + * Example + * + * Old Range: [1, 2, 3, 4, 5] + * New Range: [3, 4, 5, 6, 7] + * Result: [] [3, 4, 5][6, 7] + */ + final Range[] activationPartition = newActiveRange + .partitionWith(activeRange); + addValueChangeListeners(activationPartition[0]); + addValueChangeListeners(activationPartition[2]); + + activeRange = newActiveRange; + } + + private void addValueChangeListeners(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + + final Object itemId = datasource.getIdByIndex(i); + final Item item = datasource.getItem(itemId); + + if (valueChangeListeners.containsKey(itemId)) { + /* + * This might occur when items are removed from above the + * viewport, the escalator scrolls up to compensate, but the + * same items remain in the view: It looks as if one row was + * scrolled, when in fact the whole viewport was shifted up. + */ + continue; + } + + GridValueChangeListener listener = new GridValueChangeListener( + itemId); + valueChangeListeners.put(itemId, listener); + + for (final Object propertyId : item.getItemPropertyIds()) { + final Property<?> property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .addValueChangeListener(listener); + } + } + } + } + + private void removeValueChangeListeners(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + final Object itemId = datasource.getIdByIndex(i); + final Item item = datasource.getItem(itemId); + final GridValueChangeListener listener = valueChangeListeners + .remove(itemId); + + if (listener != null) { + for (final Object propertyId : item.getItemPropertyIds()) { + final Property<?> property = item + .getItemProperty(propertyId); + + /* + * Because listener != null, we can be certain that this + * property is a ValueChangeNotifier: It wouldn't be + * inserted in addValueChangeListeners if the property + * wasn't a suitable type. I.e. No need for "instanceof" + * check. + */ + ((ValueChangeNotifier) property) + .removeValueChangeListener(listener); + } + } + } + } + + public void clear() { + removeValueChangeListeners(activeRange); + /* + * we're doing an assert for emptiness there (instead of a + * carte-blanche ".clear()"), to be absolutely sure that everything + * is cleaned up properly, and that we have no dangling listeners. + */ + assert valueChangeListeners.isEmpty() : "GridValueChangeListeners are leaking"; + + activeRange = Range.withLength(0, 0); + } + + /** + * Manages removed properties in active rows. + * + * @param removedPropertyIds + * the property ids that have been removed from the container + */ + public void propertiesRemoved(Collection<Object> removedPropertyIds) { + /* + * no-op, for now. + * + * The Container should be responsible for cleaning out any + * ValueChangeListeners from removed Properties. Components will + * benefit from this, however. + */ + } + + /** + * Manages added properties in active rows. + * + * @param addedPropertyIds + * the property ids that have been added to the container + */ + public void propertiesAdded(Collection<Object> addedPropertyIds) { + for (int i = activeRange.getStart(); i < activeRange.getEnd(); i++) { + final Object itemId = datasource.getIdByIndex(i); + final Item item = datasource.getItem(itemId); + final GridValueChangeListener listener = valueChangeListeners + .get(itemId); + assert (listener != null) : "a listener should've been pre-made by addValueChangeListeners"; + + for (final Object propertyId : addedPropertyIds) { + final Property<?> property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .addValueChangeListener(listener); + } + } + } + } + + /** + * Handles the insertion of rows. + * <p> + * This method's responsibilities are to: + * <ul> + * <li>shift the internal bookkeeping by <code>count</code> if the + * insertion happens above currently active range + * <li>ignore rows inserted below the currently active range + * <li>shift (and deactivate) rows pushed out of view + * <li>activate rows that are inserted in the current viewport + * </ul> + * + * @param firstIndex + * the index of the first inserted rows + * @param count + * the number of rows inserted at <code>firstIndex</code> + */ + public void insertRows(int firstIndex, int count) { + if (firstIndex < activeRange.getStart()) { + activeRange = activeRange.offsetBy(count); + } else if (firstIndex < activeRange.getEnd()) { + final Range deprecatedRange = Range.withLength( + activeRange.getEnd(), count); + removeValueChangeListeners(deprecatedRange); + + final Range freshRange = Range.between(firstIndex, count); + addValueChangeListeners(freshRange); + } else { + // out of view, noop + } + } + + /** + * Removes a single item by its id. + * + * @param itemId + * the id of the removed id. <em>Note:</em> this item does + * not exist anymore in the datasource + */ + public void removeItemId(Object itemId) { + final GridValueChangeListener removedListener = valueChangeListeners + .remove(itemId); + if (removedListener != null) { + /* + * We removed an item from somewhere in the visible range, so we + * make the active range shorter. The empty hole will be filled + * by the client-side code when it asks for more information. + */ + activeRange = Range.withLength(activeRange.getStart(), + activeRange.length() - 1); + } + } + } + + /** + * A class to listen to changes in property values in the Container added + * with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies + * the data source to update the client-side representation of the modified + * item. + * <p> + * One instance of this class can (and should) be reused for all the + * properties in an item, since this class will inform that the entire row + * needs to be re-evaluated (in contrast to a property-based change + * management) + * <p> + * Since there's no Container-wide possibility to listen to any kind of + * value changes, an instance of this class needs to be attached to each and + * every Item's Property in the container. + * + * @see Grid#addValueChangeListener(Container, Object, Object) + * @see Grid#valueChangeListeners + */ + private class GridValueChangeListener implements ValueChangeListener { + private final Object itemId; + + public GridValueChangeListener(Object itemId) { + /* + * Using an assert instead of an exception throw, just to optimize + * prematurely + */ + assert itemId != null : "null itemId not accepted"; + this.itemId = itemId; + } + + @Override + public void valueChange(ValueChangeEvent event) { + datasourceExtension.updateRowData(datasource.indexOfId(itemId)); + } + } + + /** * The data source attached to the grid */ private Container.Indexed datasource; @@ -98,13 +387,17 @@ public class Grid extends AbstractComponent { columnKeys.remove(columnId); getState().columns.remove(column.getState()); } + activeRowHandler.propertiesRemoved(removedColumns); // Add new columns + HashSet<Object> addedPropertyIds = new HashSet<Object>(); for (Object propertyId : properties) { if (!columns.containsKey(propertyId)) { appendColumn(propertyId); + addedPropertyIds.add(propertyId); } } + activeRowHandler.propertiesAdded(addedPropertyIds); Object frozenPropertyId = columnKeys .get(getState(false).lastFrozenColumnId); @@ -114,8 +407,53 @@ public class Grid extends AbstractComponent { } }; + private ItemSetChangeListener itemListener = new ItemSetChangeListener() { + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + + if (event instanceof ItemAddEvent) { + ItemAddEvent addEvent = (ItemAddEvent) event; + int firstIndex = addEvent.getFirstIndex(); + int count = addEvent.getAddedItemsCount(); + datasourceExtension.insertRowData(firstIndex, count); + activeRowHandler.insertRows(firstIndex, count); + } + + else if (event instanceof ItemRemoveEvent) { + ItemRemoveEvent removeEvent = (ItemRemoveEvent) event; + int firstIndex = removeEvent.getFirstIndex(); + int count = removeEvent.getRemovedItemsCount(); + datasourceExtension.removeRowData(firstIndex, count); + + /* + * Unfortunately, there's no sane way of getting the rest of the + * removed itemIds. + * + * Fortunately, the only time _currently_ an event with more + * than one removed item seems to be when calling + * AbstractInMemoryContainer.removeAllElements(). Otherwise, + * it's only removing one item at a time. + * + * We _could_ have a backup of all the itemIds, and compare to + * that one, but we really really don't want to go there. + */ + activeRowHandler.removeItemId(removeEvent.getFirstItemId()); + } + + else { + // TODO no diff info available, redraw everything + throw new UnsupportedOperationException("bare " + + "ItemSetChangeEvents are currently " + + "not supported, use a container that " + + "uses AddItemEvents and RemoveItemEvents."); + } + } + }; + private RpcDataProviderExtension datasourceExtension; + private final ActiveRowHandler activeRowHandler = new ActiveRowHandler(); + /** * Creates a new Grid using the given datasource. * @@ -124,6 +462,14 @@ public class Grid extends AbstractComponent { */ public Grid(Container.Indexed datasource) { setContainerDatasource(datasource); + + registerRpc(new GridServerRpc() { + @Override + public void setVisibleRows(int firstVisibleRow, int visibleRowCount) { + activeRowHandler + .setActiveRows(firstVisibleRow, visibleRowCount); + } + }); } /** @@ -143,11 +489,16 @@ public class Grid extends AbstractComponent { return; } - // Remove old listener + // Remove old listeners if (datasource instanceof PropertySetChangeNotifier) { ((PropertySetChangeNotifier) datasource) .removePropertySetChangeListener(propertyListener); } + if (datasource instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) datasource) + .removeItemSetChangeListener(itemListener); + } + activeRowHandler.clear(); if (datasourceExtension != null) { removeExtension(datasourceExtension); @@ -162,6 +513,15 @@ public class Grid extends AbstractComponent { ((PropertySetChangeNotifier) datasource) .addPropertySetChangeListener(propertyListener); } + if (datasource instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) datasource) + .addItemSetChangeListener(itemListener); + } + /* + * activeRowHandler will be updated by the client-side request that + * occurs on container change - no need to actively re-insert any + * ValueChangeListeners at this point. + */ getState().columns.clear(); setLastFrozenPropertyId(null); |