diff options
author | Artur <artur@vaadin.com> | 2017-06-20 06:20:17 +0300 |
---|---|---|
committer | Ilia Motornyi <elmot@vaadin.com> | 2017-06-20 06:20:17 +0300 |
commit | b5835ba8bda071e5442a93342605692b32cc9602 (patch) | |
tree | 24e1cf64b07ad150216bac3f47140ecf770f17db /server | |
parent | fd78c9b4c656125ef03d1859e981869eb589c447 (diff) | |
download | vaadin-framework-b5835ba8bda071e5442a93342605692b32cc9602.tar.gz vaadin-framework-b5835ba8bda071e5442a93342605692b32cc9602.zip |
Add View.beforeLeave to support delayed navigation
Diffstat (limited to 'server')
5 files changed, 332 insertions, 60 deletions
diff --git a/server/src/main/java/com/vaadin/navigator/Navigator.java b/server/src/main/java/com/vaadin/navigator/Navigator.java index da5dfa0f87..23d6cc5f91 100644 --- a/server/src/main/java/com/vaadin/navigator/Navigator.java +++ b/server/src/main/java/com/vaadin/navigator/Navigator.java @@ -594,6 +594,67 @@ public class Navigator implements Serializable { * parameters passed in the navigation state to the view */ protected void navigateTo(View view, String viewName, String parameters) { + runAfterLeaveConfirmation(() -> { + performNavigateTo(view, viewName, parameters); + }); + + } + + /** + * Triggers {@link View#beforeLeave(ViewBeforeLeaveEvent)} for the current + * view with the given action. + * <p> + * This method is typically called by + * {@link #navigateTo(View, String, String)} but can be called from + * application code when you want to e.g. show a confirmation dialog before + * perfoming an action which is not a navigation but which would cause the + * view to be hidden, e.g. logging out. + * <p> + * Note that this method will not trigger any {@link ViewChangeListener}s as + * it does not navigate to a new view. Use {@link #navigateTo(String)} to + * change views and trigger all listeners. + * + * @param action + * the action to execute when the view confirms it is ok to leave + * @since 8.1 + */ + public void runAfterLeaveConfirmation(ViewLeaveAction action) { + View currentView = getCurrentView(); + if (currentView == null) { + action.run(); + } else { + ViewBeforeLeaveEvent beforeLeaveEvent = new ViewBeforeLeaveEvent( + this, action); + currentView.beforeLeave(beforeLeaveEvent); + if (!beforeLeaveEvent.isNavigateRun()) { + // The event handler prevented navigation + // Revert URL to previous state in case the navigation was + // caused by the back-button + revertNavigation(); + } + } + } + + /** + * Internal method for activating a view, setting its parameters and calling + * listeners. + * <p> + * Invoked after the current view has confirmed that leaving is ok. + * <p> + * This method also verifies that the user is allowed to perform the + * navigation operation. + * + * @param view + * view to activate + * @param viewName + * (optional) name of the view or null not to change the + * navigation state + * @param parameters + * parameters passed in the navigation state to the view + * @since 8.1 + */ + protected void performNavigateTo(View view, String viewName, + String parameters) { ViewChangeEvent event = new ViewChangeEvent(this, currentView, view, viewName, parameters); boolean navigationAllowed = beforeViewChange(event); @@ -1112,4 +1173,5 @@ public class Navigator implements Serializable { stateManager.setNavigator(null); ui.setNavigator(null); } + } diff --git a/server/src/main/java/com/vaadin/navigator/View.java b/server/src/main/java/com/vaadin/navigator/View.java index 623c8ed6e9..6e87c6e455 100644 --- a/server/src/main/java/com/vaadin/navigator/View.java +++ b/server/src/main/java/com/vaadin/navigator/View.java @@ -34,9 +34,8 @@ import com.vaadin.ui.Component; public interface View extends Serializable { /** - * This view is navigated to. - * - * This method is always called before the view is shown on screen. + * Called before the view is shown on screen. + * <p> * {@link ViewChangeEvent#getParameters() event.getParameters()} may contain * extra parameters relevant to the view. * @@ -49,6 +48,34 @@ public interface View extends Serializable { public void enter(ViewChangeEvent event); /** + * Called when the user is requesting navigation away from the view. + * <p> + * This method allows the view to accept or prevent navigation away from the + * view or optionally delay navigation away until a later stage. For + * navigation to take place, the {@link ViewBeforeLeaveEvent#navigate()} + * method must be called either directly when handling this event or later + * to perform delayed navigation. + * <p> + * The default implementation calls {@link ViewBeforeLeaveEvent#navigate()} + * directly. If you override this and do nothing, the user will never be + * able to leave the view. + * <p> + * This method is triggered before any methods in any added + * {@link ViewChangeListener ViewChangeListeners}. Whenever you call + * {@link ViewBeforeLeaveEvent#navigate()}, any {@link ViewChangeListener}s + * will be triggered. They will be handled normally and might also prevent + * navigation. + * + * @param event + * an event object providing information about the event and + * containing the {@link ViewBeforeLeaveEvent#navigate()} method + * needed to perform navigation + */ + public default void beforeLeave(ViewBeforeLeaveEvent event) { + event.navigate(); + } + + /** * Gets the component to show when navigating to the view. * * By default casts this View to a {@link Component} if possible, otherwise diff --git a/server/src/main/java/com/vaadin/navigator/ViewBeforeLeaveEvent.java b/server/src/main/java/com/vaadin/navigator/ViewBeforeLeaveEvent.java new file mode 100644 index 0000000000..2f7fcbe170 --- /dev/null +++ b/server/src/main/java/com/vaadin/navigator/ViewBeforeLeaveEvent.java @@ -0,0 +1,67 @@ +/* + * Copyright 2000-2016 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.navigator; + +import java.util.EventObject; + +/** + * Event sent to the View instance before navigating away from it. + * <p> + * Provides a {@link #navigate()} method which must be called for the navigation + * to take place. + * + * @since 8.1 + */ +public class ViewBeforeLeaveEvent extends EventObject { + + private ViewLeaveAction action; + private boolean navigateRun = false; + + /** + * Creates a new event instance for the given navigator. + * + * @param navigator + * the navigator instance + * @param action + * the command to execute when calling {@link #navigate()} + */ + public ViewBeforeLeaveEvent(Navigator navigator, ViewLeaveAction action) { + super(navigator); + this.action = action; + } + + /** + * Performs the navigation which triggered the event in the first place. + */ + public void navigate() { + if (navigateRun) { + throw new IllegalStateException( + "navigate() can only be called once"); + } + action.run(); + navigateRun = true; + } + + /** + * Checks if the navigate command has been executed. + * + * @return <code>true</code> if {@link #navigate()} has been called, + * <code>false</code> otherwise + */ + protected boolean isNavigateRun() { + return navigateRun; + } +} diff --git a/server/src/main/java/com/vaadin/navigator/ViewLeaveAction.java b/server/src/main/java/com/vaadin/navigator/ViewLeaveAction.java new file mode 100644 index 0000000000..945d13bab6 --- /dev/null +++ b/server/src/main/java/com/vaadin/navigator/ViewLeaveAction.java @@ -0,0 +1,31 @@ +/* + * Copyright 2000-2016 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.navigator; + +import java.io.Serializable; + +/** + * An action to execute when navigating away from a view. + * + * @since 8.1 + */ +@FunctionalInterface +public interface ViewLeaveAction extends Serializable { + /** + * Executes the action. + */ + public void run(); +} diff --git a/server/src/test/java/com/vaadin/tests/server/navigator/NavigatorTest.java b/server/src/test/java/com/vaadin/tests/server/navigator/NavigatorTest.java index fbc55d57cc..31e1c53ec6 100644 --- a/server/src/test/java/com/vaadin/tests/server/navigator/NavigatorTest.java +++ b/server/src/test/java/com/vaadin/tests/server/navigator/NavigatorTest.java @@ -25,6 +25,7 @@ import static org.junit.Assert.fail; import java.util.LinkedList; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.easymock.EasyMock; @@ -36,6 +37,7 @@ import org.junit.Test; import com.vaadin.navigator.NavigationStateManager; import com.vaadin.navigator.Navigator; import com.vaadin.navigator.View; +import com.vaadin.navigator.ViewBeforeLeaveEvent; import com.vaadin.navigator.ViewChangeListener; import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent; import com.vaadin.navigator.ViewDisplay; @@ -44,7 +46,6 @@ import com.vaadin.server.Page; import com.vaadin.server.VaadinRequest; import com.vaadin.shared.Registration; import com.vaadin.shared.ui.ui.PageState; -import com.vaadin.tests.server.navigator.ClassBasedViewProviderTest.TestView; import com.vaadin.tests.server.navigator.ClassBasedViewProviderTest.TestView2; import com.vaadin.ui.Component; import com.vaadin.ui.HorizontalLayout; @@ -336,8 +337,8 @@ public class NavigatorTest { .createMock(NavigationStateManager.class); ViewDisplay display = control.createMock(ViewDisplay.class); ViewProvider provider = control.createMock(ViewProvider.class); - View view1 = control.createMock(View.class); - View view2 = control.createMock(View.class); + TestView view1 = new TestView(); + TestView view2 = new TestView(); // prepare mocks: what to expect manager.setNavigator(EasyMock.anyObject(Navigator.class)); @@ -346,7 +347,6 @@ public class NavigatorTest { .times(2); EasyMock.expect(provider.getView("test1")).andReturn(view1); EasyMock.expect(manager.getState()).andReturn(""); - view1.enter(eventParametersEqual("")); display.showView(view1); manager.setState("test1"); EasyMock.expect(manager.getState()).andReturn("test1"); @@ -355,7 +355,6 @@ public class NavigatorTest { .times(2); EasyMock.expect(provider.getView("test2")).andReturn(view2); EasyMock.expect(manager.getState()).andReturn("test1"); - view2.enter(eventParametersEqual("")); display.showView(view2); manager.setState("test2"); EasyMock.expect(manager.getState()).andReturn("test2"); @@ -364,7 +363,6 @@ public class NavigatorTest { .times(2); EasyMock.expect(provider.getView("test1")).andReturn(view1); EasyMock.expect(manager.getState()).andReturn("test2"); - view1.enter(eventParametersEqual("params")); display.showView(view1); manager.setState("test1/params"); EasyMock.expect(manager.getState()).andReturn("test1/params"); @@ -377,12 +375,27 @@ public class NavigatorTest { navigator.navigateTo("test1"); assertEquals("test1", navigator.getState()); - + assertEquals("", view1.getParams()); navigator.navigateTo("test2/"); assertEquals("test2", navigator.getState()); + assertEquals("", view2.getParams()); navigator.navigateTo("test1/params"); assertEquals("test1/params", navigator.getState()); + assertEquals("params", view1.getParams()); + } + + public static class TestView implements View { + private String params; + + @Override + public void enter(ViewChangeEvent event) { + params = event.getParameters(); + } + + public String getParams() { + return params; + } } @Test @@ -392,8 +405,8 @@ public class NavigatorTest { .createMock(NavigationStateManager.class); ViewDisplay display = control.createMock(ViewDisplay.class); ViewProvider provider = control.createMock(ViewProvider.class); - View view1 = control.createMock(View.class); - View view2 = control.createMock(View.class); + TestView view1 = new TestView(); + TestView view2 = new TestView(); // prepare mocks: what to expect manager.setNavigator(EasyMock.anyObject(Navigator.class)); @@ -402,14 +415,12 @@ public class NavigatorTest { .times(2); EasyMock.expect(provider.getView("test2")).andReturn(view2); EasyMock.expect(manager.getState()).andReturn("view1"); - view2.enter(eventParametersEqual("")); display.showView(view2); manager.setState("test2"); EasyMock.expect(provider.getViewName("")).andReturn("test1").times(2); EasyMock.expect(provider.getView("test1")).andReturn(view1); EasyMock.expect(manager.getState()).andReturn(""); - view1.enter(eventParametersEqual("")); display.showView(view1); manager.setState("test1"); @@ -417,7 +428,6 @@ public class NavigatorTest { .times(2); EasyMock.expect(provider.getView("test1")).andReturn(view1); EasyMock.expect(manager.getState()).andReturn("test2"); - view1.enter(eventParametersEqual("params")); display.showView(view1); manager.setState("test1/params"); @@ -428,8 +438,12 @@ public class NavigatorTest { navigator.addProvider(provider); navigator.navigateTo("test2"); + Assert.assertEquals("", view2.getParams()); + Assert.assertEquals(null, view1.getParams()); navigator.navigateTo(""); + Assert.assertEquals("", view1.getParams()); navigator.navigateTo("test1/params"); + Assert.assertEquals("params", view1.getParams()); } @Test @@ -439,8 +453,8 @@ public class NavigatorTest { .createMock(NavigationStateManager.class); ViewDisplay display = control.createMock(ViewDisplay.class); ViewProvider provider = control.createMock(ViewProvider.class); - View view1 = control.createMock(View.class); - View view2 = control.createMock(View.class); + TestView view1 = new TestView(); + TestView view2 = new TestView(); ViewChangeTestListener listener = new ViewChangeTestListener(); // create navigator to test @@ -454,7 +468,6 @@ public class NavigatorTest { "test1", ""); listener.addExpectedIsViewChangeAllowed(event1, true); EasyMock.expect(manager.getState()).andReturn(""); - view1.enter(eventParametersEqual("")); display.showView(view1); manager.setState("test1"); listener.addExpectedNavigatorViewChange(event1); @@ -466,7 +479,6 @@ public class NavigatorTest { "test2", ""); listener.addExpectedIsViewChangeAllowed(event2, true); EasyMock.expect(manager.getState()).andReturn("test1"); - view2.enter(eventParametersEqual("")); display.showView(view2); manager.setState("test2"); listener.addExpectedNavigatorViewChange(event2); @@ -524,8 +536,8 @@ public class NavigatorTest { .createMock(NavigationStateManager.class); ViewDisplay display = control.createMock(ViewDisplay.class); ViewProvider provider = control.createMock(ViewProvider.class); - View view1 = control.createMock(View.class); - View view2 = control.createMock(View.class); + TestView view1 = new TestView(); + TestView view2 = new TestView(); ViewChangeTestListener listener1 = new ViewChangeTestListener(); ViewChangeTestListener listener2 = new ViewChangeTestListener(); @@ -560,7 +572,6 @@ public class NavigatorTest { "test1", "bar"); listener1.addExpectedIsViewChangeAllowed(event3, true); listener2.addExpectedIsViewChangeAllowed(event3, true); - view1.enter(EasyMock.isA(ViewChangeEvent.class)); display.showView(view1); manager.setState("test1/bar"); listener1.addExpectedNavigatorViewChange(event3); @@ -575,7 +586,6 @@ public class NavigatorTest { "test2", ""); listener1.addExpectedIsViewChangeAllowed(event4, true); listener2.addExpectedIsViewChangeAllowed(event4, true); - view2.enter(EasyMock.isA(ViewChangeEvent.class)); display.showView(view2); manager.setState("test2"); listener1.addExpectedNavigatorViewChange(event4); @@ -803,9 +813,7 @@ public class NavigatorTest { public void testNavigateToUnknownView() { TestNavigator navigator = new TestNavigator(); - View errorView = EasyMock.createMock(View.class); - errorView.enter(EasyMock.anyObject(ViewChangeEvent.class)); - EasyMock.replay(errorView); + TestView errorView = new TestView(); try { navigator.navigateTo("doesnotexist"); @@ -816,16 +824,11 @@ public class NavigatorTest { navigator.setErrorView(errorView); navigator.navigateTo("doesnotexist"); - View testView = EasyMock.createMock(View.class); - testView.enter(EasyMock.anyObject(ViewChangeEvent.class)); - EasyMock.replay(testView); + TestView testView = new TestView(); navigator.addView("doesnotexist", testView); navigator.navigateTo("doesnotexist"); - - View errorView2 = EasyMock.createMock(View.class); - errorView2.enter(EasyMock.anyObject(ViewChangeEvent.class)); - EasyMock.replay(errorView2); + TestView errorView2 = new TestView(); ViewProvider errorProvider = EasyMock.createMock(ViewProvider.class); EasyMock.expect(errorProvider.getView("doesnotexist2")) @@ -1064,34 +1067,11 @@ public class NavigatorTest { @Test public void parameterMapFromViewChangeEvent() { - // create navigator to test Navigator navigator = createNavigatorWithState("foo"); - View view1 = EasyMock.createMock(View.class); - View view2 = EasyMock.createMock(View.class); - ViewProvider provider = new ViewProvider() { - - @Override - public String getViewName(String viewAndParameters) { - if (viewAndParameters.contains("/")) { - return viewAndParameters.substring(0, - viewAndParameters.indexOf('/')); - } else { - return viewAndParameters; - } - } - - @Override - public View getView(String viewName) { - if (viewName.equals("view1")) { - return view1; - } else if (viewName.equals("view2")) { - return view2; - } else { - return null; - } - } - }; - navigator.addProvider(provider); + TestView view1 = new TestView(); + TestView view2 = new TestView(); + navigator.addView("view1", view1); + navigator.addView("view2", view2); AtomicReference<Map<String, String>> mapRef = new AtomicReference<>(); AtomicReference<Map<String, String>> mapRefB = new AtomicReference<>(); @@ -1114,4 +1094,109 @@ public class NavigatorTest { entry("d", "")); assertMap(mapRefB.get(), entry("a&", ""), entry("", "c&d")); } + + @Test + public void view_beforeLeave_preventNavigation() { + Navigator navigator = createNavigatorWithState("foo"); + View view1 = new View() { + + @Override + public void enter(ViewChangeEvent event) { + } + + @Override + public void beforeLeave(ViewBeforeLeaveEvent event) { + // Leaving this empty means the user can never leave + } + + }; + View view2 = EasyMock.createMock(View.class); + navigator.addView("view1", view1); + navigator.addView("view2", view2); + navigator.navigateTo("view1"); + navigator.navigateTo("view2"); + Assert.assertEquals("view1", navigator.getState()); + } + + @Test + public void view_beforeLeave_allowNavigation() { + Navigator navigator = createNavigatorWithState("foo"); + View view1 = new View() { + + @Override + public void enter(ViewChangeEvent event) { + } + + @Override + public void beforeLeave(ViewBeforeLeaveEvent event) { + event.navigate(); + } + + }; + View view2 = EasyMock.createMock(View.class); + navigator.addView("view1", view1); + navigator.addView("view2", view2); + navigator.navigateTo("view1"); + navigator.navigateTo("view2"); + Assert.assertEquals("view2", navigator.getState()); + + } + + @Test + public void view_beforeLeave_delayNavigation() { + Navigator navigator = createNavigatorWithState("foo"); + AtomicReference<ViewBeforeLeaveEvent> eventRef = new AtomicReference<ViewBeforeLeaveEvent>(); + View view1 = new View() { + + @Override + public void enter(ViewChangeEvent event) { + } + + @Override + public void beforeLeave(ViewBeforeLeaveEvent event) { + eventRef.set(event); + } + + }; + View view2 = EasyMock.createMock(View.class); + navigator.addView("view1", view1); + navigator.addView("view2", view2); + navigator.navigateTo("view1"); + navigator.navigateTo("view2"); + Assert.assertEquals("view1", navigator.getState()); + eventRef.get().navigate(); + Assert.assertEquals("view2", navigator.getState()); + + } + + @Test + public void navigator_invokeBeforeLeaveManually() { + Navigator navigator = createNavigatorWithState("foo"); + AtomicReference<ViewBeforeLeaveEvent> eventRef = new AtomicReference<ViewBeforeLeaveEvent>(); + View view1 = new View() { + + @Override + public void enter(ViewChangeEvent event) { + } + + @Override + public void beforeLeave(ViewBeforeLeaveEvent event) { + eventRef.set(event); + } + + }; + TestView view2 = new TestView(); + navigator.addView("view1", view1); + navigator.addView("view2", view2); + navigator.navigateTo("view1"); + + AtomicInteger leaveCount = new AtomicInteger(0); + navigator.runAfterLeaveConfirmation(() -> { + leaveCount.incrementAndGet(); + }); + Assert.assertEquals(0, leaveCount.get()); + eventRef.get().navigate(); + Assert.assertEquals(1, leaveCount.get()); + Assert.assertEquals("view1", navigator.getState()); + } } |