[Controls] Add multi-selection support to GroupedTileListBox - #598
Conversation
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.
There was a problem hiding this comment.
Pull request overview
Adds multi-selection support to the virtualized GroupedTileListBox.
Changes:
- Exposes
SelectionModeand two-waySelectedItems. - 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.
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).
|
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 |
## 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).
Summary
Adds multi-selection support to
GroupedTileListBoxby exposing therelevant
SelectingItemsControl-style properties:SelectionModeand
SelectedItems(two-way).SelectionModedefaults toSingle, so existing behavior is unchanged.Set it to
Multiple(optionally combined withToggle/AlwaysSelected)to enable multi-selection.
Behavior in
Multiplemodeselects a range from the anchor · Ctrl/Cmd+Shift+click adds the range.
toggles the current tile · Ctrl/Cmd+A selects all.
Notes
SelectedItemsis kept in sync in every mode (0/1 items inSingle) andcan be bound two-way to drive or observe the selection.
existing identity-based selection (avoids false positives for value-equal
items such as records).
collection writes don't loop back through the external-change handlers.
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
MultiSelectedItemscollection, witha live chip list of the current selection) so the gestures can be exercised
manually.
Testing
Adding Scenario 5 changes the
GroupedTileListBoxDemoscreenshot, so itsvisual-regression baselines will need regenerating on the CI/reference
machine.