Skip to content

[Controls] Add multi-selection support to GroupedTileListBox - #598

Merged
Xavier Fortin (xfortin-devolutions) merged 3 commits into
masterfrom
GroupedTileListBox-support-multi-selection
Jul 22, 2026
Merged

[Controls] Add multi-selection support to GroupedTileListBox#598
Xavier Fortin (xfortin-devolutions) merged 3 commits into
masterfrom
GroupedTileListBox-support-multi-selection

Conversation

@xfortin-devolutions

@xfortin-devolutions Xavier Fortin (xfortin-devolutions) commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds multi-selection support to GroupedTileListBox by exposing the
relevant SelectingItemsControl-style properties: SelectionMode
and SelectedItems (two-way).

SelectionMode defaults to Single, so existing behavior is unchanged.
Set it to Multiple (optionally combined with Toggle / AlwaysSelected)
to enable multi-selection.

Behavior in Multiple mode

  • Pointer: plain click replaces · Ctrl/Cmd+click toggles · Shift+click
    selects a range from the anchor · Ctrl/Cmd+Shift+click adds the range.
  • Keyboard: arrows move · Shift+arrows extend the range · Ctrl/Cmd+Space
    toggles the current tile · Ctrl/Cmd+A selects all.

Notes

  • SelectedItems is kept in sync in every mode (0/1 items in Single) and
    can be bound two-way to drive or observe the selection.
  • Membership uses reference equality, consistent with the control's
    existing identity-based selection (avoids false positives for value-equal
    items such as records).
  • Internal selection updates are guarded against re-entrancy so property /
    collection writes don't loop back through the external-change handlers.
  • Known limitation: there is no focus cursor distinct from selection, so
    Ctrl/Cmd+arrow behaves as a plain move (not focus-without-select). A true
    focus-vs-selection model would require a new pseudo-class styled across all
    themes and is left as a follow-up.

Demo

Adds Scenario 5: Multiple Selection to the GroupedTileListBox demo page
(Multiple-mode list two-way bound to a MultiSelectedItems collection, with
a live chip list of the current selection) so the gestures can be exercised
manually.

Testing

  • Builds clean (control + SampleApp).
  • Ran the SampleApp on the demo page; renders with no binding errors.
  • Interactive multi-select gestures validated manually via Scenario 5.

⚠️ Visual baselines

Adding Scenario 5 changes the GroupedTileListBoxDemo screenshot, so its
visual-regression baselines will need regenerating on the CI/reference
machine.

Expose SelectionMode and SelectedItems (two-way) so the control can
behave like SelectingItemsControl when multiple selection is desired.
Default SelectionMode is Single, preserving existing behavior.

In Multiple mode:
- Pointer: plain click replaces, Ctrl/Cmd+click toggles, Shift+click
  selects a range from the anchor, Ctrl/Cmd+Shift+click adds the range.
- Keyboard: Shift+arrows extend the range, Ctrl/Cmd+Space toggles the
  current tile, Ctrl/Cmd+A selects all.

SelectedItems is kept in sync in every mode (0/1 items in Single) and
membership uses reference equality to stay consistent with the control's
identity-based selection (avoids false positives for value-equal items).
Internal updates are guarded against re-entrancy so property/collection
writes don't loop back through the external-change handlers.
Add "Scenario 5: Multiple Selection" to the GroupedTileListBox demo:
a Multiple-mode list two-way bound to a MultiSelectedItems collection,
with a live chip list showing the current selection. Lets the multi-
selection gestures be exercised manually.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds multi-selection support to the virtualized GroupedTileListBox.

Changes:

  • Exposes SelectionMode and two-way SelectedItems.
  • Implements pointer and keyboard multi-selection gestures.
  • Adds a live multi-selection demo scenario.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.

File Description
GroupedTileListBox.axaml.cs Implements multi-selection state and interactions.
GroupedTileListBoxViewModel.cs Adds the demo’s selected-items collection.
GroupedTileListBoxDemo.axaml Adds the multiple-selection scenario.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread samples/SampleApp/DemoPages/GroupedTileListBoxDemo.axaml
Fixes correctness, consistency, and performance issues raised in review:

- OnCollectionChanged: reconcile the selection against the new contents
  instead of replacing it, so multi-selection survives ItemsSource
  add/remove/reset (secondary selections and a still-present primary are
  no longer dropped).
- SyncFromSelectedItems: enforce the Single-mode 0/1 invariant on an
  externally-provided SelectedItems collection.
- OnSelectedItem/IndexChanged: gate on the primary's identity (not mere
  membership) and promote an already-selected item to primary without
  clearing the rest, keeping SelectedItem/SelectedIndex consistent.
- Use reference equality consistently: GetIndexOfItem reference-scans and
  sourceToVisualMap uses ReferenceEqualityComparer, so value-equal items
  (e.g. records) map to distinct indices/anchors.
- AlwaysSelected: enforce the "never empty while items exist" invariant
  across the clear/reconcile/sync/mode-change paths, not just on toggle.
