Skip to content

Commit ed04eaa

Browse files
vaadin-botArtur-
andauthored
fix: use targeted item refresh when signal-bound item value changes (#23913) (#23922)
- Add a two-argument `refreshItem(T newItem, T oldItem)` overload to `DataProvider`, `DataView`, and related classes, enabling targeted refresh when an item's identity changes (e.g., signal-bound items replaced with new instances) - `DataRefreshEvent` now carries an optional old item reference via `getOldItem()`, allowing`DataCommunicator` and `KeyMapper` to remap from the old identity to the new one instead of failing to find the item - `HierarchicalDataCommunicator` throws `UnsupportedOperationException` for identity-changing refreshes, deferring TreeGrid `bindItems` support to a future change Fixes #8938 Co-authored-by: Artur Signell <artur@vaadin.com>
1 parent 31a34c9 commit ed04eaa

File tree

15 files changed

+233
-14
lines changed

15 files changed

+233
-14
lines changed

flow-data/src/main/java/com/vaadin/flow/data/provider/AbstractDataProvider.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ public void refreshItem(T item) {
8282
fireEvent(new DataRefreshEvent<>(this, item));
8383
}
8484

85+
@Override
86+
public void refreshItem(T newItem, T oldItem) {
87+
fireEvent(new DataRefreshEvent<>(this, newItem, oldItem));
88+
}
89+
8590
/**
8691
* Registers a new listener with the specified activation method to listen
8792
* events generated by this component. If the activation method does not

flow-data/src/main/java/com/vaadin/flow/data/provider/AbstractDataView.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,13 @@ public void refreshItem(T item) {
177177
dataProviderSupplier.get().refreshItem(item);
178178
}
179179

180+
@Override
181+
public void refreshItem(T newItem, T oldItem) {
182+
Objects.requireNonNull(newItem, NULL_ITEM_ERROR_MESSAGE);
183+
Objects.requireNonNull(oldItem, NULL_ITEM_ERROR_MESSAGE);
184+
dataProviderSupplier.get().refreshItem(newItem, oldItem);
185+
}
186+
180187
@Override
181188
public void refreshAll() {
182189
dataProviderSupplier.get().refreshAll();

flow-data/src/main/java/com/vaadin/flow/data/provider/DataChangeEvent.java

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public class DataChangeEvent<T> extends EventObject {
4545
public static class DataRefreshEvent<T> extends DataChangeEvent<T> {
4646

4747
private final T item;
48+
private final T oldItem;
4849
private boolean refreshChildren;
4950

5051
/**
@@ -57,7 +58,7 @@ public static class DataRefreshEvent<T> extends DataChangeEvent<T> {
5758
* the updated item, not null
5859
*/
5960
public DataRefreshEvent(DataProvider<T, ?> source, T item) {
60-
this(source, item, false);
61+
this(source, item, null, false);
6162
}
6263

6364
/**
@@ -74,9 +75,48 @@ public DataRefreshEvent(DataProvider<T, ?> source, T item) {
7475
*/
7576
public DataRefreshEvent(DataProvider<T, ?> source, T item,
7677
boolean refreshChildren) {
78+
this(source, item, null, refreshChildren);
79+
}
80+
81+
/**
82+
* Creates a new data refresh event originating from the given data
83+
* provider, carrying the previous item instance so that downstream
84+
* consumers can remap their internal state.
85+
*
86+
* @param source
87+
* the data provider, not null
88+
* @param item
89+
* the updated item, not null
90+
* @param oldItem
91+
* the old item before the update, or null if the identity
92+
* has not changed
93+
*/
94+
public DataRefreshEvent(DataProvider<T, ?> source, T item, T oldItem) {
95+
this(source, item, oldItem, false);
96+
}
97+
98+
/**
99+
* Creates a new data refresh event originating from the given data
100+
* provider, carrying the previous item instance so that downstream
101+
* consumers can remap their internal state.
102+
*
103+
* @param source
104+
* the data provider, not null
105+
* @param item
106+
* the updated item, not null
107+
* @param oldItem
108+
* the old item before the update, or null if the identity
109+
* has not changed
110+
* @param refreshChildren
111+
* whether, in hierarchical providers, subelements should be
112+
* refreshed as well
113+
*/
114+
public DataRefreshEvent(DataProvider<T, ?> source, T item, T oldItem,
115+
boolean refreshChildren) {
77116
super(source);
78117
Objects.requireNonNull(item, "Refreshed item can't be null");
79118
this.item = item;
119+
this.oldItem = oldItem;
80120
this.refreshChildren = refreshChildren;
81121
}
82122

@@ -89,6 +129,16 @@ public T getItem() {
89129
return item;
90130
}
91131

132+
/**
133+
* Gets the old item before the update. If no old item was provided,
134+
* returns the current item as a safe default.
135+
*
136+
* @return the old item, or the current item if no old item was provided
137+
*/
138+
public T getOldItem() {
139+
return oldItem != null ? oldItem : item;
140+
}
141+
92142
/**
93143
* Gets the a boolean whether the refresh is supposed to be
94144
* refreshChildren (in hierarchical data providers).

flow-data/src/main/java/com/vaadin/flow/data/provider/DataCommunicator.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,9 +427,22 @@ public void reset() {
427427
* updated data object; not {@code null}
428428
*/
429429
public void refresh(T data) {
430+
refresh(data, data);
431+
}
432+
433+
/**
434+
* Informs the DataCommunicator that a data object has been updated,
435+
* providing the old data object for identity remapping in the KeyMapper.
436+
*
437+
* @param data
438+
* the new data object; not {@code null}
439+
* @param oldData
440+
* the old data object used to find the existing key mapping
441+
*/
442+
protected void refresh(T data, T oldData) {
430443
Objects.requireNonNull(data,
431444
"DataCommunicator can not refresh null object");
432-
getKeyMapper().refresh(data);
445+
getKeyMapper().refresh(data, oldData);
433446
dataGenerator.refreshData(data);
434447
updatedData.add(data);
435448
requestFlushUpdatedData();
@@ -1141,7 +1154,7 @@ private void handleAttach() {
11411154
}
11421155

11431156
protected void handleDataRefreshEvent(DataRefreshEvent<T> event) {
1144-
refresh(event.getItem());
1157+
refresh(event.getItem(), event.getOldItem());
11451158
}
11461159

11471160
private void handleDetach() {

flow-data/src/main/java/com/vaadin/flow/data/provider/DataKeyMapper.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,23 @@ public interface DataKeyMapper<T> extends Serializable {
8282
*/
8383
void refresh(T dataObject);
8484

85+
/**
86+
* Updates any existing mappings of given data object, using the old data
87+
* object to find the existing mapping. This is useful when the identity of
88+
* the data object has changed (e.g., the object was replaced with a new
89+
* instance).
90+
*
91+
* @param dataObject
92+
* the new data object to update to
93+
* @param oldDataObject
94+
* the old data object to find the existing mapping
95+
*/
96+
default void refresh(T dataObject, T oldDataObject) {
97+
throw new UnsupportedOperationException(
98+
"This DataKeyMapper does not support identity remapping. "
99+
+ "Override refresh(T, T) to handle item replacement.");
100+
}
101+
85102
/**
86103
* Takes identifier getter into use and updates existing mappings
87104
*

flow-data/src/main/java/com/vaadin/flow/data/provider/DataProvider.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,24 @@ default void refreshItem(T item, boolean refreshChildren) {
131131
refreshItem(item);
132132
}
133133

134+
/**
135+
* Refreshes an item that has changed identity, passing the old item so that
136+
* downstream consumers (e.g. key mappers) can remap from the old identity
137+
* to the new one.
138+
* <p>
139+
* The default implementation simply delegates to
140+
* {@link #refreshItem(Object)}, discarding the old item. Implementations
141+
* that need to remap internal state should override this method.
142+
*
143+
* @param newItem
144+
* the new item after the update, not null
145+
* @param oldItem
146+
* the old item before the update, not null
147+
*/
148+
default void refreshItem(T newItem, T oldItem) {
149+
refreshItem(newItem);
150+
}
151+
134152
/**
135153
* Refreshes all data based on currently available data in the underlying
136154
* provider.

flow-data/src/main/java/com/vaadin/flow/data/provider/DataProviderWrapper.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ public void refreshItem(T item) {
7878
dataProvider.refreshItem(item);
7979
}
8080

81+
@Override
82+
public void refreshItem(T newItem, T oldItem) {
83+
dataProvider.refreshItem(newItem, oldItem);
84+
}
85+
8186
@Override
8287
public Object getId(T item) {
8388
return dataProvider.getId(item);

flow-data/src/main/java/com/vaadin/flow/data/provider/DataView.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,21 @@ public interface DataView<T> extends Serializable {
119119
*/
120120
void refreshItem(T item);
121121

122+
/**
123+
* Refreshes an item that has changed identity, carrying the old item
124+
* through the data pipeline so that internal mappings (e.g. key mappers)
125+
* can be remapped from the old identity to the new one.
126+
* <p>
127+
* This method delegates the update to
128+
* {@link DataProvider#refreshItem(Object, Object)}.
129+
*
130+
* @param newItem
131+
* the new item after the update, not null
132+
* @param oldItem
133+
* the old item before the update, not null
134+
*/
135+
void refreshItem(T newItem, T oldItem);
136+
122137
/**
123138
* Notifies the component that all the items should be refreshed.
124139
*/

flow-data/src/main/java/com/vaadin/flow/data/provider/DataViewUtils.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
import com.vaadin.flow.component.Component;
2424
import com.vaadin.flow.component.ComponentUtil;
2525
import com.vaadin.flow.data.binder.HasDataProvider;
26+
import com.vaadin.flow.function.SerializableBiConsumer;
2627
import com.vaadin.flow.function.SerializableComparator;
27-
import com.vaadin.flow.function.SerializableConsumer;
2828
import com.vaadin.flow.function.SerializableFunction;
2929
import com.vaadin.flow.function.SerializablePredicate;
3030
import com.vaadin.flow.internal.nodefeature.SignalBindingFeature;
@@ -340,14 +340,14 @@ private static <T, V extends DataView<T>> V bindItemsInternal(
340340
* @param refreshAll
341341
* callback to refresh all items
342342
* @param refreshItem
343-
* callback to refresh a single item
343+
* callback to replace a single item, accepting old and new items
344344
* @param <T>
345345
* item type
346346
*/
347347
private static <T> void setupItemsEffect(Component component,
348348
Signal<? extends List<? extends Signal<T>>> itemsSignal,
349349
List<T> backingList, Runnable refreshAll,
350-
SerializableConsumer<T> refreshItem) {
350+
SerializableBiConsumer<T, T> refreshItem) {
351351
// List to store inner effect registrations
352352
List<Registration> innerEffectRegistrations = new ArrayList<>();
353353

@@ -390,23 +390,24 @@ private static <T> void setupItemsEffect(Component component,
390390
* @param backingList
391391
* the backing list to update
392392
* @param refreshItem
393-
* callback to refresh the item
393+
* callback to replace the item, accepting old and new items
394394
* @param <T>
395395
* item type
396396
* @return the registration for the effect
397397
*/
398398
private static <T> Registration createItemEffect(Component component,
399399
Signal<T> itemSignal, int index, List<T> backingList,
400-
SerializableConsumer<T> refreshItem) {
400+
SerializableBiConsumer<T, T> refreshItem) {
401401
return Signal.effect(component, context -> {
402402
// register a dependency on the initial run
403403
T newValue = itemSignal.get();
404404

405405
// Skip refreshItem on the first run since refreshAll was just
406406
// called
407407
if (!context.isInitialRun()) {
408+
T oldValue = backingList.get(index);
408409
backingList.set(index, newValue);
409-
refreshItem.accept(newValue);
410+
refreshItem.accept(newValue, oldValue);
410411
}
411412
});
412413
}

flow-data/src/main/java/com/vaadin/flow/data/provider/KeyMapper.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,17 @@ public void refresh(V dataObject) {
163163
}
164164
}
165165

166+
@Override
167+
public void refresh(V dataObject, V oldDataObject) {
168+
Object oldId = identifierGetter.apply(oldDataObject);
169+
String key = objectIdKeyMap.remove(oldId);
170+
if (key != null) {
171+
Object newId = identifierGetter.apply(dataObject);
172+
objectIdKeyMap.put(newId, key);
173+
keyObjectMap.put(key, dataObject);
174+
}
175+
}
176+
166177
/**
167178
* Gets all mapped objects.
168179
*/

0 commit comments

Comments
 (0)