diff options
author | Teemu Suo-Anttila <teemusa@vaadin.com> | 2015-09-29 19:13:36 +0300 |
---|---|---|
committer | Teemu Suo-Anttila <teemusa@vaadin.com> | 2015-10-12 07:18:12 +0000 |
commit | 8a66300779e9dcc949d68ab1a66d445921e0d40b (patch) | |
tree | f08f6b36213e289bcc11595b7ba602cd5b462f5c /server | |
parent | e5634deaa77cdcebc577a1f04d146b960b3b54d1 (diff) | |
download | vaadin-framework-8a66300779e9dcc949d68ab1a66d445921e0d40b.tar.gz vaadin-framework-8a66300779e9dcc949d68ab1a66d445921e0d40b.zip |
Refactor DetailComponentManager to be a static nested class of Grid
While refactoring any special cases are removed. This needs Grid
extensions to have a way for adding and removing components from Grid.
Removing any and all parts of RpcDataProvider work towards having it
separate from Grid and maybe usable for other components as well.
Change-Id: Ia4e25d5f0acaf2085478346b0ff6e23c8334e1b9
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() { |