-
Notifications
You must be signed in to change notification settings - Fork 2k
[WinUI][CV2] Fix for Null Item Handling - follow-ups from CollectionView2 Windows handler (#34600) #36170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: net11.0
Are you sure you want to change the base?
[WinUI][CV2] Fix for Null Item Handling - follow-ups from CollectionView2 Windows handler (#34600) #36170
Changes from 4 commits
75bec78
a1d258e
c855b13
9717de3
d4897ac
3f152dc
2ca466a
662726c
2a7fbde
18bdfbd
f5716e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,10 @@ public partial class CollectionViewHandler2 : ReorderableItemsViewHandler2<Reord | |
| { | ||
| bool _ignorePlatformSelectionChange; | ||
| bool _selectionDirty; | ||
| // Prevents MapSelectedItem from calling UpdatePlatformSelection when | ||
| // UpdateVirtualSingleSelection is writing SelectedItem in response to a | ||
| // platform tap — otherwise the null-item WinUI selection gets undone. | ||
| bool _ignoreVirtualSelectionChange; | ||
|
|
||
| // Cache for MeasureFirstItem optimization | ||
| global::Windows.Foundation.Size _firstItemMeasuredSize = global::Windows.Foundation.Size.Empty; | ||
|
|
@@ -75,6 +79,13 @@ public static void MapItemsSource(CollectionViewHandler2 handler, SelectableItem | |
|
|
||
| public static void MapSelectedItem(CollectionViewHandler2 handler, SelectableItemsView itemsView) | ||
| { | ||
| // When UpdateVirtualSingleSelection sets SelectedItem in response to a platform | ||
| // tap, skip the round-trip back to UpdatePlatformSelection. Without this guard, | ||
| // tapping a null item causes: WinUI selects null → SelectedItem = null → | ||
| // MapSelectedItem → UpdatePlatformSelection → DeselectAll (undoes the selection). | ||
| if (handler._ignoreVirtualSelectionChange) | ||
|
SuthiYuvaraj marked this conversation as resolved.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[major] Logic and Correctness / Regression Prevention - This guard is too broad and breaks the most common MAUI selection idiom on Windows CV2.
Concrete failing scenario - the canonical tap-then-clear pattern: cv.SelectionChanged += (s, e) => { Navigate(e.CurrentSelection[0]); cv.SelectedItem = null; };
Before this PR both writes reached Suggested fix: make the guard value-scoped rather than time-scoped - record the value being written (e.g.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[major] Handler Mapper and Property Patterns — This guard is broader than the round-trip it is meant to break, and it swallows re-entrant
Concrete scenario (very common MAUI pattern — clearing selection so the same row can be tapped twice):
Result: MAUI
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Handler Mapper and Property Patterns — Suppressing |
||
| return; | ||
|
|
||
| handler.UpdatePlatformSelection(); | ||
| } | ||
|
|
||
|
|
@@ -277,7 +288,10 @@ void UpdateVisualStates() | |
| if (itemContainer?.Child is ElementWrapper wrapper && wrapper.VirtualView is VisualElement visualElement) | ||
| { | ||
| var actualItem = visualElement.BindingContext; | ||
| bool isSelected = object.Equals(ItemsView.SelectedItem, actualItem) || ItemsView.SelectedItems.Contains(actualItem); | ||
| // Guard against false positives for null items: object.Equals(null, null) = true would | ||
| // mark every null-item container as Selected whenever SelectedItem is null (no selection). | ||
| bool isSelected = actualItem is not null && | ||
| (object.Equals(ItemsView.SelectedItem, actualItem) || ItemsView.SelectedItems.Contains(actualItem)); | ||
| VisualStateManager.GoToState(visualElement, isSelected ? VisualStateManager.CommonStates.Selected : VisualStateManager.CommonStates.Normal); | ||
|
|
||
| // When the item template defines a "Selected" visual state, MAUI | ||
|
|
@@ -313,10 +327,13 @@ void UpdateVirtualSingleSelection() | |
| ? itemPair.Item | ||
| : PlatformView.SelectedItem; | ||
|
|
||
| // Use flag instead of detach/re-attach so that MapSelectedItem is suppressed | ||
| // while SelectedItem is set. Both fire synchronously; the flag is reset after. | ||
| _ignoreVirtualSelectionChange = true; | ||
|
SuthiYuvaraj marked this conversation as resolved.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Cross-Platform Behavioral Consistency — The round-trip guard is applied only to the single-selection path. |
||
| ItemsView.SelectionChanged -= VirtualSelectionChanged; | ||
| ItemsView.SelectedItem = selectedItem; | ||
|
|
||
| ItemsView.SelectionChanged += VirtualSelectionChanged; | ||
| _ignoreVirtualSelectionChange = false; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [moderate] Handler Mapper and Property Patterns —
SuthiYuvaraj marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| void UpdateVirtualMultipleSelection() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,7 +48,7 @@ protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) | |
| } | ||
| } | ||
|
|
||
| ItemTemplateContext2 CreateItemContext(object item) => | ||
| ItemTemplateContext2 CreateItemContext(object? item) => | ||
| new(_itemTemplate, item, _container, mauiContext: _mauiContext); | ||
|
|
||
| ItemTemplateContext2? CreateHeaderContext(object group) => | ||
|
|
@@ -320,11 +320,9 @@ void HandleGroupItemsReplace(NotifyCollectionChangedEventArgs e, int flatIndex) | |
| for (int i = 0; i < e.NewItems.Count; i++) | ||
| { | ||
| oldItems.Add(Items[replaceIndex + i]); | ||
| var item = e.NewItems[i]; | ||
| if (item is null) | ||
| continue; | ||
|
|
||
| var newItem = CreateItemContext(e.NewItems[i]!); | ||
| // Always create a new context even for null new items — skipping the slot update | ||
| // would leave the stale old ItemTemplateContext2 in the flat list (desync). | ||
| var newItem = CreateItemContext(e.NewItems[i]); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Regression Prevention - The The grouped path received two behaviour-affecting changes in this PR - this one and the switch of Please add at least: (a) a test that replaces a group item with
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Regression Prevention and Test Coverage — This desync fix (always creating a replacement context so the flat mirror keeps a slot for a null new item) is correct and matches the initial-build path at line 140, which never skipped nulls. It has no test: neither device test in this PR uses |
||
| newItems.Add(newItem); | ||
| Items[replaceIndex + i] = newItem; | ||
|
Comment on lines
+323
to
327
|
||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,25 @@ internal partial class ItemFactory(ItemsView view) : IElementFactory | |
| // NOTE: 1.6: replace w/ RecyclePool | ||
| if (args.Data is ItemTemplateContext2 templateContext) | ||
| { | ||
| // Null regular-data items render as blank rows with no template content, | ||
| // matching CV1 behaviour (ItemContentControl.Realize() returns early when | ||
| // dataContext is null). Header/footer items fall through to the normal | ||
| // path so they can inherit the parent ItemsView.BindingContext. | ||
| if (templateContext.Item is null && !templateContext.IsHeader && !templateContext.IsFooter) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Performance-Critical Path — The new blank
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Cross-Platform Behavioral Consistency — Short-circuiting here means a null data item no longer inflates the On Android ( If the goal is only to avoid the crash, the narrower fix is to keep realizing the template (and to guard |
||
| { | ||
| // CV1's ItemContentControl.MeasureOverride returns a 32px height hint when | ||
| // _handler is null (virtualization hint only). The actual rendered height | ||
| // is driven by WinUI's ListViewItem default MinHeight (40px from the | ||
| // ListViewItemMinHeight theme resource). CV2 has no ListViewItem wrapper, so | ||
| // set MinHeight = 40 directly on the ItemContainer to match CV1's visual slot. | ||
| return new ItemContainer | ||
|
SuthiYuvaraj marked this conversation as resolved.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[major] Windows CollectionView layout — The null-item placeholder only sets
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[major] Memory Leak Prevention / Performance-Critical Path — Null-item
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Cross-Platform Behavioral Consistency / Backward Compatibility - Returning a bare Before this change a null item still realized the On iOS/MacCatalyst CV2 ( Also,
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Layout Measure-Arrange Correctness — Hardcoding
Please either exclude blank containers from the layout''s size derivation, or size the blank container from the measured/cached item size ( |
||
| { | ||
| MinHeight = 40, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] CollectionView Windows Layout — The null-item placeholder only sets a vertical size hint. In a horizontal |
||
| VerticalAlignment = VerticalAlignment.Stretch, | ||
| HorizontalAlignment = HorizontalAlignment.Stretch | ||
| }; | ||
|
SuthiYuvaraj marked this conversation as resolved.
Outdated
|
||
| } | ||
|
Comment on lines
+56
to
+76
|
||
|
|
||
| DataTemplate? template = templateContext.MauiDataTemplate; | ||
| if (template is DataTemplateSelector selector) | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -323,7 +323,7 @@ void ItemsRepeater_ElementPrepared(ItemsRepeater sender, ItemsRepeaterElementPre | |
| // item's new position, hide it immediately. This is what makes the | ||
| // "empty source slot" follow the dragged item without us having to | ||
| // chase a moving _sourceContainer reference through dispatcher races. | ||
| if (_draggedItem is not null && IsContainerBoundToDraggedItem(itemContainer)) | ||
| if (_draggedSourceIndex >= 0 && IsContainerBoundToDraggedItem(itemContainer)) | ||
| { | ||
| itemContainer.Opacity = 0; | ||
| itemContainer.IsHitTestVisible = false; | ||
|
|
@@ -332,7 +332,7 @@ void ItemsRepeater_ElementPrepared(ItemsRepeater sender, ItemsRepeaterElementPre | |
| else | ||
| { | ||
| // Apply dim if a drag is in progress and this is not the source. | ||
| itemContainer.Opacity = _draggedItem is not null ? DragDimOpacity : 1; | ||
| itemContainer.Opacity = _draggedSourceIndex >= 0 ? DragDimOpacity : 1; | ||
| itemContainer.IsHitTestVisible = true; | ||
| } | ||
| } | ||
|
|
@@ -409,21 +409,31 @@ void ItemContainer_DragStarting(UIElement sender, UI.Xaml.DragStartingEventArgs | |
| { | ||
| var itemContainer = (ItemContainer)sender; | ||
|
|
||
| // Check whether this container is bound to a source slot at all. | ||
| // A container that has an ElementWrapper with a View IS bound — the item | ||
| // itself may be null (valid null data row), so we must not treat null as | ||
| // "no binding". We store the result so the null-item path shares the same | ||
| // cancel logic as the "container not yet set up" path. | ||
| bool hasBinding = itemContainer.Child is ElementWrapper _ew && _ew.VirtualView is View; | ||
|
Copilot marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Use the container's currently bound item first. The Tag/index can become | ||
| // stale after a reorder because the element is reused without being recreated. | ||
| object? item = GetContainerItem(itemContainer); | ||
|
|
||
| // Fallback: look up by index from the source (works for IList and IEnumerable). | ||
| if (item is null && itemContainer.Tag is int index && index >= 0) | ||
| // Only run when there is no ElementWrapper binding (container not yet set up), | ||
| // NOT when item is null — a null BindingContext is a valid null data row. | ||
| if (!hasBinding && itemContainer.Tag is int index && index >= 0) | ||
| { | ||
| var sourceList = GetSourceList(); | ||
| if (sourceList is not null && index < sourceList.Count) | ||
| { | ||
| item = GetItemAtIndex(index, sourceList); | ||
| hasBinding = true; | ||
| } | ||
| } | ||
|
|
||
| if (item is null) | ||
| if (!hasBinding) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Logic and Correctness — Now that every downstream guard was switched from
|
||
| { | ||
| args.Cancel = true; | ||
| return; | ||
|
Comment on lines
+446
to
449
|
||
|
|
@@ -539,17 +549,28 @@ void ScrollViewer_DragOver(object sender, UI.Xaml.DragEventArgs e) | |
|
|
||
| bool IsContainerBoundToDraggedItem(ItemContainer container) | ||
| { | ||
| return _draggedItem is not null && IsContainerBoundToItem(container, _draggedItem); | ||
| // Use _draggedSourceIndex as the "drag active" sentinel so that a null | ||
| // _draggedItem (valid null data row) does not short-circuit the check. | ||
| return _draggedSourceIndex >= 0 && IsContainerBoundToItem(container, _draggedItem); | ||
| } | ||
|
|
||
| static bool IsContainerBoundToItem(ItemContainer container, object item) | ||
| static bool IsContainerBoundToItem(ItemContainer container, object? item) | ||
| { | ||
| if (container.Child is not ElementWrapper wrapper || wrapper.VirtualView is not View view) | ||
| { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[minor] Complexity Reduction — This branch is unreachable from the only caller and over-matches if it ever becomes reachable.
|
||
| return false; | ||
| // Blank container (no ElementWrapper) represents a null data item. | ||
| // It matches when the dragged item is also null. | ||
| return item is null; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [major] Logic and Correctness — Treating any blank
SuthiYuvaraj marked this conversation as resolved.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Logic and Correctness - This new branch (and the It is also unreachable for the case the comment describes. The only caller, Recommend deleting these null-matching branches and keeping
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[minor] Complexity Reduction — This new early-return is unreachable. |
||
| } | ||
|
|
||
| var bound = view.BindingContext; | ||
| // Allow null-bound containers to match a null dragged item. | ||
| // ReferenceEquals(null, null) == true, Equals(null, null) == true. | ||
| if (bound is null && item is null) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] CollectionView Windows (Drag/Drop) — This new null-matching branch (plus the corresponding removal of the |
||
| { | ||
| return true; | ||
| } | ||
|
|
||
| if (bound is null) | ||
| { | ||
| return false; | ||
|
|
@@ -619,7 +640,9 @@ void ScrollViewer_DragLeave(object sender, UI.Xaml.DragEventArgs e) | |
|
|
||
| void ScrollViewer_Drop(object sender, UI.Xaml.DragEventArgs e) | ||
| { | ||
| if (!_canReorderItems || _draggedItem is null || _insertionIndex < 0 || _mauiVirtualView is null) | ||
| // _draggedSourceIndex < 0 means no active drag. _draggedItem may be null for | ||
| // null data rows, so we cannot use _draggedItem is null as the guard here. | ||
| if (!_canReorderItems || _draggedSourceIndex < 0 || _insertionIndex < 0 || _mauiVirtualView is null) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] Null Safety and Defensive Coding - Swapping the drag-in-progress sentinel from When that happens Guard at the source - after computing the index in _draggedSourceIndex = GetContainerIndex(itemContainer);
if (_draggedSourceIndex < 0) { args.Cancel = true; return; } |
||
| { | ||
| CleanupDragState(); | ||
| return; | ||
|
|
@@ -701,7 +724,8 @@ void ScrollViewer_Drop(object sender, UI.Xaml.DragEventArgs e) | |
|
|
||
| bool PerformReorder(IList itemsList) | ||
| { | ||
| if (_draggedItem is null) | ||
| // _draggedSourceIndex < 0 means no active drag; _draggedItem may be null for null data rows. | ||
| if (_draggedSourceIndex < 0) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[major] Logic and Correctness Verification — With |
||
| { | ||
| return false; | ||
| } | ||
|
|
@@ -799,7 +823,8 @@ bool PerformReorder(IList itemsList) | |
| /// </summary> | ||
| bool PerformGroupedReorder(IList groupsList) | ||
| { | ||
| if (_draggedItem is null || _mauiVirtualView is not GroupableItemsView groupableView) | ||
| // _draggedSourceIndex < 0 means no active drag; _draggedItem may be null for null data rows. | ||
| if (_draggedSourceIndex < 0 || _mauiVirtualView is not GroupableItemsView groupableView) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [major] Logic and Correctness — Allowing
SuthiYuvaraj marked this conversation as resolved.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[major] CollectionView Windows (Drag/Drop) — Changing this guard from
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[major] CollectionView — Shared Models — Allowing null drags into |
||
| { | ||
| return false; | ||
| } | ||
|
|
@@ -1179,18 +1204,23 @@ int GetContainerIndex(FrameworkElement container) | |
| // index and avoids the ambiguity where group headers and footers share the | ||
| // same underlying Item (the group object). Validate the tag by checking that | ||
| // the item at that index still matches the container's current item. | ||
| // The containerItem is not null guard was intentionally removed: null-item | ||
| // containers (valid null data rows) need tag validation too, and | ||
| // Equals(null, null) correctly returns true for them. | ||
| if (container.Tag is int tagIndex && sourceList is not null && | ||
|
Comment on lines
1255
to
1262
|
||
| tagIndex >= 0 && tagIndex < sourceList.Count) | ||
| { | ||
| var tagItem = GetItemAtIndex(tagIndex, sourceList); | ||
| if (containerItem is not null && Equals(tagItem, containerItem)) | ||
| if (Equals(tagItem, containerItem)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [major] Logic and Correctness — The tag validation now accepts
SuthiYuvaraj marked this conversation as resolved.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[moderate] CollectionView Windows (Drag/Drop) — Removing the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[major] CollectionView — Windows — This validation now accepts
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
[minor] Complexity Reduction - The comment added above this line ( The new early return on line 1240 ( Either drop these two comments, or restore the explicit |
||
| { | ||
|
Comment on lines
+1259
to
1267
|
||
| return tagIndex; | ||
| } | ||
| } | ||
|
|
||
| // Tag is stale — fall back to a linear search. | ||
| if (sourceList is not null && containerItem is not null) | ||
| // The containerItem is not null guard was removed: null-item containers | ||
| // need linear search fallback just like any other item. | ||
| if (sourceList is not null) | ||
|
Comment on lines
1272
to
+1275
|
||
| { | ||
| var liveIndex = IndexOfItem(containerItem, sourceList); | ||
| if (liveIndex >= 0) | ||
|
|
@@ -1209,7 +1239,7 @@ int GetContainerIndex(FrameworkElement container) | |
| return allContainers.IndexOf(container); | ||
| } | ||
|
|
||
| int IndexOfItem(object item, IList itemsList) | ||
| int IndexOfItem(object? item, IList itemsList) | ||
| { | ||
| // First pass: reference equality — correctly distinguishes two items that are | ||
| // value-equal but distinct objects (e.g., duplicate records in the list). | ||
|
|
@@ -1256,6 +1286,8 @@ void UpdateAllContainerIndices() | |
| return; | ||
| } | ||
|
|
||
| var repeater = ItemsRepeaterControl; | ||
|
|
||
| // Derive each container's Tag from its item's actual position in the source. | ||
| // A positional loop (containers[i].Tag = i) is wrong when ItemsRepeater | ||
| // virtualizes: FindAllContainers skips unrealized slots, so containers[i] | ||
|
|
@@ -1271,6 +1303,20 @@ void UpdateAllContainerIndices() | |
| container.Tag = actualIndex; | ||
| } | ||
| } | ||
| else | ||
| { | ||
| // For null-item containers, IndexOfItem returns the first null which | ||
| // may be a different row. Use the ItemsRepeater's authoritative element | ||
| // index instead — it is always accurate after a layout pass. | ||
| if (repeater is not null && container is ItemContainer ic) | ||
| { | ||
| int repeaterIndex = repeater.GetElementIndex(ic); | ||
| if (repeaterIndex >= 0) | ||
| { | ||
| container.Tag = repeaterIndex; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -1664,7 +1710,9 @@ static void RemoveDragGhostAppearance(ItemContainer container) | |
| /// </summary> | ||
| void DimNonSourceContainers() | ||
| { | ||
| if (_draggedItem is null) | ||
| // _draggedSourceIndex < 0 means no drag is in progress. | ||
| // _draggedItem may be null for null data rows, so we cannot use that as the guard. | ||
| if (_draggedSourceIndex < 0) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[moderate] Cross-Platform Behavioral Consistency - The new flag is only honoured for
SelectionMode.Single.UpdateVirtualMultipleSelectiondoes not set it,MapSelectedItemshas no equivalent guard, andExtractPlatformSelectedItems()explicitly drops nulls (if (selectedItem is not null) result.Add(...), line 427).So in
SelectionMode.Multiplea null row can now be checked in the WinUI UI (it gets a real, selectableItemContainerfrom the newItemFactorypath) but it never lands inSelectedItems, and the first subsequentMapSelectedItems->UpdatePlatformSelection->DeselectAll()visually undoes the check. That is exactly the round-trip this PR fixes for Single mode, left in place for Multiple.Either extend the fix to multi-select or explicitly document/assert that null items are not selectable in Multiple mode, and add a device test pinning the chosen behaviour.