diff options
14 files changed, 761 insertions, 39 deletions
diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java index ff8847ea44..127eb80696 100644 --- a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java +++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java @@ -22,7 +22,7 @@ import java.util.List; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.vaadin.client.Profiler; -import com.vaadin.client.ui.grid.Range; +import com.vaadin.shared.ui.grid.Range; /** * Base implementation for data sources that fetch data from a remote system. @@ -238,4 +238,86 @@ public abstract class AbstractRemoteDataSource<T> implements DataSource<T> { Profiler.leave("AbstractRemoteDataSource.setRowData"); } + + /** + * Informs this data source that the server has removed data. + * + * @param firstRowIndex + * the index of the first removed row + * @param count + * the number of removed rows, starting from + * <code>firstRowIndex</code> + */ + protected void removeRowData(int firstRowIndex, int count) { + Profiler.enter("AbstractRemoteDataSource.removeRowData"); + + // pack the cached data + for (int i = 0; i < count; i++) { + Integer oldIndex = Integer.valueOf(firstRowIndex + count + i); + if (rowCache.containsKey(oldIndex)) { + Integer newIndex = Integer.valueOf(firstRowIndex + i); + rowCache.put(newIndex, rowCache.remove(oldIndex)); + } + } + + Range removedRange = Range.withLength(firstRowIndex, count); + if (removedRange.intersects(cached)) { + Range[] partitions = cached.partitionWith(removedRange); + Range remainsBefore = partitions[0]; + Range transposedRemainsAfter = partitions[2].offsetBy(-removedRange + .length()); + cached = remainsBefore.combineWith(transposedRemainsAfter); + } + estimatedSize -= count; + dataChangeHandler.dataRemoved(firstRowIndex, count); + checkCacheCoverage(); + + Profiler.leave("AbstractRemoteDataSource.removeRowData"); + } + + /** + * Informs this data source that new data has been inserted from the server. + * + * @param firstRowIndex + * the destination index of the new row data + * @param count + * the number of rows inserted + */ + protected void insertRowData(int firstRowIndex, int count) { + Profiler.enter("AbstractRemoteDataSource.insertRowData"); + + if (cached.contains(firstRowIndex)) { + int oldCacheEnd = cached.getEnd(); + /* + * We need to invalidate the cache from the inserted row onwards, + * since the cache wants to be a contiguous range. It doesn't + * support holes. + * + * If holes were supported, we could shift the higher part of + * "cached" and leave a hole the size of "count" in the middle. + */ + cached = cached.splitAt(firstRowIndex)[0]; + + for (int i = firstRowIndex; i < oldCacheEnd; i++) { + rowCache.remove(Integer.valueOf(i)); + } + } + + else if (firstRowIndex < cached.getStart()) { + Range oldCached = cached; + cached = cached.offsetBy(count); + + for (int i = 0; i < rowCache.size(); i++) { + Integer oldIndex = Integer.valueOf(oldCached.getEnd() - i); + Integer newIndex = Integer.valueOf(cached.getEnd() - i); + rowCache.put(newIndex, rowCache.remove(oldIndex)); + } + } + + estimatedSize += count; + dataChangeHandler.dataAdded(firstRowIndex, count); + checkCacheCoverage(); + + Profiler.leave("AbstractRemoteDataSource.insertRowData"); + } } diff --git a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java index 1785fc62c2..4d22c10197 100644 --- a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java +++ b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java @@ -56,6 +56,16 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { public void setRowData(int firstRow, List<String[]> rows) { dataSource.setRowData(firstRow, rows); } + + @Override + public void removeRowData(int firstRow, int count) { + dataSource.removeRowData(firstRow, count); + } + + @Override + public void insertRowData(int firstRow, int count) { + dataSource.insertRowData(firstRow, count); + } }); } diff --git a/client/src/com/vaadin/client/ui/grid/Escalator.java b/client/src/com/vaadin/client/ui/grid/Escalator.java index 20a187e1a5..a395038890 100644 --- a/client/src/com/vaadin/client/ui/grid/Escalator.java +++ b/client/src/com/vaadin/client/ui/grid/Escalator.java @@ -35,12 +35,12 @@ import com.google.gwt.dom.client.NodeList; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.user.client.ui.Widget; -import com.google.web.bindery.event.shared.HandlerRegistration; import com.vaadin.client.Profiler; import com.vaadin.client.Util; import com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle; @@ -50,6 +50,7 @@ import com.vaadin.client.ui.grid.PositionFunction.TranslatePosition; import com.vaadin.client.ui.grid.PositionFunction.WebkitTranslate3DPosition; import com.vaadin.client.ui.grid.ScrollbarBundle.HorizontalScrollbarBundle; import com.vaadin.client.ui.grid.ScrollbarBundle.VerticalScrollbarBundle; +import com.vaadin.shared.ui.grid.Range; import com.vaadin.shared.util.SharedUtil; /*- @@ -1633,6 +1634,8 @@ public class Escalator extends Widget { return; } + boolean rowsWereMoved = false; + final int topRowPos = getRowTop(visualRowOrder.getFirst()); // TODO [[mpixscroll]] final int scrollTop = tBodyScrollTop; @@ -1655,6 +1658,8 @@ public class Escalator extends Widget { final int logicalRowIndex = scrollTop / ROW_HEIGHT_PX; moveAndUpdateEscalatorRows(Range.between(start, end), 0, logicalRowIndex); + + rowsWereMoved = (rowsToMove != 0); } else if (viewportOffset + ROW_HEIGHT_PX <= 0) { @@ -1723,9 +1728,13 @@ public class Escalator extends Widget { .get(1)) - 1; moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex); } + + rowsWereMoved = (rowsToMove != 0); } - fireRowVisibilityChangeEvent(); + if (rowsWereMoved) { + fireRowVisibilityChangeEvent(); + } } @Override @@ -1805,6 +1814,8 @@ public class Escalator extends Widget { setRowPosition(tr, 0, rowTop); rowTop += ROW_HEIGHT_PX; } + + fireRowVisibilityChangeEvent(); } return addedRows; } @@ -1919,8 +1930,6 @@ public class Escalator extends Widget { newRowTop += ROW_HEIGHT_PX; } } - - fireRowVisibilityChangeEvent(); } /** @@ -3181,9 +3190,15 @@ public class Escalator extends Widget { */ @Override public void setHeight(final String height) { + final int escalatorRowsBefore = body.visualRowOrder.size(); + super.setHeight(height != null && !height.isEmpty() ? height : DEFAULT_HEIGHT); recalculateElementSizes(); + + if (escalatorRowsBefore != body.visualRowOrder.size()) { + fireRowVisibilityChangeEvent(); + } } /** @@ -3437,26 +3452,30 @@ public class Escalator extends Widget { * Adds an event handler that gets notified when the range of visible rows * changes e.g. because of scrolling. * - * @param rowVisibilityChangeHadler + * @param rowVisibilityChangeHandler * the event handler * @return a handler registration for the added handler */ public HandlerRegistration addRowVisibilityChangeHandler( - RowVisibilityChangeHandler rowVisibilityChangeHadler) { - return addHandler(rowVisibilityChangeHadler, + RowVisibilityChangeHandler rowVisibilityChangeHandler) { + return addHandler(rowVisibilityChangeHandler, RowVisibilityChangeEvent.TYPE); } private void fireRowVisibilityChangeEvent() { - int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder - .getFirst()); - int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder - .getLast()) + 1; + if (!body.visualRowOrder.isEmpty()) { + int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder + .getFirst()); + int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder + .getLast()) + 1; - int visibleRowCount = visibleRangeEnd - visibleRangeStart; + int visibleRowCount = visibleRangeEnd - visibleRangeStart; - fireEvent(new RowVisibilityChangeEvent(visibleRangeStart, - visibleRowCount)); + fireEvent(new RowVisibilityChangeEvent(visibleRangeStart, + visibleRowCount)); + } else { + fireEvent(new RowVisibilityChangeEvent(0, 0)); + } } /** diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java index 2dbb0275cd..7f8ab408a9 100644 --- a/client/src/com/vaadin/client/ui/grid/Grid.java +++ b/client/src/com/vaadin/client/ui/grid/Grid.java @@ -21,6 +21,7 @@ import java.util.List; import com.google.gwt.core.shared.GWT; import com.google.gwt.dom.client.Element; +import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HasVisibility; import com.google.gwt.user.client.ui.Widget; @@ -73,6 +74,10 @@ public class Grid<T> extends Composite { */ private final List<GridColumn<?, T>> columns = new ArrayList<GridColumn<?, T>>(); + /** + * The datasource currently in use. <em>Note:</em> it is <code>null</code> + * on initialization, but not after that. + */ private DataSource<T> dataSource; /** @@ -1211,4 +1216,14 @@ public class Grid<T> extends Composite { public GridColumn<?, T> getLastFrozenColumn() { return lastFrozenColumn; } + + public HandlerRegistration addRowVisibilityChangeHandler( + RowVisibilityChangeHandler handler) { + /* + * Reusing Escalator's RowVisibilityChangeHandler, since a scroll + * concept is too abstract. e.g. the event needs to be re-sent when the + * widget is resized. + */ + return escalator.addRowVisibilityChangeHandler(handler); + } } diff --git a/client/src/com/vaadin/client/ui/grid/GridConnector.java b/client/src/com/vaadin/client/ui/grid/GridConnector.java index ffe1444942..f04326c7e6 100644 --- a/client/src/com/vaadin/client/ui/grid/GridConnector.java +++ b/client/src/com/vaadin/client/ui/grid/GridConnector.java @@ -30,6 +30,7 @@ import com.vaadin.shared.ui.Connect; import com.vaadin.shared.ui.grid.ColumnGroupRowState; import com.vaadin.shared.ui.grid.ColumnGroupState; import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; /** @@ -83,6 +84,21 @@ public class GridConnector extends AbstractComponentConnector { } @Override + protected void init() { + super.init(); + getWidget().addRowVisibilityChangeHandler( + new RowVisibilityChangeHandler() { + @Override + public void onRowVisibilityChange( + RowVisibilityChangeEvent event) { + getRpcProxy(GridServerRpc.class).setVisibleRows( + event.getFirstVisibleRow(), + event.getVisibleRowCount()); + } + }); + } + + @Override public void onStateChanged(StateChangeEvent stateChangeEvent) { super.onStateChanged(stateChangeEvent); diff --git a/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java index 3cbc6351b1..e97bb339e4 100644 --- a/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java +++ b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java @@ -21,6 +21,8 @@ import static org.junit.Assert.assertTrue; import org.junit.Test; +import com.vaadin.shared.ui.grid.Range; + @SuppressWarnings("static-method") public class PartitioningTest { 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); diff --git a/shared/src/com/vaadin/shared/data/DataProviderRpc.java b/shared/src/com/vaadin/shared/data/DataProviderRpc.java index 7d82ecc342..79e3f17f8d 100644 --- a/shared/src/com/vaadin/shared/data/DataProviderRpc.java +++ b/shared/src/com/vaadin/shared/data/DataProviderRpc.java @@ -37,4 +37,25 @@ public interface DataProviderRpc extends ClientRpc { * the updated row data */ public void setRowData(int firstRowIndex, List<String[]> rowData); + + /** + * Informs the client to remove row data. + * + * @param firstRowIndex + * the index of the first removed row + * @param count + * the number of rows removed from <code>firstRowIndex</code> and + * onwards + */ + public void removeRowData(int firstRowIndex, int count); + + /** + * Informs the client to insert new row data. + * + * @param firstRowIndex + * the index of the first new row + * @param count + * the number of rows inserted at <code>firstRowIndex</code> + */ + public void insertRowData(int firstRowIndex, int count); } diff --git a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java new file mode 100644 index 0000000000..db0a31ed2c --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2013 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.shared.ui.grid; + +import com.vaadin.shared.communication.ServerRpc; + +/** + * TODO + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface GridServerRpc extends ServerRpc { + + /** + * TODO + * + * @param firstVisibleRow + * the index of the first visible row + * @param visibleRowCount + * the number of rows visible, counted from + * <code>firstVisibleRow</code> + */ + void setVisibleRows(int firstVisibleRow, int visibleRowCount); + +} diff --git a/client/src/com/vaadin/client/ui/grid/Range.java b/shared/src/com/vaadin/shared/ui/grid/Range.java index 634a182421..3114a79c82 100644 --- a/client/src/com/vaadin/client/ui/grid/Range.java +++ b/shared/src/com/vaadin/shared/ui/grid/Range.java @@ -14,7 +14,7 @@ * the License. */ -package com.vaadin.client.ui.grid; +package com.vaadin.shared.ui.grid; /** * An immutable representation of a range, marked by start and end points. diff --git a/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java index d73b0fb02f..b042cee509 100644 --- a/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java +++ b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.vaadin.client.ui.grid; +package com.vaadin.shared.ui.grid; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java index 82b2d7a4e8..c28feb8d10 100644 --- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java @@ -40,20 +40,22 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { private final int ROWS = 1000; + private IndexedContainer ds; + @Override protected Grid constructComponent() { // Build data source - IndexedContainer ds = new IndexedContainer(); + ds = new IndexedContainer(); for (int col = 0; col < COLUMNS; col++) { - ds.addContainerProperty("Column" + col, String.class, ""); + ds.addContainerProperty(getColumnProperty(col), String.class, ""); } for (int row = 0; row < ROWS; row++) { Item item = ds.addItem(Integer.valueOf(row)); for (int col = 0; col < COLUMNS; col++) { - item.getItemProperty("Column" + col).setValue( + item.getItemProperty(getColumnProperty(col)).setValue( "(" + row + ", " + col + ")"); } } @@ -63,7 +65,8 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { // Add footer values (header values are automatically created) for (int col = 0; col < COLUMNS; col++) { - grid.getColumn("Column" + col).setFooterCaption("Footer " + col); + grid.getColumn(getColumnProperty(col)).setFooterCaption( + "Footer " + col); } // Set varying column widths @@ -81,6 +84,8 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { createColumnGroupActions(); + createRowActions(); + return grid; } @@ -131,9 +136,9 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { createCategory("Columns", null); for (int c = 0; c < COLUMNS; c++) { - createCategory("Column" + c, "Columns"); + createCategory(getColumnProperty(c), "Columns"); - createBooleanAction("Visible", "Column" + c, true, + createBooleanAction("Visible", getColumnProperty(c), true, new Command<Grid, Boolean>() { @Override @@ -148,7 +153,7 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { } }, c); - createClickAction("Remove", "Column" + c, + createClickAction("Remove", getColumnProperty(c), new Command<Grid, String>() { @Override @@ -158,7 +163,7 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { } }, null, c); - createClickAction("Freeze", "Column" + c, + createClickAction("Freeze", getColumnProperty(c), new Command<Grid, String>() { @Override @@ -167,7 +172,7 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { } }, null, c); - createCategory("Column" + c + " Width", "Column" + c); + createCategory("Column" + c + " Width", getColumnProperty(c)); createClickAction("Auto", "Column" + c + " Width", new Command<Grid, Integer>() { @@ -203,6 +208,10 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { } } + private static String getColumnProperty(int c) { + return "Column" + c; + } + protected void createColumnGroupActions() { createCategory("Column groups", null); @@ -269,6 +278,58 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> { } + protected void createRowActions() { + createCategory("Body rows", null); + + createClickAction("Add first row", "Body rows", + new Command<Grid, String>() { + @Override + public void execute(Grid c, String value, Object data) { + Item item = ds.addItemAt(0, new Object()); + for (int i = 0; i < COLUMNS; i++) { + item.getItemProperty(getColumnProperty(i)) + .setValue("newcell: " + i); + } + } + }, null); + + createClickAction("Remove first row", "Body rows", + new Command<Grid, String>() { + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + ds.removeItem(firstItemId); + } + }, null); + + createClickAction("Modify first row (getItemProperty)", "Body rows", + new Command<Grid, String>() { + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + Item item = ds.getItem(firstItemId); + for (int i = 0; i < COLUMNS; i++) { + item.getItemProperty(getColumnProperty(i)) + .setValue("modified: " + i); + } + } + }, null); + + createClickAction("Modify first row (getContainerProperty)", + "Body rows", new Command<Grid, String>() { + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + for (Object containerPropertyId : ds + .getContainerPropertyIds()) { + ds.getContainerProperty(firstItemId, + containerPropertyId).setValue( + "modified: " + containerPropertyId); + } + } + }, null); + } + @Override protected Integer getTicketNumber() { return 12829; diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java index 8beee46156..bc43f2be98 100644 --- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java @@ -22,6 +22,7 @@ import java.util.List; import org.junit.Test; import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; import org.openqa.selenium.interactions.Actions; @@ -253,6 +254,55 @@ public class GridBasicFeaturesTest extends MultiBrowserTest { assertPrimaryStylename("v-grid"); } + /** + * Test that the current view is updated when a server-side container change + * occurs (without scrolling back and forth) + */ + @Test + public void testItemSetChangeEvent() throws Exception { + openTestURL(); + + final By newRow = By.xpath("//td[text()='newcell: 0']"); + + assertTrue("Unexpected initial state", !elementIsFound(newRow)); + + selectMenuPath("Component", "Body rows", "Add first row"); + assertTrue("Add row failed", elementIsFound(newRow)); + + selectMenuPath("Component", "Body rows", "Remove first row"); + assertTrue("Remove row failed", !elementIsFound(newRow)); + } + + /** + * Test that the current view is updated when a property's value is reflect + * to the client, when the value is modified server-side. + */ + @Test + public void testPropertyValueChangeEvent() throws Exception { + openTestURL(); + + assertEquals("Unexpected cell initial state", "(0, 0)", + getBodyCellByRowAndColumn(1, 1).getText()); + + selectMenuPath("Component", "Body rows", + "Modify first row (getItemProperty)"); + assertEquals("(First) modification with getItemProperty failed", + "modified: 0", getBodyCellByRowAndColumn(1, 1).getText()); + + selectMenuPath("Component", "Body rows", + "Modify first row (getContainerProperty)"); + assertEquals("(Second) modification with getItemProperty failed", + "modified: Column0", getBodyCellByRowAndColumn(1, 1).getText()); + } + + private boolean elementIsFound(By locator) { + try { + return driver.findElement(locator) != null; + } catch (NoSuchElementException e) { + return false; + } + } + private void assertPrimaryStylename(String stylename) { assertTrue(getGridElement().getAttribute("class").contains(stylename)); |