diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/src/com/vaadin/data/DataGenerator.java | 4 | ||||
-rw-r--r-- | server/src/com/vaadin/data/RpcDataProviderExtension.java | 289 | ||||
-rw-r--r-- | server/src/com/vaadin/ui/Grid.java | 304 |
3 files changed, 272 insertions, 325 deletions
diff --git a/server/src/com/vaadin/data/DataGenerator.java b/server/src/com/vaadin/data/DataGenerator.java index 5e301d8151..fd64a389b8 100644 --- a/server/src/com/vaadin/data/DataGenerator.java +++ b/server/src/com/vaadin/data/DataGenerator.java @@ -47,11 +47,11 @@ public interface DataGenerator extends Serializable { /** * Informs the DataGenerator that an item id has been dropped and is no - * longer needed. + * longer needed. This method should clean up any unneeded stored data + * related to the item. * * @param itemId * removed item id */ public void destroyData(Object itemId); - } diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java index 45caf01587..293940aec7 100644 --- a/server/src/com/vaadin/data/RpcDataProviderExtension.java +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -26,9 +26,6 @@ import java.util.List; import java.util.Map; import java.util.Set; -import com.google.gwt.thirdparty.guava.common.collect.ImmutableSet; -import com.google.gwt.thirdparty.guava.common.collect.Maps; -import com.google.gwt.thirdparty.guava.common.collect.Sets; import com.vaadin.data.Container.Indexed; import com.vaadin.data.Container.Indexed.ItemAddEvent; import com.vaadin.data.Container.Indexed.ItemRemoveEvent; @@ -43,14 +40,10 @@ import com.vaadin.server.ClientConnector; import com.vaadin.server.KeyMapper; import com.vaadin.shared.data.DataProviderRpc; import com.vaadin.shared.data.DataRequestRpc; -import com.vaadin.shared.ui.grid.GridClientRpc; import com.vaadin.shared.ui.grid.GridState; import com.vaadin.shared.ui.grid.Range; -import com.vaadin.ui.Component; import com.vaadin.ui.Grid; import com.vaadin.ui.Grid.Column; -import com.vaadin.ui.Grid.DetailsGenerator; -import com.vaadin.ui.Grid.RowReference; import elemental.json.Json; import elemental.json.JsonArray; @@ -220,167 +213,6 @@ public class RpcDataProviderExtension extends AbstractExtension { } } - /** - * A class that makes detail component related internal communication - * possible between {@link RpcDataProviderExtension} and grid. - * - * @since 7.5.0 - * @author Vaadin Ltd - */ - // TODO this should probably be a static nested class - public final class DetailComponentManager implements DataGenerator { - /** - * This map represents all the components that have been requested for - * each item id. - * <p> - * Normally this map is consistent with what is displayed in the - * component hierarchy (and thus the DOM). The only time this map is out - * of sync with the DOM is between the any calls to - * {@link #createDetails(Object)} or {@link #destroyDetails(Object)}, - * and {@link GridClientRpc#setDetailsConnectorChanges(Set)}. - * <p> - * This is easily checked: if {@link #unattachedComponents} is - * {@link Collection#isEmpty() empty}, then this field is consistent - * with the connector hierarchy. - */ - private final Map<Object, Component> visibleDetailsComponents = Maps - .newHashMap(); - - /** - * Keeps tabs on all the details that did not get a component during - * {@link #createDetails(Object)}. - */ - private final Set<Object> emptyDetails = Sets.newHashSet(); - - private Grid grid; - - /** - * Creates a details component by the request of the client side, with - * the help of the user-defined {@link DetailsGenerator}. - * <p> - * Also keeps internal bookkeeping up to date. - * - * @param itemId - * the item id for which to create the details component. - * Assumed not <code>null</code> and that a component is not - * currently present for this item previously - * @throws IllegalStateException - * if the current details generator provides a component - * that was manually attached, or if the same instance has - * already been provided - */ - public void createDetails(Object itemId) throws IllegalStateException { - assert itemId != null : "itemId was null"; - - if (visibleDetailsComponents.containsKey(itemId) - || emptyDetails.contains(itemId)) { - // Don't overwrite existing components - return; - } - - RowReference rowReference = new RowReference(grid); - rowReference.set(itemId); - - DetailsGenerator detailsGenerator = grid.getDetailsGenerator(); - Component details = detailsGenerator.getDetails(rowReference); - if (details != null) { - if (details.getParent() != null) { - String name = detailsGenerator.getClass().getName(); - throw new IllegalStateException(name - + " generated a details component that already " - + "was attached. (itemId: " + itemId - + ", component: " + details + ")"); - } - - visibleDetailsComponents.put(itemId, details); - - details.setParent(grid); - grid.markAsDirty(); - - assert !emptyDetails.contains(itemId) : "Bookeeping thinks " - + "itemId is empty even though we just created a " - + "component for it (" + itemId + ")"; - } else { - emptyDetails.add(itemId); - } - - } - - /** - * Destroys correctly a details component, by the request of the client - * side. - * <p> - * Also keeps internal bookkeeping up to date. - * - * @param itemId - * the item id for which to destroy the details component - */ - public void destroyDetails(Object itemId) { - emptyDetails.remove(itemId); - - Component removedComponent = visibleDetailsComponents - .remove(itemId); - if (removedComponent == null) { - return; - } - - removedComponent.setParent(null); - grid.markAsDirty(); - } - - /** - * Gets all details components that are currently attached to the grid. - * <p> - * Used internally by the Grid object. - * - * @return all details components that are currently attached to the - * grid - */ - public Collection<Component> getComponents() { - Set<Component> components = new HashSet<Component>( - visibleDetailsComponents.values()); - return components; - } - - public void refresh(Object itemId) { - destroyDetails(itemId); - createDetails(itemId); - } - - void setGrid(Grid grid) { - if (this.grid != null) { - throw new IllegalStateException("Grid may injected only once."); - } - this.grid = grid; - } - - /** - * {@inheritDoc} - * - * @since 7.6 - */ - @Override - public void generateData(Object itemId, Item item, JsonObject rowData) { - if (visibleDetails.contains(itemId)) { - // Double check to be sure details component exists. - detailComponentManager.createDetails(itemId); - Component detailsComponent = visibleDetailsComponents - .get(itemId); - rowData.put( - GridState.JSONKEY_DETAILS_VISIBLE, - (detailsComponent != null ? detailsComponent - .getConnectorId() : "")); - } - } - - @Override - public void destroyData(Object itemId) { - if (visibleDetails.contains(itemId)) { - destroyDetails(itemId); - } - } - } - private final Indexed container; private DataProviderRpc rpc; @@ -404,49 +236,6 @@ public class RpcDataProviderExtension extends AbstractExtension { } else { - - /* - * Clear everything we have in view, and let the client - * re-request for whatever it needs. - * - * Why this shortcut? Well, since anything could've happened, we - * don't know what has happened. There are a lot of use-cases we - * can cover at once with this carte blanche operation: - * - * 1) Grid is scrolled somewhere in the middle and all the - * rows-inview are removed. We need a new pageful. - * - * 2) Grid is scrolled somewhere in the middle and none of the - * visible rows are removed. We need no new rows. - * - * 3) Grid is scrolled all the way to the bottom, and the last - * rows are being removed. Grid needs to scroll up and request - * for more rows at the top. - * - * 4) Grid is scrolled pretty much to the bottom, and the last - * rows are being removed. Grid needs to be aware that some - * scrolling is needed, but not to compensate for all the - * removed rows. And it also needs to request for some more rows - * to the top. - * - * 5) Some ranges of rows are removed from view. We need to - * collapse the gaps with existing rows and load the missing - * rows. - * - * 6) The ultimate use case! Grid has 1.5 pages of rows and - * scrolled a bit down. One page of rows is removed. We need to - * make sure that new rows are loaded, but not all old slots are - * occupied, since the page can't be filled with new row data. - * It also needs to be scrolled to the top. - * - * So, it's easier (and safer) to do the simple thing instead of - * taking all the corner cases into account. - */ - - for (Object itemId : visibleDetails) { - detailComponentManager.destroyDetails(itemId); - } - /* Mark as dirty to push changes in beforeClientResponse */ bareItemSetTriggeredSizeChange = true; markAsDirty(); @@ -469,15 +258,6 @@ public class RpcDataProviderExtension extends AbstractExtension { /** Size possibly changed with a bare ItemSetChangeEvent */ private boolean bareItemSetTriggeredSizeChange = false; - /** - * This map represents all the details that are user-defined as visible. - * This does not reflect the status in the DOM. - */ - // TODO this should probably be inside DetailComponentManager - private final Set<Object> visibleDetails = new HashSet<Object>(); - - private final DetailComponentManager detailComponentManager = new DetailComponentManager(); - private final Set<DataGenerator> dataGenerators = new LinkedHashSet<DataGenerator>(); private final ActiveItemHandler activeItemHandler = new ActiveItemHandler(); @@ -515,7 +295,6 @@ public class RpcDataProviderExtension extends AbstractExtension { } addDataGenerator(activeItemHandler); - addDataGenerator(detailComponentManager); } /** @@ -612,7 +391,6 @@ public class RpcDataProviderExtension extends AbstractExtension { * the key mapper for columns */ public void extend(Grid component) { - detailComponentManager.setGrid(component); super.extend(component); } @@ -809,71 +587,4 @@ public class RpcDataProviderExtension extends AbstractExtension { protected Grid getGrid() { return (Grid) getParent(); } - - /** - * Marks a row's details to be visible or hidden. - * <p> - * If that row is currently in the client side's cache, this information - * will be sent over to the client. - * - * @since 7.5.0 - * @param itemId - * the id of the item of which to change the details visibility - * @param visible - * <code>true</code> to show the details, <code>false</code> to - * hide - */ - public void setDetailsVisible(Object itemId, boolean visible) { - if (visible) { - visibleDetails.add(itemId); - - /* - * This might be an issue with a huge number of open rows, but as of - * now this works in most of the cases. - */ - detailComponentManager.createDetails(itemId); - } else { - visibleDetails.remove(itemId); - - detailComponentManager.destroyDetails(itemId); - } - - updateRowData(itemId); - } - - /** - * Checks whether the details for a row is marked as visible. - * - * @since 7.5.0 - * @param itemId - * the id of the item of which to check the visibility - * @return <code>true</code> iff the detials are visible for the item. This - * might return <code>true</code> even if the row is not currently - * visible in the DOM - */ - public boolean isDetailsVisible(Object itemId) { - return visibleDetails.contains(itemId); - } - - /** - * Refreshes all visible detail sections. - * - * @since 7.5.0 - */ - public void refreshDetails() { - for (Object itemId : ImmutableSet.copyOf(visibleDetails)) { - detailComponentManager.refresh(itemId); - updateRowData(itemId); - } - } - - /** - * Gets the detail component manager for this data provider - * - * @since 7.5.0 - * @return the detail component manager - * */ - public DetailComponentManager getDetailComponentManager() { - return detailComponentManager; - } } diff --git a/server/src/com/vaadin/ui/Grid.java b/server/src/com/vaadin/ui/Grid.java index 6e1520e028..f7832ece40 100644 --- a/server/src/com/vaadin/ui/Grid.java +++ b/server/src/com/vaadin/ui/Grid.java @@ -42,6 +42,7 @@ import org.jsoup.nodes.Attributes; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; +import com.google.gwt.thirdparty.guava.common.collect.Maps; import com.google.gwt.thirdparty.guava.common.collect.Sets; import com.google.gwt.thirdparty.guava.common.collect.Sets.SetView; import com.vaadin.data.Container; @@ -57,7 +58,6 @@ import com.vaadin.data.DataGenerator; import com.vaadin.data.Item; import com.vaadin.data.Property; import com.vaadin.data.RpcDataProviderExtension; -import com.vaadin.data.RpcDataProviderExtension.DetailComponentManager; import com.vaadin.data.Validator.InvalidValueException; import com.vaadin.data.fieldgroup.DefaultFieldGroupFieldFactory; import com.vaadin.data.fieldgroup.FieldGroup; @@ -281,15 +281,14 @@ public class Grid extends AbstractFocusable implements SelectionNotifier, }; /** - * This method is called for whenever a new details row needs to be - * generated. + * This method is called for whenever a details row needs to be shown on + * the client. Grid removes all of its references to details components + * when they are no longer displayed on the client-side and will + * re-request once needed again. * <p> * <em>Note:</em> If a component gets generated, it may not be manually - * attached anywhere, nor may it be a reused instance – each - * invocation of this method should produce a unique and isolated - * component instance. Essentially, this should mostly be a - * self-contained fire-and-forget method, as external references to the - * generated component might cause unexpected behavior. + * attached anywhere. The same details component can not be displayed + * for multiple different rows. * * @param rowReference * the reference for the row for which to generate details @@ -300,6 +299,225 @@ public class Grid extends AbstractFocusable implements SelectionNotifier, } /** + * A class that manages details components by calling + * {@link DetailsGenerator} as needed. Details components are attached by + * this class when the {@link RpcDataProviderExtension} is sending data to + * the client. Details components are detached and forgotten when client + * informs that it has dropped the corresponding item. + * + * @since + */ + private final static class DetailComponentManager extends + AbstractGridExtension implements DataGenerator { + + /** + * The user-defined details generator. + * + * @see #setDetailsGenerator(DetailsGenerator) + */ + private DetailsGenerator detailsGenerator = DetailsGenerator.NULL; + + /** + * This map represents all details that are currently visible on the + * client. Details components get destroyed once they scroll out of + * view. + */ + private final Map<Object, Component> itemIdToDetailsComponent = Maps + .newHashMap(); + + /** + * Set of item ids that got <code>null</code> from DetailsGenerator when + * {@link DetailsGenerator#getDetails(RowReference)} was called. + */ + private final Set<Object> emptyDetails = new HashSet<Object>(); + + /** + * Set of item IDs for all open details rows. Contains even the ones + * that are not currently visible on the client. + */ + private final Set<Object> openDetails = new HashSet<Object>(); + + public DetailComponentManager(Grid grid) { + super(grid); + } + + /** + * Creates a details component with the help of the user-defined + * {@link DetailsGenerator}. + * <p> + * This method attaches created components to the parent {@link Grid}. + * + * @param itemId + * the item id for which to create the details component. + * @throws IllegalStateException + * if the current details generator provides a component + * that was manually attached. + */ + private void createDetails(Object itemId) throws IllegalStateException { + assert itemId != null : "itemId was null"; + + if (itemIdToDetailsComponent.containsKey(itemId) + || emptyDetails.contains(itemId)) { + // Don't overwrite existing components + return; + } + + RowReference rowReference = new RowReference(getParentGrid()); + rowReference.set(itemId); + + DetailsGenerator detailsGenerator = getParentGrid() + .getDetailsGenerator(); + Component details = detailsGenerator.getDetails(rowReference); + if (details != null) { + if (details.getParent() != null) { + String name = detailsGenerator.getClass().getName(); + throw new IllegalStateException(name + + " generated a details component that already " + + "was attached. (itemId: " + itemId + + ", component: " + details + ")"); + } + + itemIdToDetailsComponent.put(itemId, details); + + addComponentToGrid(details); + + assert !emptyDetails.contains(itemId) : "Bookeeping thinks " + + "itemId is empty even though we just created a " + + "component for it (" + itemId + ")"; + } else { + emptyDetails.add(itemId); + } + + } + + /** + * Destroys a details component correctly. + * <p> + * This method will detach the component from parent {@link Grid}. + * + * @param itemId + * the item id for which to destroy the details component + */ + private void destroyDetails(Object itemId) { + emptyDetails.remove(itemId); + + Component removedComponent = itemIdToDetailsComponent + .remove(itemId); + if (removedComponent == null) { + return; + } + + removeComponentFromGrid(removedComponent); + } + + /** + * Recreates all visible details components. + */ + public void refreshDetails() { + Set<Object> visibleItemIds = new HashSet<Object>( + itemIdToDetailsComponent.keySet()); + for (Object itemId : visibleItemIds) { + destroyDetails(itemId); + createDetails(itemId); + refreshRow(itemId); + } + } + + /** + * Sets details visiblity status of given item id. + * + * @param itemId + * item id to set + * @param visible + * <code>true</code> if visible; <code>false</code> if not + */ + public void setDetailsVisible(Object itemId, boolean visible) { + if ((visible && openDetails.contains(itemId)) + || (!visible && !openDetails.contains(itemId))) { + return; + } + + if (visible) { + openDetails.add(itemId); + refreshRow(itemId); + } else { + openDetails.remove(itemId); + destroyDetails(itemId); + refreshRow(itemId); + } + } + + @Override + public void generateData(Object itemId, Item item, JsonObject rowData) { + // DetailComponentManager should not send anything if details + // generator is the default null version. + if (openDetails.contains(itemId) + && !detailsGenerator.equals(DetailsGenerator.NULL)) { + // Double check to be sure details component exists. + createDetails(itemId); + + Component detailsComponent = itemIdToDetailsComponent + .get(itemId); + rowData.put( + GridState.JSONKEY_DETAILS_VISIBLE, + (detailsComponent != null ? detailsComponent + .getConnectorId() : "")); + } + } + + @Override + public void destroyData(Object itemId) { + if (openDetails.contains(itemId)) { + destroyDetails(itemId); + } + } + + /** + * Sets a new details generator for row details. + * <p> + * The currently opened row details will be re-rendered. + * + * @param detailsGenerator + * the details generator to set + * @throws IllegalArgumentException + * if detailsGenerator is <code>null</code>; + */ + public void setDetailsGenerator(DetailsGenerator detailsGenerator) + throws IllegalArgumentException { + if (detailsGenerator == null) { + throw new IllegalArgumentException( + "Details generator may not be null"); + } else if (detailsGenerator == this.detailsGenerator) { + return; + } + + this.detailsGenerator = detailsGenerator; + + refreshDetails(); + } + + /** + * Gets the current details generator for row details. + * + * @return the detailsGenerator the current details generator + */ + public DetailsGenerator getDetailsGenerator() { + return detailsGenerator; + } + + /** + * Checks whether details are visible for the given item. + * + * @param itemId + * the id of the item for which to check details visibility + * @return <code>true</code> iff the details are visible + */ + public boolean isDetailsVisible(Object itemId) { + return openDetails.contains(itemId); + } + } + + /** * Custom field group that allows finding property types before an item has * been bound. */ @@ -4119,6 +4337,30 @@ public class Grid extends AbstractFocusable implements SelectionNotifier, protected void refreshRow(Object itemId) { getParentGrid().datasourceExtension.updateRowData(itemId); } + + /** + * Informs the parent Grid that this Extension wants to add a child + * component to it. + * + * @since + * @param c + * component + */ + protected void addComponentToGrid(Component c) { + getParentGrid().addComponent(c); + } + + /** + * Informs the parent Grid that this Extension wants to remove a child + * component from it. + * + * @since + * @param c + * component + */ + protected void removeComponentFromGrid(Component c) { + getParentGrid().removeComponent(c); + } } /** @@ -4241,15 +4483,10 @@ public class Grid extends AbstractFocusable implements SelectionNotifier, private EditorErrorHandler editorErrorHandler = new DefaultEditorErrorHandler(); - /** - * The user-defined details generator. - * - * @see #setDetailsGenerator(DetailsGenerator) - */ - private DetailsGenerator detailsGenerator = DetailsGenerator.NULL; - private DetailComponentManager detailComponentManager = null; + private Set<Component> extensionComponents = new HashSet<Component>(); + private static final Method SELECTION_CHANGE_METHOD = ReflectTools .findMethod(SelectionListener.class, "select", SelectionEvent.class); @@ -4637,8 +4874,7 @@ public class Grid extends AbstractFocusable implements SelectionNotifier, datasourceExtension.extend(this); datasourceExtension.addDataGenerator(new RowDataGenerator()); - detailComponentManager = datasourceExtension - .getDetailComponentManager(); + detailComponentManager = new DetailComponentManager(this); /* * selectionModel == null when the invocation comes from the @@ -6093,6 +6329,18 @@ public class Grid extends AbstractFocusable implements SelectionNotifier, return footer.isVisible(); } + private void addComponent(Component c) { + extensionComponents.add(c); + c.setParent(this); + markAsDirty(); + } + + private void removeComponent(Component c) { + extensionComponents.remove(c); + c.setParent(null); + markAsDirty(); + } + @Override public Iterator<Component> iterator() { // This is a hash set to avoid adding header/footer components inside @@ -6123,7 +6371,7 @@ public class Grid extends AbstractFocusable implements SelectionNotifier, componentList.addAll(getEditorFields()); - componentList.addAll(detailComponentManager.getComponents()); + componentList.addAll(extensionComponents); return componentList.iterator(); } @@ -6818,16 +7066,7 @@ public class Grid extends AbstractFocusable implements SelectionNotifier, */ public void setDetailsGenerator(DetailsGenerator detailsGenerator) throws IllegalArgumentException { - if (detailsGenerator == null) { - throw new IllegalArgumentException( - "Details generator may not be null"); - } else if (detailsGenerator == this.detailsGenerator) { - return; - } - - this.detailsGenerator = detailsGenerator; - - datasourceExtension.refreshDetails(); + detailComponentManager.setDetailsGenerator(detailsGenerator); } /** @@ -6837,7 +7076,7 @@ public class Grid extends AbstractFocusable implements SelectionNotifier, * @return the detailsGenerator the current details generator */ public DetailsGenerator getDetailsGenerator() { - return detailsGenerator; + return detailComponentManager.getDetailsGenerator(); } /** @@ -6851,10 +7090,7 @@ public class Grid extends AbstractFocusable implements SelectionNotifier, * to hide them */ public void setDetailsVisible(Object itemId, boolean visible) { - if (DetailsGenerator.NULL.equals(detailsGenerator)) { - return; - } - datasourceExtension.setDetailsVisible(itemId, visible); + detailComponentManager.setDetailsVisible(itemId, visible); } /** @@ -6866,7 +7102,7 @@ public class Grid extends AbstractFocusable implements SelectionNotifier, * @return <code>true</code> iff the details are visible */ public boolean isDetailsVisible(Object itemId) { - return datasourceExtension.isDetailsVisible(itemId); + return detailComponentManager.isDetailsVisible(itemId); } private static SelectionMode getDefaultSelectionMode() { |