- Store the range anchor as an item reference (resolved to a visual index
  on demand) so it stays valid when grouping rebuilds the visual map, and
  set it whenever the primary is derived.
- SelectRangeToVisual: use a reference-equality HashSet for O(1) membership
  so a k-item range is O(k) instead of O(k^2).
@xfortin-devolutions

Copy link
Copy Markdown
Contributor Author

Copilot Can you review again to make sure that all the previous comments have been properly addressed?

Copilot AI commented Jul 22, 2026

Copy link
Copy Markdown

Copilot Can you review again to make sure that all the previous comments have been properly addressed?

I re-checked the feedback and the current branch: the prior code-review concerns on GroupedTileListBox.axaml.cs are addressed in 41ac7f6 (selection reconciliation, Single-mode normalization, primary promotion, reference-equality lookups, AlwaysSelected enforcement, anchor handling, and range-selection performance). The only previously discussed item not applied remains the visual-baseline regeneration, which was explicitly deferred in the earlier thread.

@xfortin-devolutions
Xavier Fortin (xfortin-devolutions) merged commit 16472f6 into master Jul 22, 2026
1 check passed
@xfortin-devolutions
Xavier Fortin (xfortin-devolutions) deleted the GroupedTileListBox-support-multi-selection branch July 22, 2026 17:06
Xavier Fortin (xfortin-devolutions) added a commit that referenced this pull request Jul 28, 2026
## Summary

Adds Windows Explorer-style **rubber-band (marquee) selection** to
`GroupedTileListBox`, building on the multi-selection support from #598.
Dragging over empty space draws a rectangle and selects every tile it
covers, with auto-scroll when the pointer reaches the top/bottom edge.

## Behavior

- **Activation** — active only when `SelectionMode` includes `Multiple`,
and gated by a new opt-out `EnableRectangleSelection` property (default
`true`). Single-select usages are unaffected.
- **Modifiers** — plain drag **replaces** the selection with the
rectangle's contents; **Ctrl/Cmd+drag adds** (union). A plain click on
empty space clears the selection (Explorer-style); **Escape** or lost
pointer-capture reverts to the exact pre-drag selection (collection,
primary, and range anchor).
- **Auto-scroll** — dragging past the top/bottom edge scrolls with a
ramped speed and keeps extending the selection.

## Implementation notes

- **Overlay** — a viewport-fixed `Canvas` template part
(`PART_SelectionRectangle`) shares the `ScrollViewer`'s origin, so it
needs no coordinate reconciliation and never scrolls with content.
Accent-tinted brushes are theme-aware via `SystemAccentColor`; the
template lives only in the default theme (the per-theme files use
`BasedOn`), so one edit covers all themes.
- **Hit-testing is analytic, not container-based** — a marquee is a 2D
region test (a narrow vertical drag must select only one column, not a
contiguous range), and items are virtualized, so off-screen tiles must
still be testable. Rectangles are computed analytically from the layout.
- **Single source of truth for tile geometry** — a shared
`EnumerateItemLayout` walk (headers, rows, inter-group spacing) is
consumed by both the marquee hit-test and
`CalculateScrollOffsetForItem`, so scroll positioning and selection
can't drift apart.
- Selection is applied by diff-reconciling `SelectedItems` (reference
equality, existing re-entrancy guard), and deliberately bypasses
auto-scroll-on-selection so it doesn't fight the marquee's own
scrolling.

## Demo

SampleApp **Scenario 5** ("Multiple Selection") now uses a dedicated
multi-group dataset (8 groups, uneven counts ≈ 426 items) so groups span
many rows and the list scrolls — exercising cross-group rectangles and
edge auto-scroll. Its description was updated with the new gesture.

## Follow-up fixes (review feedback + edge cases)

- **Collection mutation mid-drag** — `OnCollectionChanged` now aborts an
in-progress marquee, so an in-place `ItemsSource` mutation can't leave a
stale baseline/anchor that re-adds a removed item on the next move or on
Escape.
- **Secondary mouse buttons** — a marquee now starts only on the
initiating left-button press (and only when none is already in progress)
and finishes only on that button's release, so a secondary button
pressed/released mid-drag no longer resets or prematurely commits it.
- **Exact-order cancel** — cancelling a marquee restores the pre-drag
selection in its exact original order.
- **Right-click preserves multi-selection** — right-clicking an item
that is already selected no longer collapses the selection to that
single item, so a context menu can act on every selected item;
right-clicking an unselected item still selects it (replacing), and the
press is left unhandled so the menu can open.

## Testing

- ✅ `Devolutions.AvaloniaControls` and `SampleApp` build clean.
- ✅ App loads the demo page without runtime/template errors.
- ⚠️ **Visual baselines**: the `GroupedTileListBoxDemo` baselines need
regenerating (per-OS, via CI) — the Scenario 5 description reflow and
the new dataset change that page at rest. The overlay itself is
`IsVisible=false` when idle, so it causes no other baseline changes.

Please give the marquee an interactive pass (drag across group headers;
drag to an edge to confirm auto-scroll into later groups; Ctrl/Cmd+drag
union; Escape revert; right-click within a multi-selection).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants