Refactored selection state management to use only the SelectionCheckboxColumn's RefreshableMultiSelectionModel, eliminating the orphaned MultiSelectionModel that was previously created by LeaderboardPanel. Changes: - Use SelectionCheckboxColumn.getSelectionModel() as the single source of truth - Added updatingSelectionFromProvider guard flag to prevent infinite recursion - Simplified synchronization logic by removing handler registration/removal complexity - SelectionCheckboxColumn no longer implements CompetitorSelectionChangeListener - SelectionCheckboxColumn.getValue() now uses selection model instead of provider - All selection state synchronization now flows through LeaderboardPanel only This eliminates the convoluted dual selection model architecture and ensures proper bidirectional synchronization with CompetitorSelectionProvider without endless recursions.
16 KiB
LeaderboardPanel Selection State Refactoring Analysis
Current Architecture
Three Selection Models in Play
-
leaderboardSelectionModel(LeaderboardPanel:239)- Type:
MultiSelectionModel<LeaderboardRowDTO> - Created at line 527
- Attached to the CellTable at line 547
- Has a
selectionChangeHandlerthat syncs TO CompetitorSelectionProvider (lines 530-544)
- Type:
-
SelectionCheckboxColumn.selectionModel(SelectionCheckboxColumn:51)- Type:
RefreshableMultiSelectionModel<T> - Created by SelectionCheckboxColumn constructor
- Currently NOT used by the LeaderboardPanel's CellTable
- Has display.flush() callbacks for UI updates
- Type:
-
CompetitorSelectionProvider- Application-level selection state
- Both LeaderboardPanel and SelectionCheckboxColumn listen to it
- Notifies listeners via
addedToSelection()/removedFromSelection()
Current Selection Flow Problems
Flow 1: User clicks checkbox in UI
User clicks checkbox
↓
SelectionCheckboxColumn handles click event
↓
leaderboardSelectionModel.setSelected() called ← Uses WRONG selection model!
↓
selectionChangeHandler fires (lines 530-544)
↓
competitorSelectionProvider.setSelection() called
↓
CompetitorSelectionProvider fires addedToSelection/removedFromSelection
↓
BOTH LeaderboardPanel AND SelectionCheckboxColumn receive callback:
- LeaderboardPanel.addedToSelection() (line 3393)
→ calls leaderboardSelectionModel.setSelected()
- SelectionCheckboxColumn.addedToSelection() (line 2160)
→ calls getSelectionModel().setSelected() ← Different model!
Problem: Two selection models exist but only one (leaderboardSelectionModel) is attached to the table. The SelectionCheckboxColumn's RefreshableMultiSelectionModel is created but orphaned.
Flow 2: CompetitorSelectionProvider changes externally
External change to CompetitorSelectionProvider
↓
Fires addedToSelection/removedFromSelection
↓
BOTH listeners receive callback:
- LeaderboardPanel.addedToSelection() (line 3393)
→ leaderboardSelectionModel.setSelected(row, true)
→ Could trigger selectionChangeHandler → infinite loop potential!
- SelectionCheckboxColumn.addedToSelection() (line 2160)
→ getSelectionModel().setSelected(row, true) ← Orphaned model!
Problem: LeaderboardPanel guards against recursion by temporarily removing the handler (line 2712), but this is complex. SelectionCheckboxColumn updates an unused selection model.
Inconsistencies Found
- Line 527: LeaderboardPanel creates its own
MultiSelectionModel - Line 547: Attaches that model to the table, ignoring SelectionCheckboxColumn's model
- Line 2163/2174: SelectionCheckboxColumn callbacks update its own orphaned model
- Line 3396/3404: LeaderboardPanel callbacks update the table's model
- SelectionCheckboxColumn.getValue() (line 2140): Checks CompetitorSelectionProvider directly, NOT its own selection model!
Proposed Refactoring
Goal
Use ONLY the RefreshableMultiSelectionModel from SelectionCheckboxColumn as the single source of truth, synchronized bidirectionally with CompetitorSelectionProvider.
Step-by-Step Plan
Step 1: Use SelectionCheckboxColumn's Model as Table's Model
File: LeaderboardPanel.java
Current code (lines 525-547):
selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider);
leaderboardTable.setWidth("100%");
leaderboardSelectionModel = new MultiSelectionModel<LeaderboardRowDTO>(); // ← REMOVE THIS
selectionChangeHandler = new Handler() {
@Override
public void onSelectionChange(SelectionChangeEvent event) {
List<CompetitorDTO> selection = new ArrayList<>();
for (LeaderboardRowDTO row : getSelectedRows()) {
selection.add(row.competitor);
}
LeaderboardPanel.this.competitorSelectionProvider.setSelection(selection,
/* listenersNotToNotify */LeaderboardPanel.this);
if (blurInOnSelectionChanged > 0) {
blurInOnSelectionChanged--;
blurFocusedElementAfterSelectionChange();
}
}
};
leaderboardAsTableSelectionModelRegistration = leaderboardSelectionModel
.addSelectionChangeHandler(selectionChangeHandler);
leaderboardTable.setSelectionModel(leaderboardSelectionModel, selectionCheckboxColumn.getSelectionManager());
Refactored:
selectionCheckboxColumn = new LeaderboardSelectionCheckboxColumn(competitorSelectionProvider);
leaderboardTable.setWidth("100%");
// Use the SelectionCheckboxColumn's model as THE selection model
leaderboardSelectionModel = selectionCheckboxColumn.getSelectionModel();
selectionChangeHandler = new Handler() {
@Override
public void onSelectionChange(SelectionChangeEvent event) {
if (updatingSelectionFromProvider) {
// Guard: don't sync back to provider if we're currently syncing FROM provider
return;
}
List<CompetitorDTO> selection = new ArrayList<>();
for (LeaderboardRowDTO row : getSelectedRows()) {
selection.add(row.competitor);
}
LeaderboardPanel.this.competitorSelectionProvider.setSelection(selection,
/* listenersNotToNotify */LeaderboardPanel.this);
if (blurInOnSelectionChanged > 0) {
blurInOnSelectionChanged--;
blurFocusedElementAfterSelectionChange();
}
}
};
leaderboardAsTableSelectionModelRegistration = leaderboardSelectionModel
.addSelectionChangeHandler(selectionChangeHandler);
leaderboardTable.setSelectionModel(leaderboardSelectionModel, selectionCheckboxColumn.getSelectionManager());
Changes:
- Line 527: Change from
new MultiSelectionModel<>()toselectionCheckboxColumn.getSelectionModel() - Add guard flag
updatingSelectionFromProviderto prevent recursion - Declare field:
private boolean updatingSelectionFromProvider = false;
Step 2: Update Field Declaration
Current (line 239):
private final MultiSelectionModel<LeaderboardRowDTO> leaderboardSelectionModel;
Refactored:
private final RefreshableMultiSelectionModel<LeaderboardRowDTO> leaderboardSelectionModel;
Add import:
import com.sap.sse.gwt.client.celltable.RefreshableMultiSelectionModel;
Step 3: Fix LeaderboardPanel's Selection Callbacks
Current (lines 3393-3406):
@Override
public void addedToSelection(CompetitorDTO competitor) {
LeaderboardRowDTO row = getRow(competitor.getIdAsString());
if (row != null) {
leaderboardSelectionModel.setSelected(row, true);
}
}
@Override
public void removedFromSelection(CompetitorDTO competitor) {
LeaderboardRowDTO row = getRow(competitor.getIdAsString());
if (row != null) {
leaderboardSelectionModel.setSelected(row, false);
}
}
Refactored:
@Override
public void addedToSelection(CompetitorDTO competitor) {
LeaderboardRowDTO row = getRow(competitor.getIdAsString());
if (row != null) {
updatingSelectionFromProvider = true;
try {
leaderboardSelectionModel.setSelected(row, true);
} finally {
updatingSelectionFromProvider = false;
}
}
}
@Override
public void removedFromSelection(CompetitorDTO competitor) {
LeaderboardRowDTO row = getRow(competitor.getIdAsString());
if (row != null) {
updatingSelectionFromProvider = true;
try {
leaderboardSelectionModel.setSelected(row, false);
} finally {
updatingSelectionFromProvider = false;
}
}
}
Changes:
- Set guard flag before updating selection model
- Ensures selectionChangeHandler won't sync back to CompetitorSelectionProvider
Step 4: Remove SelectionCheckboxColumn's Redundant Listener
File: LeaderboardPanel.java, inner class LeaderboardSelectionCheckboxColumn
Current (lines 2120-2136):
protected LeaderboardSelectionCheckboxColumn(final CompetitorSelectionProvider competitorSelectionProvider) {
super(style.getTableresources().cellTableStyle().cellTableCheckboxSelected(),
style.getTableresources().cellTableStyle().cellTableCheckboxDeselected(),
style.getTableresources().cellTableStyle().cellTableCheckboxColumnCell(),
new EntityIdentityComparator<LeaderboardRowDTO>() {
@Override
public boolean representSameEntity(LeaderboardRowDTO dto1, LeaderboardRowDTO dto2) {
return dto1.competitor.getIdAsString().equals(dto2.competitor.getIdAsString());
}
@Override
public int hashCode(LeaderboardRowDTO t) {
return t.competitor.getIdAsString().hashCode();
}
}, getData(), leaderboardTable);
competitorSelectionProvider.addCompetitorSelectionChangeListener(this); // ← REMOVE THIS
}
Refactored:
protected LeaderboardSelectionCheckboxColumn(final CompetitorSelectionProvider competitorSelectionProvider) {
super(style.getTableresources().cellTableStyle().cellTableCheckboxSelected(),
style.getTableresources().cellTableStyle().cellTableCheckboxDeselected(),
style.getTableresources().cellTableStyle().cellTableCheckboxColumnCell(),
new EntityIdentityComparator<LeaderboardRowDTO>() {
@Override
public boolean representSameEntity(LeaderboardRowDTO dto1, LeaderboardRowDTO dto2) {
return dto1.competitor.getIdAsString().equals(dto2.competitor.getIdAsString());
}
@Override
public int hashCode(LeaderboardRowDTO t) {
return t.competitor.getIdAsString().hashCode();
}
}, getData(), leaderboardTable);
// REMOVED: competitorSelectionProvider.addCompetitorSelectionChangeListener(this);
// LeaderboardPanel is now the ONLY listener that syncs selection state
}
Current (lines 2160-2176, now unnecessary):
@Override
public void addedToSelection(CompetitorDTO competitor) {
final LeaderboardRowDTO row = getRow(competitor.getIdAsString());
if (row != null) {
getSelectionModel().setSelected(row, true);
}
}
@Override
public void removedFromSelection(CompetitorDTO competitor) {
final LeaderboardRowDTO row = getRow(competitor.getIdAsString());
if (row != null) {
getSelectionModel().setSelected(row, false);
}
}
Refactored: Remove these methods entirely. The SelectionCheckboxColumn no longer needs to implement CompetitorSelectionChangeListener.
Also remove (line 2119):
// FROM:
private class LeaderboardSelectionCheckboxColumn
extends com.sap.sse.gwt.client.celltable.SelectionCheckboxColumn<LeaderboardRowDTO>
implements CompetitorSelectionChangeListener {
// TO:
private class LeaderboardSelectionCheckboxColumn
extends com.sap.sse.gwt.client.celltable.SelectionCheckboxColumn<LeaderboardRowDTO> {
Remove the empty listener method implementations at lines 2144-2154.
Step 5: Simplify updateSelection()
Current (lines 2707-2721):
private void updateSelection(LeaderboardRowDTO row) {
final boolean shallBeSelected = competitorSelectionProvider.isSelected(row.competitor);
if (leaderboardAsTableSelectionModelRegistration != null) {
// suspend selection events while actively adjusting the leaderboardSelectionModel to match the
// competitorSelectionProvider
leaderboardAsTableSelectionModelRegistration.removeHandler();
leaderboardAsTableSelectionModelRegistration = null;
}
if (leaderboardSelectionModel.isSelected(row) != shallBeSelected) {
leaderboardSelectionModel.setSelected(row, shallBeSelected);
}
// register the selection change handler again
leaderboardAsTableSelectionModelRegistration = leaderboardTable.getSelectionModel()
.addSelectionChangeHandler(selectionChangeHandler);
}
Refactored:
private void updateSelection(LeaderboardRowDTO row) {
final boolean shallBeSelected = competitorSelectionProvider.isSelected(row.competitor);
updatingSelectionFromProvider = true;
try {
if (leaderboardSelectionModel.isSelected(row) != shallBeSelected) {
leaderboardSelectionModel.setSelected(row, shallBeSelected);
}
} finally {
updatingSelectionFromProvider = false;
}
}
Changes:
- Removed handler removal/re-registration complexity
- Use guard flag instead
- Much simpler and clearer
Step 6: Fix getValue() in LeaderboardSelectionCheckboxColumn
Current (lines 2139-2141):
@Override
public Boolean getValue(LeaderboardRowDTO row) {
return competitorSelectionProvider.isSelected(row.competitor);
}
Refactored:
@Override
public Boolean getValue(LeaderboardRowDTO row) {
// Use the selection model as the source of truth, not CompetitorSelectionProvider
return getSelectionModel().isSelected(row);
}
Rationale: The selection model should be the single source of truth for rendering. It stays synchronized with CompetitorSelectionProvider via the LeaderboardPanel's listeners.
Summary of Changes
- LeaderboardPanel.java line 527: Change to use
selectionCheckboxColumn.getSelectionModel() - LeaderboardPanel.java line 239: Change type to
RefreshableMultiSelectionModel - LeaderboardPanel.java: Add field
private boolean updatingSelectionFromProvider = false; - LeaderboardPanel.java lines 530-544: Add guard check in selectionChangeHandler
- LeaderboardPanel.java lines 3393-3406: Add guard flag around setSelected calls
- LeaderboardPanel.java lines 2707-2721: Simplify updateSelection() using guard flag
- LeaderboardPanel.LeaderboardSelectionCheckboxColumn line 2135: Remove listener registration
- LeaderboardPanel.LeaderboardSelectionCheckboxColumn line 2119: Remove
implements CompetitorSelectionChangeListener - LeaderboardPanel.LeaderboardSelectionCheckboxColumn lines 2144-2176: Remove listener methods
- LeaderboardPanel.LeaderboardSelectionCheckboxColumn line 2140: Use selection model instead of provider
Benefits
- Single Selection Model: RefreshableMultiSelectionModel is the only selection model
- No Orphaned State: Eliminates the unused selection model in SelectionCheckboxColumn
- Simpler Synchronization: Single bidirectional sync point between model and provider
- No Handler Removal: Guard flag is simpler than removing/re-adding handlers
- Clearer Ownership: LeaderboardPanel owns synchronization, SelectionCheckboxColumn just renders
Testing Strategy
- Test user clicking checkboxes - selection should sync to CompetitorSelectionProvider
- Test external CompetitorSelectionProvider changes - should update checkboxes
- Test rapid clicking - no infinite loops or race conditions
- Test with filtering - filtered rows should maintain correct selection state
- Test leaderboard updates - selection should persist across data refreshes
Potential Risks
-
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.
-
Multiple LeaderboardPanels: If multiple LeaderboardPanels share the same CompetitorSelectionProvider, each will receive callbacks. This should work fine as each has its own guard flag.
-
RefreshableMultiSelectionModel.refreshSelectionModel(): This method has its own
dontCheckSelectionStateguard (RefreshableMultiSelectionModel.java line 131). Verify this doesn't interfere with our guard flag.
Future Improvements
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.