diff --git a/flow-server/src/main/java/com/vaadin/flow/component/ComponentEvent.java b/flow-server/src/main/java/com/vaadin/flow/component/ComponentEvent.java index f6c57edd1fb..47c0141bf4e 100644 --- a/flow-server/src/main/java/com/vaadin/flow/component/ComponentEvent.java +++ b/flow-server/src/main/java/com/vaadin/flow/component/ComponentEvent.java @@ -34,6 +34,7 @@ public class ComponentEvent extends EventObject { private boolean fromClient = false; private Command unregisterListenerCommand = null; + private UI ui; /** * Creates a new event using the given source and indicator whether the @@ -50,6 +51,32 @@ public ComponentEvent(T source, boolean fromClient) { this.fromClient = fromClient; } + /** + * Creates a new event using the given source, indicator whether the event + * originated from the client side or the server side, and an explicit UI to + * associate with the event. + *

+ * Use this constructor when the source component may not be attached to a + * UI at the time the event is fired, but the UI is still known (for + * example, when the event is dispatched from code that has access to the + * current UI). The supplied UI is then returned by {@link #getUI()} without + * relying on the source's attachment state. + * + * @param source + * the source component + * @param fromClient + * true if the event originated from the client + * side, false otherwise + * @param ui + * the UI associated with the event, or null if not + * available + */ + public ComponentEvent(T source, boolean fromClient, UI ui) { + super(source); + this.fromClient = fromClient; + this.ui = ui; + } + @SuppressWarnings("unchecked") @Override public T getSource() { @@ -57,29 +84,45 @@ public T getSource() { } /** - * Gets the UI the source component is attached to. + * Gets the UI associated with this event. *

- * This is a convenience for {@code getSource().getUI().get()} when the - * event is fired while the source is attached to a UI, which is the common - * case. + * The UI is resolved in the following order: + *

    + *
  1. If a {@link UI} was explicitly provided at + * {@linkplain #ComponentEvent(Component, boolean, UI) construction time}, + * that instance is returned.
  2. + *
  3. If the source component is itself a {@link UI}, it is returned + * directly.
  4. + *
  5. Otherwise, the UI is obtained from + * {@code getSource().getUI().get()}.
  6. + *
*

- * If the source component is not currently attached to a UI, this method - * throws an {@link IllegalStateException}. This can happen, for example, - * when an initial value is set on a field before it is added to the UI and - * a value-change listener is invoked. If your listener can run while the - * source is detached, use {@code getSource().getUI()} instead, which - * returns an {@link java.util.Optional} and lets you handle the detached - * case explicitly. + * In the common case the source is attached to a UI when the event fires, + * so this method is a convenient shorthand for + * {@code getSource().getUI().get()}. + *

+ * If none of the above applies and the source component is not currently + * attached to a UI, this method throws an {@link IllegalStateException}. + * This can happen, for example, when an initial value is set on a field + * before it is added to the UI and a value-change listener is invoked. If + * your listener can run while the source is detached, use + * {@code getSource().getUI()} instead, which returns an + * {@link java.util.Optional} and lets you handle the detached case + * explicitly. * - * @return the UI the source component is attached to, never {@code null} + * @return the UI associated with this event, never {@code null} * @throws IllegalStateException - * if the source component is not currently attached to a UI + * if the source component is not currently attached to a UI and + * no UI was provided at construction time */ public UI getUI() { - T source = getSource(); - if (source instanceof UI ui) { + if (ui != null) { return ui; } + T source = getSource(); + if (source instanceof UI sourceUI) { + return sourceUI; + } return source.getUI() .orElseThrow(() -> new IllegalStateException( "Cannot resolve UI for event source " + source diff --git a/flow-server/src/test/java/com/vaadin/flow/component/ComponentEventTest.java b/flow-server/src/test/java/com/vaadin/flow/component/ComponentEventTest.java index f2fa41d9e15..5bc43d55b71 100644 --- a/flow-server/src/test/java/com/vaadin/flow/component/ComponentEventTest.java +++ b/flow-server/src/test/java/com/vaadin/flow/component/ComponentEventTest.java @@ -61,4 +61,14 @@ void getUI_sourceDetached_throwsIllegalStateException() { assertEquals(true, exception.getMessage().contains("not") && exception.getMessage().contains("attached")); } + + @Test + void getUI_explicitlyProvided_returnsProvidedUI() { + MockUI ui = new MockUI(); + TestComponent source = new TestComponent(); + ComponentEvent event = new ComponentEvent<>(source, + false, ui); + + assertSame(ui, event.getUI()); + } }