From d1864557a59c66f4e0f7d6fd5b6e10e5bba1a0fe Mon Sep 17 00:00:00 2001 From: caalador Date: Wed, 4 Apr 2018 15:18:57 +0300 Subject: [PATCH] Added possibility to add listener for connectorMarkedDirty (#10773) --- .../event/MarkedAsDirtyConnectorEvent.java | 44 +++++ .../vaadin/event/MarkedAsDirtyListener.java | 33 ++++ .../java/com/vaadin/ui/ConnectorTracker.java | 47 +++++- .../tests/event/MarkAsDirtyListenerTest.java | 155 ++++++++++++++++++ 4 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 server/src/main/java/com/vaadin/event/MarkedAsDirtyConnectorEvent.java create mode 100644 server/src/main/java/com/vaadin/event/MarkedAsDirtyListener.java create mode 100644 server/src/test/java/com/vaadin/tests/event/MarkAsDirtyListenerTest.java diff --git a/server/src/main/java/com/vaadin/event/MarkedAsDirtyConnectorEvent.java b/server/src/main/java/com/vaadin/event/MarkedAsDirtyConnectorEvent.java new file mode 100644 index 0000000000..7a18cbd16c --- /dev/null +++ b/server/src/main/java/com/vaadin/event/MarkedAsDirtyConnectorEvent.java @@ -0,0 +1,44 @@ +/* + * Copyright 2000-2018 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.event; + +import com.vaadin.server.ClientConnector; +import com.vaadin.ui.UI; + +/** + * Event which is fired for all registered MarkDirtyListeners when a + * connector is marked as dirty. + * + * @since 8.4 + */ +public class MarkedAsDirtyConnectorEvent extends ConnectorEvent { + + private final UI ui; + + public MarkedAsDirtyConnectorEvent(ClientConnector source, UI ui) { + super(source); + this.ui = ui; + } + + /** + * Get the UI for which the connector event was fired + * + * @return target ui for event + */ + public UI getUi() { + return ui; + } +} diff --git a/server/src/main/java/com/vaadin/event/MarkedAsDirtyListener.java b/server/src/main/java/com/vaadin/event/MarkedAsDirtyListener.java new file mode 100644 index 0000000000..f973de322a --- /dev/null +++ b/server/src/main/java/com/vaadin/event/MarkedAsDirtyListener.java @@ -0,0 +1,33 @@ +/* + * Copyright 2000-2018 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.event; + +/** + * An interface used for listening to marked as dirty events. + * + * @since 8.4 + */ +@FunctionalInterface +public interface MarkedAsDirtyListener extends ConnectorEventListener { + + /** + * Method called when a client connector has been marked as dirty. + * + * @param event + * marked as dirty connector event object + */ + void connectorMarkedAsDirty(MarkedAsDirtyConnectorEvent event); +} diff --git a/server/src/main/java/com/vaadin/ui/ConnectorTracker.java b/server/src/main/java/com/vaadin/ui/ConnectorTracker.java index 10ba15b6a6..0c331ae8ee 100644 --- a/server/src/main/java/com/vaadin/ui/ConnectorTracker.java +++ b/server/src/main/java/com/vaadin/ui/ConnectorTracker.java @@ -25,12 +25,15 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; +import com.vaadin.event.MarkedAsDirtyConnectorEvent; +import com.vaadin.event.MarkedAsDirtyListener; import com.vaadin.server.AbstractClientConnector; import com.vaadin.server.ClientConnector; import com.vaadin.server.DragAndDropService; @@ -40,6 +43,7 @@ import com.vaadin.server.StreamVariable; import com.vaadin.server.VaadinRequest; import com.vaadin.server.VaadinService; import com.vaadin.server.communication.ConnectorHierarchyWriter; +import com.vaadin.shared.Registration; import elemental.json.Json; import elemental.json.JsonException; @@ -71,6 +75,8 @@ public class ConnectorTracker implements Serializable { private final Set dirtyConnectors = new HashSet<>(); private final Set uninitializedConnectors = new HashSet<>(); + private List markedDirtyListeners = new ArrayList<>(0); + /** * Connectors that have been unregistered and should be cleaned up the next * time {@link #cleanConnectorMap(boolean)} is invoked unless they have been @@ -502,7 +508,8 @@ public class ConnectorTracker implements Serializable { } /** - * Mark the connector as dirty. This should not be done while the response + * Mark the connector as dirty and notifies any marked as dirty listeners. + * This should not be done while the response * is being written. * * @see #getDirtyConnectors() @@ -518,12 +525,16 @@ public class ConnectorTracker implements Serializable { } if (getLogger().isLoggable(Level.FINE)) { - if (!dirtyConnectors.contains(connector)) { + if (!isDirty(connector)) { getLogger().log(Level.FINE, "{0} is now dirty", getConnectorAndParentInfo(connector)); } } + if(!isDirty(connector)) { + notifyMarkedAsDirtyListeners(connector); + } + dirtyConnectors.add(connector); } @@ -899,4 +910,36 @@ public class ConnectorTracker implements Serializable { public int getCurrentSyncId() { return currentSyncId; } + + /** + * Add a marked as dirty listener that will be called when a client + * connector is marked as dirty. + * + * @param listener + * listener to add + * @since 8.4 + * @return registration for removing listener registration + */ + public Registration addMarkedAsDirtyListener( + MarkedAsDirtyListener listener) { + markedDirtyListeners.add(listener); + return () -> markedDirtyListeners.remove(listener); + } + + /** + * Notify all registered MarkedAsDirtyListeners the given client connector + * has been marked as dirty. + * + * @param connector + * client connector marked as dirty + * @since 8.4 + */ + public void notifyMarkedAsDirtyListeners(ClientConnector connector) { + MarkedAsDirtyConnectorEvent event = new MarkedAsDirtyConnectorEvent( + connector, uI); + new ArrayList<>(markedDirtyListeners).forEach(listener -> { + listener.connectorMarkedAsDirty(event); + }); + } + } diff --git a/server/src/test/java/com/vaadin/tests/event/MarkAsDirtyListenerTest.java b/server/src/test/java/com/vaadin/tests/event/MarkAsDirtyListenerTest.java new file mode 100644 index 0000000000..461ab74bcf --- /dev/null +++ b/server/src/test/java/com/vaadin/tests/event/MarkAsDirtyListenerTest.java @@ -0,0 +1,155 @@ +package com.vaadin.tests.event; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +import org.junit.Assert; +import org.junit.Test; + +import com.vaadin.event.MarkedAsDirtyConnectorEvent; +import com.vaadin.server.ClientConnector; +import com.vaadin.tests.util.MockUI; +import com.vaadin.ui.Button; +import com.vaadin.ui.ComponentTest; +import com.vaadin.ui.ConnectorTracker; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.TextField; +import com.vaadin.ui.UI; + +/** + * Test for mark as dirty listener functionality. + */ +public class MarkAsDirtyListenerTest { + + @Test + public void fire_event_when_ui_marked_dirty() { + UI ui = new MockUI(); + + AtomicReference events = new AtomicReference<>(); + ui.getConnectorTracker().addMarkedAsDirtyListener(event -> Assert + .assertTrue("No reference should have been registered", + events.compareAndSet(null, event))); + // UI is marked dirty on creation and when adding a listener + ComponentTest.syncToClient(ui); + + ui.getConnectorTracker().markDirty(ui); + + Assert.assertNotNull("Mark as dirty event should have fired", + events.get()); + Assert.assertEquals("Event contains wrong ui", ui, + events.get().getUi()); + Assert.assertEquals("Found wrong connector in event", ui, + events.get().getConnector()); + } + + @Test + public void fire_event_for_setContent() { + List events = new ArrayList<>(); + UI ui = new MockUI() {{ + getConnectorTracker().addMarkedAsDirtyListener(event -> events.add(event)); + }}; + ComponentTest.syncToClient(ui); + + Button button = new Button("Button"); + ui.setContent(button); + + Assert.assertEquals("Mark as dirty events should have fired", 2, + events.size()); + Assert.assertEquals("Expected button to inform first for creation", + button, events.get(0).getConnector()); + Assert.assertEquals("Expected UI marked as dirty for setContent", ui, + events.get(1).getConnector()); + } + + @Test + public void fire_event_for_component_stateChange() { + UI ui = new MockUI(); + Button button = new Button("empty"); + ui.setContent(button); + ComponentTest.syncToClient(button); + + AtomicReference events = new AtomicReference<>(); + ui.getConnectorTracker().addMarkedAsDirtyListener(event -> Assert + .assertTrue("No reference should have been registered", + events.compareAndSet(null, event))); + + button.setIconAlternateText("alternate"); + + Assert.assertNotNull("Mark as dirty event should have fired", + events.get()); + Assert.assertEquals("Event contains wrong ui", ui, + events.get().getUi()); + Assert.assertEquals("Found wrong connector in event", button, + events.get().getConnector()); + } + + @Test + public void fire_events_for_each_component() { + List events = new ArrayList<>(); + UI ui = new MockUI() {{ + getConnectorTracker().addMarkedAsDirtyListener(event -> events.add(event)); + }}; + + HorizontalLayout layout = new HorizontalLayout(); + // UI initially marked as dirty so should not show as event. + ui.setContent(layout); + TextField field = new TextField("Name"); + Button button = new Button("say hello"); + layout.addComponents(field, button); + + Assert.assertFalse("Mark as dirty event should have fired", + events.isEmpty()); + Assert.assertEquals("Unexpected amount of connector events", 3, + events.size()); + + Set connectors = events.stream() + .map(MarkedAsDirtyConnectorEvent::getConnector) + .collect(Collectors.toSet()); + + Assert.assertTrue( + "HorizontalLayout should have fired an markedAsDirty event", + connectors.contains(layout)); + Assert.assertTrue("TextField should have fired an markedAsDirty event", + connectors.contains(field)); + Assert.assertTrue("Button should have fired an markedAsDirty event", + connectors.contains(button)); + } + + @Test + public void event_should_only_fire_once_for_an_connector_per_roundtrip() { + UI ui = new MockUI(); + Button button = new Button("empty"); + ui.setContent(button); + ComponentTest.syncToClient(button); + + AtomicReference events = new AtomicReference<>(); + ui.getConnectorTracker().addMarkedAsDirtyListener(event -> Assert + .assertTrue("Only one event should have registered", + events.compareAndSet(null, event))); + + button.setIconAlternateText("alternate"); + button.setCaption("Update"); + button.setDisableOnClick(true); + + Assert.assertNotNull("Mark as dirty event should have fired", + events.get()); + Assert.assertEquals("Event contains wrong ui", ui, + events.get().getUi()); + Assert.assertEquals("Found wrong connector in event", button, + events.get().getConnector()); + + events.set(null); + ComponentTest.syncToClient(button); + + button.setCaption("new caption"); + + Assert.assertNotNull("Mark as dirty event should have fired", + events.get()); + Assert.assertEquals("Found wrong connector in event", button, + events.get().getConnector()); + } + +} -- 2.39.5