SELECTION_REFACTORING_ANALYSIS.md
... ...
@@ -1,400 +0,0 @@
1
-# LeaderboardPanel Selection State Refactoring Analysis
2
-
3
-## Current Architecture
4
-
5
-### Three Selection Models in Play
6
-
7
-1. **`leaderboardSelectionModel`** (LeaderboardPanel:239)
8
- - Type: `MultiSelectionModel<LeaderboardRowDTO>`
9
- - Created at line 527
10
- - Attached to the CellTable at line 547
11
- - Has a `selectionChangeHandler` that syncs TO CompetitorSelectionProvider (lines 530-544)
12
-
13
-2. **`SelectionCheckboxColumn.selectionModel`** (SelectionCheckboxColumn:51)
14
- - Type: `RefreshableMultiSelectionModel<T>`
15
- - Created by SelectionCheckboxColumn constructor
16
- - **Currently NOT used by the LeaderboardPanel's CellTable**
17
- - Has display.flush() callbacks for UI updates
18
-
19
-3. **`CompetitorSelectionProvider`**
20
- - Application-level selection state
21
- - Both LeaderboardPanel and SelectionCheckboxColumn listen to it
22
- - Notifies listeners via `addedToSelection()` / `removedFromSelection()`
23
-
24
-### Current Selection Flow Problems
25
-
26
-#### Flow 1: User clicks checkbox in UI
27
-```
28
-User clicks checkbox
29
- ↓
30
-SelectionCheckboxColumn handles click event
31
- ↓
32
-leaderboardSelectionModel.setSelected() called ← Uses WRONG selection model!
33
- ↓
34
-selectionChangeHandler fires (lines 530-544)
35
- ↓
36
-competitorSelectionProvider.setSelection() called
37
- ↓
38
-CompetitorSelectionProvider fires addedToSelection/removedFromSelection
39
- ↓
40
-BOTH LeaderboardPanel AND SelectionCheckboxColumn receive callback:
41
- - LeaderboardPanel.addedToSelection() (line 3393)
42
- → calls leaderboardSelectionModel.setSelected()
43
- - SelectionCheckboxColumn.addedToSelection() (line 2160)
44
- → calls getSelectionModel().setSelected() ← Different model!
45
-```
46
-
47
-**Problem**: Two selection models exist but only one (leaderboardSelectionModel) is attached to the table. The SelectionCheckboxColumn's RefreshableMultiSelectionModel is created but orphaned.
48
-
49
-#### Flow 2: CompetitorSelectionProvider changes externally
50
-```
51
-External change to CompetitorSelectionProvider
52
- ↓
53
-Fires addedToSelection/removedFromSelection
54
- ↓
55
-BOTH listeners receive callback:
56
- - LeaderboardPanel.addedToSelection() (line 3393)
57
- → leaderboardSelectionModel.setSelected(row, true)
58
- → Could trigger selectionChangeHandler → infinite loop potential!
59
- - SelectionCheckboxColumn.addedToSelection() (line 2160)
60
- → getSelectionModel().setSelected(row, true) ← Orphaned model!
61
-```
62
-
63
-**Problem**: LeaderboardPanel guards against recursion by temporarily removing the handler (line 2712), but this is complex. SelectionCheckboxColumn updates an unused selection model.
64
-
65
-### Inconsistencies Found
66
-
67
-1. **Line 527**: LeaderboardPanel creates its own `MultiSelectionModel`
68
-2. **Line 547**: Attaches that model to the table, ignoring SelectionCheckboxColumn's model
69
-3. **Line 2163/2174**: SelectionCheckboxColumn callbacks update its own orphaned model
70
-4. **Line 3396/3404**: LeaderboardPanel callbacks update the table's model
71
-5. **SelectionCheckboxColumn.getValue()** (line 2140): Checks CompetitorSelectionProvider directly, NOT its own selection model!
72
-
73
-## Proposed Refactoring
74
-
75
-### Goal
76
-Use **ONLY** the `RefreshableMultiSelectionModel` from SelectionCheckboxColumn as the single source of truth, synchronized bidirectionally with CompetitorSelectionProvider.
77
-
78
-### Step-by-Step Plan
79
-
80
-#### Step 1: Use SelectionCheckboxColumn's Model as Table's Model
81
-
82
-**File**: `LeaderboardPanel.java`
83
-
84
-**Current code** (lines 525-547):
85
-```java
86
-selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider);
87
-leaderboardTable.setWidth("100%");
88
-leaderboardSelectionModel = new MultiSelectionModel<LeaderboardRowDTO>(); // ← REMOVE THIS
89
-selectionChangeHandler = new Handler() {
90
- @Override
91
- public void onSelectionChange(SelectionChangeEvent event) {
92
- List<CompetitorDTO> selection = new ArrayList<>();
93
- for (LeaderboardRowDTO row : getSelectedRows()) {
94
- selection.add(row.competitor);
95
- }
96
- LeaderboardPanel.this.competitorSelectionProvider.setSelection(selection,
97
- /* listenersNotToNotify */LeaderboardPanel.this);
98
- if (blurInOnSelectionChanged > 0) {
99
- blurInOnSelectionChanged--;
100
- blurFocusedElementAfterSelectionChange();
101
- }
102
- }
103
-};
104
-leaderboardAsTableSelectionModelRegistration = leaderboardSelectionModel
105
- .addSelectionChangeHandler(selectionChangeHandler);
106
-leaderboardTable.setSelectionModel(leaderboardSelectionModel, selectionCheckboxColumn.getSelectionManager());
107
-```
108
-
109
-**Refactored**:
110
-```java
111
-selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider);
112
-leaderboardTable.setWidth("100%");
113
-
114
-// Use the SelectionCheckboxColumn's model as THE selection model
115
-leaderboardSelectionModel = selectionCheckboxColumn.getSelectionModel();
116
-
117
-selectionChangeHandler = new Handler() {
118
- @Override
119
- public void onSelectionChange(SelectionChangeEvent event) {
120
- if (updatingSelectionFromProvider) {
121
- // Guard: don't sync back to provider if we're currently syncing FROM provider
122
- return;
123
- }
124
- List<CompetitorDTO> selection = new ArrayList<>();
125
- for (LeaderboardRowDTO row : getSelectedRows()) {
126
- selection.add(row.competitor);
127
- }
128
- LeaderboardPanel.this.competitorSelectionProvider.setSelection(selection,
129
- /* listenersNotToNotify */LeaderboardPanel.this);
130
- if (blurInOnSelectionChanged > 0) {
131
- blurInOnSelectionChanged--;
132
- blurFocusedElementAfterSelectionChange();
133
- }
134
- }
135
-};
136
-leaderboardAsTableSelectionModelRegistration = leaderboardSelectionModel
137
- .addSelectionChangeHandler(selectionChangeHandler);
138
-leaderboardTable.setSelectionModel(leaderboardSelectionModel, selectionCheckboxColumn.getSelectionManager());
139
-```
140
-
141
-**Changes**:
142
-- Line 527: Change from `new MultiSelectionModel<>()` to `selectionCheckboxColumn.getSelectionModel()`
143
-- Add guard flag `updatingSelectionFromProvider` to prevent recursion
144
-- Declare field: `private boolean updatingSelectionFromProvider = false;`
145
-
146
-#### Step 2: Update Field Declaration
147
-
148
-**Current** (line 239):
149
-```java
150
-private final MultiSelectionModel<LeaderboardRowDTO> leaderboardSelectionModel;
151
-```
152
-
153
-**Refactored**:
154
-```java
155
-private final RefreshableMultiSelectionModel<LeaderboardRowDTO> leaderboardSelectionModel;
156
-```
157
-
158
-Add import:
159
-```java
160
-import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel;
161
-```
162
-
163
-#### Step 3: Fix LeaderboardPanel's Selection Callbacks
164
-
165
-**Current** (lines 3393-3406):
166
-```java
167
-@Override
168
-public void addedToSelection(CompetitorDTO competitor) {
169
- LeaderboardRowDTO row = getRow(competitor.getIdAsString());
170
- if (row != null) {
171
- leaderboardSelectionModel.setSelected(row, true);
172
- }
173
-}
174
-
175
-@Override
176
-public void removedFromSelection(CompetitorDTO competitor) {
177
- LeaderboardRowDTO row = getRow(competitor.getIdAsString());
178
- if (row != null) {
179
- leaderboardSelectionModel.setSelected(row, false);
180
- }
181
-}
182
-```
183
-
184
-**Refactored**:
185
-```java
186
-@Override
187
-public void addedToSelection(CompetitorDTO competitor) {
188
- LeaderboardRowDTO row = getRow(competitor.getIdAsString());
189
- if (row != null) {
190
- updatingSelectionFromProvider = true;
191
- try {
192
- leaderboardSelectionModel.setSelected(row, true);
193
- } finally {
194
- updatingSelectionFromProvider = false;
195
- }
196
- }
197
-}
198
-
199
-@Override
200
-public void removedFromSelection(CompetitorDTO competitor) {
201
- LeaderboardRowDTO row = getRow(competitor.getIdAsString());
202
- if (row != null) {
203
- updatingSelectionFromProvider = true;
204
- try {
205
- leaderboardSelectionModel.setSelected(row, false);
206
- } finally {
207
- updatingSelectionFromProvider = false;
208
- }
209
- }
210
-}
211
-```
212
-
213
-**Changes**:
214
-- Set guard flag before updating selection model
215
-- Ensures selectionChangeHandler won't sync back to CompetitorSelectionProvider
216
-
217
-#### Step 4: Remove SelectionCheckboxColumn's Redundant Listener
218
-
219
-**File**: `LeaderboardPanel.java`, inner class `LeaderboardSelectionCheckboxColumn`
220
-
221
-**Current** (lines 2120-2136):
222
-```java
223
-protected LeaderboardSelectionCheckboxColumn(final CompetitorSelectionProvider competitorSelectionProvider) {
224
- super(style.getTableresources().cellTableStyle().cellTableCheckboxSelected(),
225
- style.getTableresources().cellTableStyle().cellTableCheckboxDeselected(),
226
- style.getTableresources().cellTableStyle().cellTableCheckboxColumnCell(),
227
- new EntityIdentityComparator<LeaderboardRowDTO>() {
228
- @Override
229
- public boolean representSameEntity(LeaderboardRowDTO dto1, LeaderboardRowDTO dto2) {
230
- return dto1.competitor.getIdAsString().equals(dto2.competitor.getIdAsString());
231
- }
232
-
233
- @Override
234
- public int hashCode(LeaderboardRowDTO t) {
235
- return t.competitor.getIdAsString().hashCode();
236
- }
237
- }, getData(), leaderboardTable);
238
- competitorSelectionProvider.addCompetitorSelectionChangeListener(this); // ← REMOVE THIS
239
-}
240
-```
241
-
242
-**Refactored**:
243
-```java
244
-protected LeaderboardSelectionCheckboxColumn(final CompetitorSelectionProvider competitorSelectionProvider) {
245
- super(style.getTableresources().cellTableStyle().cellTableCheckboxSelected(),
246
- style.getTableresources().cellTableStyle().cellTableCheckboxDeselected(),
247
- style.getTableresources().cellTableStyle().cellTableCheckboxColumnCell(),
248
- new EntityIdentityComparator<LeaderboardRowDTO>() {
249
- @Override
250
- public boolean representSameEntity(LeaderboardRowDTO dto1, LeaderboardRowDTO dto2) {
251
- return dto1.competitor.getIdAsString().equals(dto2.competitor.getIdAsString());
252
- }
253
-
254
- @Override
255
- public int hashCode(LeaderboardRowDTO t) {
256
- return t.competitor.getIdAsString().hashCode();
257
- }
258
- }, getData(), leaderboardTable);
259
- // REMOVED: competitorSelectionProvider.addCompetitorSelectionChangeListener(this);
260
- // LeaderboardPanel is now the ONLY listener that syncs selection state
261
-}
262
-```
263
-
264
-**Current** (lines 2160-2176, now unnecessary):
265
-```java
266
-@Override
267
-public void addedToSelection(CompetitorDTO competitor) {
268
- final LeaderboardRowDTO row = getRow(competitor.getIdAsString());
269
- if (row != null) {
270
- getSelectionModel().setSelected(row, true);
271
- }
272
-}
273
-
274
-@Override
275
-public void removedFromSelection(CompetitorDTO competitor) {
276
- final LeaderboardRowDTO row = getRow(competitor.getIdAsString());
277
- if (row != null) {
278
- getSelectionModel().setSelected(row, false);
279
- }
280
-}
281
-```
282
-
283
-**Refactored**: Remove these methods entirely. The SelectionCheckboxColumn no longer needs to implement `CompetitorSelectionChangeListener`.
284
-
285
-**Also remove** (line 2119):
286
-```java
287
-// FROM:
288
-private class LeaderboardSelectionCheckboxColumn
289
- extends com.sap.sse.gwt.client.celltable.SelectionCheckboxColumn<LeaderboardRowDTO>
290
- implements CompetitorSelectionChangeListener {
291
-
292
-// TO:
293
-private class LeaderboardSelectionCheckboxColumn
294
- extends com.sap.sse.gwt.client.celltable.SelectionCheckboxColumn<LeaderboardRowDTO> {
295
-```
296
-
297
-Remove the empty listener method implementations at lines 2144-2154.
298
-
299
-#### Step 5: Simplify updateSelection()
300
-
301
-**Current** (lines 2707-2721):
302
-```java
303
-private void updateSelection(LeaderboardRowDTO row) {
304
- final boolean shallBeSelected = competitorSelectionProvider.isSelected(row.competitor);
305
- if (leaderboardAsTableSelectionModelRegistration != null) {
306
- // suspend selection events while actively adjusting the leaderboardSelectionModel to match the
307
- // competitorSelectionProvider
308
- leaderboardAsTableSelectionModelRegistration.removeHandler();
309
- leaderboardAsTableSelectionModelRegistration = null;
310
- }
311
- if (leaderboardSelectionModel.isSelected(row) != shallBeSelected) {
312
- leaderboardSelectionModel.setSelected(row, shallBeSelected);
313
- }
314
- // register the selection change handler again
315
- leaderboardAsTableSelectionModelRegistration = leaderboardTable.getSelectionModel()
316
- .addSelectionChangeHandler(selectionChangeHandler);
317
-}
318
-```
319
-
320
-**Refactored**:
321
-```java
322
-private void updateSelection(LeaderboardRowDTO row) {
323
- final boolean shallBeSelected = competitorSelectionProvider.isSelected(row.competitor);
324
- updatingSelectionFromProvider = true;
325
- try {
326
- if (leaderboardSelectionModel.isSelected(row) != shallBeSelected) {
327
- leaderboardSelectionModel.setSelected(row, shallBeSelected);
328
- }
329
- } finally {
330
- updatingSelectionFromProvider = false;
331
- }
332
-}
333
-```
334
-
335
-**Changes**:
336
-- Removed handler removal/re-registration complexity
337
-- Use guard flag instead
338
-- Much simpler and clearer
339
-
340
-#### Step 6: Fix getValue() in LeaderboardSelectionCheckboxColumn
341
-
342
-**Current** (lines 2139-2141):
343
-```java
344
-@Override
345
-public Boolean getValue(LeaderboardRowDTO row) {
346
- return competitorSelectionProvider.isSelected(row.competitor);
347
-}
348
-```
349
-
350
-**Refactored**:
351
-```java
352
-@Override
353
-public Boolean getValue(LeaderboardRowDTO row) {
354
- // Use the selection model as the source of truth, not CompetitorSelectionProvider
355
- return getSelectionModel().isSelected(row);
356
-}
357
-```
358
-
359
-**Rationale**: The selection model should be the single source of truth for rendering. It stays synchronized with CompetitorSelectionProvider via the LeaderboardPanel's listeners.
360
-
361
-### Summary of Changes
362
-
363
-1. **LeaderboardPanel.java line 527**: Change to use `selectionCheckboxColumn.getSelectionModel()`
364
-2. **LeaderboardPanel.java line 239**: Change type to `RefreshableMultiSelectionModel`
365
-3. **LeaderboardPanel.java**: Add field `private boolean updatingSelectionFromProvider = false;`
366
-4. **LeaderboardPanel.java lines 530-544**: Add guard check in selectionChangeHandler
367
-5. **LeaderboardPanel.java lines 3393-3406**: Add guard flag around setSelected calls
368
-6. **LeaderboardPanel.java lines 2707-2721**: Simplify updateSelection() using guard flag
369
-7. **LeaderboardPanel.LeaderboardSelectionCheckboxColumn line 2135**: Remove listener registration
370
-8. **LeaderboardPanel.LeaderboardSelectionCheckboxColumn line 2119**: Remove `implements CompetitorSelectionChangeListener`
371
-9. **LeaderboardPanel.LeaderboardSelectionCheckboxColumn lines 2144-2176**: Remove listener methods
372
-10. **LeaderboardPanel.LeaderboardSelectionCheckboxColumn line 2140**: Use selection model instead of provider
373
-
374
-## Benefits
375
-
376
-1. **Single Selection Model**: RefreshableMultiSelectionModel is the only selection model
377
-2. **No Orphaned State**: Eliminates the unused selection model in SelectionCheckboxColumn
378
-3. **Simpler Synchronization**: Single bidirectional sync point between model and provider
379
-4. **No Handler Removal**: Guard flag is simpler than removing/re-adding handlers
380
-5. **Clearer Ownership**: LeaderboardPanel owns synchronization, SelectionCheckboxColumn just renders
381
-
382
-## Testing Strategy
383
-
384
-1. Test user clicking checkboxes - selection should sync to CompetitorSelectionProvider
385
-2. Test external CompetitorSelectionProvider changes - should update checkboxes
386
-3. Test rapid clicking - no infinite loops or race conditions
387
-4. Test with filtering - filtered rows should maintain correct selection state
388
-5. Test leaderboard updates - selection should persist across data refreshes
389
-
390
-## Potential Risks
391
-
392
-1. **GWT Selection Model Flush Timing**: GWT selection models flush changes at end of event loop. The guard flag approach should handle this correctly since the flag is set synchronously.
393
-
394
-2. **Multiple LeaderboardPanels**: If multiple LeaderboardPanels share the same CompetitorSelectionProvider, each will receive callbacks. This should work fine as each has its own guard flag.
395
-
396
-3. **RefreshableMultiSelectionModel.refreshSelectionModel()**: This method has its own `dontCheckSelectionState` guard (RefreshableMultiSelectionModel.java line 131). Verify this doesn't interfere with our guard flag.
397
-
398
-## Future Improvements
399
-
400
-Consider enhancing RefreshableMultiSelectionModel to accept a callback for selection changes, eliminating the need for the external SelectionChangeHandler and making the synchronization more encapsulated within the model itself.