[WinUI][CV2] CollectionView2 Handler Implementation for Windows - #34600
Conversation
|
Hi @kubaflo , The conflict has been resolved. Please review and let me know if you have any feedback or concerns. |
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Round 4 — Adversarial Multi-Model Review
Methodology: 3 independent reviewers (different model families) reviewed the changes since the previous round, with adversarial consensus — unanimous findings included directly, single-reviewer findings sent through a dispute round where the other two models weigh in. Factual claims were verified against source before posting.
Scope: the delta since the previous review HEAD — the ObservableItemTemplateCollection2 null-handling change, the CV1 ItemsViewHandler.Windows.cs update, MauiItemsView.DragDrop additions, CollectionViewHandler2 try/finally, ItemTemplateContext2, and the eng/pipelines UI-test YAML changes — plus the interactions those changes expose in sibling files.
✅ Verified correct (do not revert)
- The null-handling change (removing
if (item is null) continue;inObservableItemTemplateCollection2) is correct and fixes a latent desync:Reset()already kept nulls whilePopulateInitialItems/Add/Replacedropped them, so the wrapper could hold fewer entries than the source and drift index alignment — whichMoveItemAndSyncSourcerelies on for drag/drop. 3/3 reviewers confirmed a null item renders safely (nullBindingContext→ blank cell, matching CV1;DataTemplateSelector.SelectTemplate(null, …)does not crash in framework code). CollectionViewHandler2.UpdateItemsSourcenow resets_ignorePlatformSelectionChangein afinally. ✓ItemFactorynow assignsIsHeaderOrFooterunconditionally for new and recycled wrappers. ✓- CV1
ItemsViewHandler.Windows.csdisconnect-safety + deferredScrollIntoView. ✓ MauiItemsView.DragDropLoadedre-subscription is idempotent and unwired on disconnect. ✓eng/pipelinesUI-test YAML: everydependsOnresolves; the Mono→CoreCLR migration left no dangling stage references. ✓
Findings (each 3/3 consensus after the dispute round)
All three are downstream consequences of the (correct) decision to let null source items flow through — three different consumers don't yet handle the now-reachable null.
❌ ItemFactory.cs (~L124) — null data items render as Selected. SelectionMode=Single + nothing selected + a null source item → object.Equals(null, null) == true highlights the blank cell. Fix: guard templateContext.Item is not null before comparing. (inline)
MauiItemsView.DragDrop.cs (~L1266) — stale drag index on null cells. UpdateAllContainerIndices skips null-item containers, so their Tag goes stale after a reorder and dragging the blank cell moves the wrong row. 1 reviewer rated this ❌ (silent data corruption). Fix: fail safe by clearing the tag (or use GetElementIndex). (inline)
GroupedItemTemplateCollection2.cs (~L324) — grouped Replace with null desyncs. The null-skip that was correctly removed from the observable class still exists in the grouped sibling's HandleGroupItemsReplace, leaving the old context in place and emitting a malformed Replace. Fix: mirror the null-parity fix. (inline)
Open items from prior reviews (still unresolved — not re-flagged here)
ObservableItemTemplateCollection2.cs:178 (_innerCollectionChange not in try/finally), ItemFactory.cs:233 (RecycleElement doesn't clear BindingContext/DataContext), ItemFactory.cs:420 (MeasureFirstItem cache pollution), ItemsViewHandler2.Windows.cs:362 (collection-typed items flattened when IsGrouped=false), and the duplicate Windows failed-artifact name in ui-tests-build-sample.yml. @kubaflo's CHANGES_REQUESTED review remains open.
This review evaluates code only; CI status is out of scope.
Round‑4 update — 2026‑06‑22 (head unchanged at 27171d2643)
A follow‑up 3‑model adversarial pass was run against the unchanged head. No new merge‑blocker surfaced.
- All three findings above (
ItemFactory.cs:124,MauiItemsView.DragDrop.cs:1266,GroupedItemTemplateCollection2.cs:324) and every "open item from prior reviews" listed above remain unresolved — the head is identical, so nothing has changed. - The previously‑noted
ItemsViewHandler2.Windows.cs:362flatten open item was independently re‑confirmed: in the non‑grouped branch, whenIsGrouped=falsebut theItemsSourceitems are themselves non‑stringIEnumerable, theIsItemsSourceGroupedheuristic (L360) flattens the source — whereas CV1 (CreateCollectionViewSource) binds each inner collection as a single item. Real CV1 behavior difference; non‑crashing; uncommon shape (also reachable if a model type happens to implementIEnumerable). Fix: gate theelse‑branch flatten onIsGrouped.
One net‑new (non‑blocking) observation:
- 💡
ObservableItemTemplateCollection2.cs— indexlessReplace/Movewould throw. If a customINotifyCollectionChangedsource raisesReplace(L420) orMove(L239/L247) with a-1starting index, the flat handlers computeargs.OldStartingIndex + index→this[-1]→ArgumentOutOfRangeException. This is pre‑existing parity, not a regression introduced here: the current default CV1ObservableItemTemplateCollection(Platform/Windows/CollectionView) has line‑for‑line identicalReplace/Movewith the same gap, whileRemovein both already guards< 0with aReset(). Trigger is rare (ObservableCollection<T>always supplies indices), and the grouped sibling already bounds‑guards these paths (ResetWithoutResubscribe()). Optional fix (ideally cross‑platform): mirror theRemove< 0 → Reset()guard inReplace/Move.
Consolidated into this Round‑4 review rather than opening a 5th. Code‑only; CI out of scope.
| { | ||
| bool isSelected = selectableItemsView.SelectionMode != SelectionMode.None && | ||
| (selectableItemsView.SelectionMode == SelectionMode.Single | ||
| ? object.Equals(selectableItemsView.SelectedItem, templateContext.Item) |
There was a problem hiding this comment.
❌ Regression — Null data items render in the Selected visual state.
Flagged by: 3/3 reviewers
With SelectionMode="Single" and nothing selected, SelectedItem is null. For a null source element templateContext.Item is also null, so object.Equals(null, null) returns true and the blank cell is pushed into Selected on every (re)bind — even though no selection exists. This path became reachable now that ObservableItemTemplateCollection2 lets null items render instead of skipping them. (The Multiple branch is safe: SelectedItems.Contains(null) is false.)
Fix: require a non-null item before comparing, e.g.
bool isSelected = templateContext.Item is not null && selectableItemsView.SelectionMode != SelectionMode.None && (...)
| foreach (var container in FindAllContainers()) | ||
| { | ||
| var item = GetContainerItem(container); | ||
| if (item is not null) |
There was a problem hiding this comment.
null-item containers keep a stale drag index, so dragging a blank cell after a reorder moves the wrong row.
Flagged by: 3/3 reviewers (1 rated this ❌ silent data corruption)
UpdateAllContainerIndices skips containers whose item is null (GetContainerItem returns the null BindingContext), so their Tag is never refreshed after a reorder. ItemContainer_DragStarting (~L417) then takes the item is null && Tag is int index path and resolves _draggedItem from the stale index — silently dragging whatever item now occupies that old slot. Trigger: a reorderable CollectionView whose source contains null items → reorder → drag the blank cell.
The skip is understandable (a null item can't be resolved by value), but resolving to a plausible-but-wrong index is worse than failing safe. Fix: clear the tag for unresolved containers so the drag cancels — else { container.Tag = null; } — or assign the authoritative visual index via repeater.GetElementIndex(container).
| { | ||
| oldItems.Add(Items[replaceIndex + i]); | ||
| var item = e.NewItems[i]; | ||
| if (item is null) |
There was a problem hiding this comment.
Replace with a null value desyncs the wrapper from the source; the null-parity fix applied to ObservableItemTemplateCollection2 was not mirrored in this sibling class.
Flagged by: 3/3 reviewers
When e.NewItems[i] is null, the continue skips both Items[replaceIndex + i] = newItem and newItems.Add(...), leaving the old context in Items while the source now holds null at that index — breaking the exact index-alignment invariant this PR establishes elsewhere. RebuildFlatList and HandleGroupItemsAdd already keep nulls, so this branch is inconsistent. It also emits a malformed Replace (NewItems.Count < OldItems.Count) to the live ItemsRepeater/ItemsSourceView.
(Verified: the 4-arg NotifyCollectionChangedEventArgs(Replace, …) constructor does not throw on unequal counts, so the symptom is desync + a contract-violating event, not an immediate exception.)
Trigger: a grouped CollectionView whose group is an IList/ObservableCollection and an element is replaced with null (group[i] = null).
Fix: keep null parity — drop the if (item is null) continue; and always assign + add a null-backed context, matching Add/Rebuild.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 3 findings
See inline comments for details.
| { | ||
| oldItems.Add(Items[replaceIndex + i]); | ||
| var item = e.NewItems[i]; | ||
| if (item is null) |
There was a problem hiding this comment.
[major] CollectionView Shared Models — Grouped item replacement drops null values. In a grouped ObservableCollection, replacing an item with null hits this continue, leaving the old ItemTemplateContext2 in Items while raising a Replace notification with mismatched old/new item lists. Concrete scenario: a grouped CollectionView whose group contains nullable items replaces group[0] with null; CV2 keeps rendering the old item instead of the expected blank/null cell.
|
|
||
| foreach (var group in itemsSource) | ||
| { | ||
| if (group is IList groupList) |
There was a problem hiding this comment.
[major] CollectionView Windows — Grouped ScrollTo only handles groups that implement IList, but the grouped data source/display path accepts any non-string IEnumerable group. A CollectionView grouped with LINQ GroupBy/IGrouping will render, but ScrollTo(groupIndex, itemIndex) returns -1 because the target group is skipped here. Use enumerable counting/indexing fallback like GroupedItemTemplateCollection2 does instead of requiring IList.
| if (PlatformView is null || ItemsView is null) | ||
| return; | ||
|
|
||
| var selectedItem = PlatformView.SelectedItem is ItemTemplateContext2 itemPair |
There was a problem hiding this comment.
[major] CollectionView Windows Selection — Platform-to-MAUI selection unwraps every ItemTemplateContext2 without excluding group headers/footers. The header/footer template removes visual selection chrome but does not remove those contexts from the WinUI ItemsView selection model, so clicking/focusing a group header can set SelectedItem to the group object instead of ignoring the selection. Filter IsHeader/IsFooter here and in multiple-selection extraction before updating MAUI selection.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@SuthiYuvaraj — new AI review results are available based on this last commit:
0b642fa. To request a fresh review after new comments or commits, comment/review rerun.
🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix
Gate Result: ⚠️ INCONCLUSIVE
Platform: WINDOWS · Base: net11.0 · Merge base: bed36265
🩺 Base branch does not compile — the without-fix build failed. The gate's "does the test fail without the fix" check is unreliable here; this usually means main is broken or a merge-base file went missing. Investigate before trusting this gate.
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🖥️ UITest UITest |
🛠️ BUILD ERROR | ❌ FAIL — 8742s |
🔴 Without fix — 🖥️ UITest: 🛠️ BUILD ERROR · 237s
(truncated to last 15,000 chars)
\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\ItemsViewHandler2.Windows.cs(175,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.MapEmptyView(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView!>! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\ItemsViewHandler2.Windows.cs(180,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.MapEmptyViewTemplate(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView!>! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\ItemsViewHandler2.Windows.cs(185,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.MapFlowDirection(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView!>! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\ItemsViewHandler2.Windows.cs(190,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.MapIsVisible(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView!>! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\ItemsViewHandler2.Windows.cs(196,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.MapItemsLayout(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView!>! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\ItemsViewHandler2.Windows.cs(201,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.MapHeader(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView!>! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\ItemsViewHandler2.Windows.cs(206,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.MapHeaderTemplate(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView!>! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\ItemsViewHandler2.Windows.cs(211,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.MapFooter(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView!>! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\ItemsViewHandler2.Windows.cs(216,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.MapFooterTemplate(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView!>! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\ItemsViewHandler2.Windows.cs(228,26): error RS0016: Symbol 'override Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.ConnectHandler(Microsoft.UI.Xaml.Controls.ItemsView! platformView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\ItemsViewHandler2.Windows.cs(221,32): error RS0016: Symbol 'override Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.CreatePlatformView() -> Microsoft.UI.Xaml.Controls.ItemsView!' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\ItemsViewHandler2.Windows.cs(248,26): error RS0016: Symbol 'override Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.DisconnectHandler(Microsoft.UI.Xaml.Controls.ItemsView! platformView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\ItemsViewHandler2.Windows.cs(419,25): error RS0016: Symbol 'virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.UpdateItemsSource() -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\ItemsViewHandler2.Windows.cs(704,17): error RS0016: Symbol 'Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2<TItemsView>.UpdateItemsLayout() -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\CollectionViewHandler2.Windows.cs(14,9): error RS0016: Symbol 'Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.CollectionViewHandler2() -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\CollectionViewHandler2.Windows.cs(19,9): error RS0016: Symbol 'Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.CollectionViewHandler2(Microsoft.Maui.PropertyMapper? mapper = null) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\CollectionViewHandler2.Windows.cs(43,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapIsGrouped(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.GroupableItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\CollectionViewHandler2.Windows.cs(55,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapGroupHeaderTemplate(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.GroupableItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\CollectionViewHandler2.Windows.cs(60,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapGroupFooterTemplate(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.GroupableItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\CollectionViewHandler2.Windows.cs(65,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapItemSizingStrategy(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\CollectionViewHandler2.Windows.cs(71,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapItemsSource(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\CollectionViewHandler2.Windows.cs(76,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapSelectedItem(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\CollectionViewHandler2.Windows.cs(81,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapSelectedItems(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\CollectionViewHandler2.Windows.cs(93,21): error RS0016: Symbol 'static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapSelectionMode(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\CollectionViewHandler2.Windows.cs(157,26): error RS0016: Symbol 'override Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.ConnectHandler(Microsoft.UI.Xaml.Controls.ItemsView! platformView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\CollectionViewHandler2.Windows.cs(190,26): error RS0016: Symbol 'override Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.DisconnectHandler(Microsoft.UI.Xaml.Controls.ItemsView! platformView) -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\CollectionViewHandler2.Windows.cs(209,26): error RS0016: Symbol 'override Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.UpdateItemsSource() -> void' is not part of the declared public API (https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/PublicApiAnalyzers/PublicApiAnalyzers.Help.md) [D:\a\1\s\src\Controls\src\Core\Controls.Core.csproj::TargetFramework=net11.0-windows10.0.19041.0]
0 Warning(s)
50 Error(s)
Time Elapsed 00:02:32.59
🟢 With fix — 🖥️ UITest: FAIL ❌ · 8742s
(truncated to last 15,000 chars)
MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
Failed VisitAndUpdateItemsSource("Default Text","VerticalGridCode",101,11) [4 m 15 s]
Error Message:
OneTimeSetUp: OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: The system cannot find the file specified
Stack Trace:
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
at UITest.Appium.AppiumWindowsApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumWindowsApp.cs:line 11
at UITest.Appium.AppiumServerContext.CreateUIClientContext(IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumServerContext.cs:line 41
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context, Boolean reset) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 77
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 55
at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 215
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
Failed VisitAndUpdateItemsSource("Default Text","HorizontalGridCode",101,11) [4 m 15 s]
Error Message:
OneTimeSetUp: OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: The system cannot find the file specified
Stack Trace:
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
at UITest.Appium.AppiumWindowsApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumWindowsApp.cs:line 11
at UITest.Appium.AppiumServerContext.CreateUIClientContext(IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumServerContext.cs:line 41
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context, Boolean reset) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 77
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 55
at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 215
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
Failed VisitAndUpdateItemsSource("DataTemplate","VerticalListCode",19,6) [4 m 15 s]
Error Message:
OneTimeSetUp: OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: The system cannot find the file specified
Stack Trace:
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
at UITest.Appium.AppiumWindowsApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumWindowsApp.cs:line 11
at UITest.Appium.AppiumServerContext.CreateUIClientContext(IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumServerContext.cs:line 41
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context, Boolean reset) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 77
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 55
at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 215
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
Failed VisitAndUpdateItemsSource("DataTemplate","HorizontalListCode",19,6) [4 m 15 s]
Error Message:
OneTimeSetUp: OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: The system cannot find the file specified
Stack Trace:
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
at UITest.Appium.AppiumWindowsApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumWindowsApp.cs:line 11
at UITest.Appium.AppiumServerContext.CreateUIClientContext(IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumServerContext.cs:line 41
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context, Boolean reset) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 77
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 55
at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 215
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
Failed VisitAndUpdateItemsSource("DataTemplate","VerticalGridCode",19,6) [4 m 15 s]
Error Message:
OneTimeSetUp: OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: The system cannot find the file specified
Stack Trace:
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
at UITest.Appium.AppiumWindowsApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumWindowsApp.cs:line 11
at UITest.Appium.AppiumServerContext.CreateUIClientContext(IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumServerContext.cs:line 41
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context, Boolean reset) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 77
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 55
at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 215
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
Failed VisitAndUpdateItemsSource("DataTemplate","HorizontalGridCode",19,6) [4 m 15 s]
Error Message:
OneTimeSetUp: OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: The system cannot find the file specified
Stack Trace:
at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
at UITest.Appium.AppiumWindowsApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumWindowsApp.cs:line 11
at UITest.Appium.AppiumServerContext.CreateUIClientContext(IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumServerContext.cs:line 41
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context, Boolean reset) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 77
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 55
at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 215
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
>>>>> 6/22/2026 4:17:11 PM The SaveDeviceDiagnosticInfo threw an exception during VisitAndUpdateItemsSourceUITests(Windows).
Exception details: System.InvalidOperationException: Call InitialSetup before accessing the App property.
at UITest.Appium.NUnit.UITestContextBase.get_App() in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 32
at UITest.Appium.NUnit.UITestBase.SaveDeviceDiagnosticInfo(String note, Boolean storeForReattachment) in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 255
TearDown failed for test fixture Microsoft.Maui.TestCases.Tests.VisitAndUpdateItemsSourceUITests(Windows)
OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: The system cannot find the file specified
TearDown : System.InvalidOperationException : Call InitialSetup before accessing the App property.
StackTrace: at OpenQA.Selenium.WebDriver.UnpackAndThrowOnError(Response errorResponse, String commandToExecute)
at OpenQA.Selenium.WebDriver.ExecuteAsync(String driverCommandToExecute, Dictionary`2 parameters)
at OpenQA.Selenium.WebDriver.StartSession(ICapabilities capabilities)
at OpenQA.Selenium.WebDriver..ctor(ICommandExecutor executor, ICapabilities capabilities)
at OpenQA.Selenium.Appium.AppiumDriver..ctor(ICommandExecutor commandExecutor, ICapabilities appiumOptions)
at UITest.Appium.AppiumWindowsApp..ctor(Uri remoteAddress, IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumWindowsApp.cs:line 11
at UITest.Appium.AppiumServerContext.CreateUIClientContext(IConfig config) in /_/src/TestUtils/src/UITest.Appium/AppiumServerContext.cs:line 41
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context, Boolean reset) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 77
at UITest.Appium.NUnit.UITestContextBase.InitialSetup(IServerContext context) in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 55
at UITest.Appium.NUnit.UITestBase.OneTimeSetup() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 215
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
--TearDown
at UITest.Appium.NUnit.UITestContextBase.get_App() in /_/src/TestUtils/src/UITest.NUnit/UITestContextBase.cs:line 32
at UITest.Appium.NUnit.UITestBase.OneTimeTearDown() in /_/src/TestUtils/src/UITest.NUnit/UITestBase.cs:line 244
at System.RuntimeMethodHandle.InvokeMethod(ObjectHandleOnStack target, Void** arguments, ObjectHandleOnStack sig, BOOL isConstructor, ObjectHandleOnStack result)
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
NUnit Adapter 4.5.0.0: Test execution complete
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 11.0.0-preview.5.26256.105)
[xUnit.net 00:00:00.09] Discovering: Controls.TestCases.WinUI.Tests
[xUnit.net 00:00:00.26] Discovered: Controls.TestCases.WinUI.Tests
Results File: D:\a\1\s\CustomAgentLogsTmp\UITests\TestResults\UITest.trx
Total tests: 87
Failed: 87
Total time: 2.3471 Hours
Test Run Failed.
>>> TRX_RESULT_FILE: D:\a\1\s\CustomAgentLogsTmp\UITests\TestResults\UITest.trx
⚠️ Failure Details
- 🛠️ UITest without fix: build failed before tests could run
D:\a\1\s\src\Controls\src\Core\Handlers\Items2\CollectionViewHandler2.Windows.cs(12,22): error RS0016: Symbol 'Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2' is not part of the declar...
- ❌ UITest FAILED with fix (should pass)
Clicked [4 m 11 s]; IsEnabled [4 m 11 s]; IsVisible [4 m 11 s]; IsEnabled [4 m 16 s]; IsVisible [4 m 16 s]; ItemsFromViewModelShouldBeSelected [4 m 16 s]; SelectionShouldUpdateBinding [4 m 15 s]; DragAndDropBetweenLayouts [4 m 16 s]; DragEventCoordinates [4 m 16 s]; DragEvents [4 m 16 s]; DragStartEventCoordinates [4 m 16 s]; PlatformDragEventArgs [4 m 16 s]; IsEnabled [4 m 16 s]; IsVisible [4 m 16 s]; DisabledSingleTap [4 m 15 s]; DoubleTap [4 m 15 s]; DynamicallyAddedTapGesturesDontCauseMultipleTapEvents [4 m 15 s]; PointerGestureTest [4 m 15 s]; SingleTap [4 m 15 s]; Aspect [4 m 16 s]; Aspect_AspectFill [4 m 16 s]; Aspect_AspectFit [4 m 16 s]; Aspect_Center [4 m 16 s]; Aspect_Fill [4 m 16 s]; BorderColor [4 m 16 s]; BorderColor_WithBackground [4 m 16 s]; BorderWidth [4 m 16 s]; BorderWidth_WithBackground [4 m 16 s]; Clicked [4 m 16 s]; Command [4 m 16 s]; CornerRadius [4 m 16 s]; CornerRadius_WithBackground [4 m 16 s]; IsEnabled [4 m 16 s]; IsVisible [4 m 16 s]; Padding [4 m 16 s]; Padding_Add [4 m 16 s]; IsAnimationPlaying [4 m 15 s]; IsEnabled [4 m 15 s]; IsVisible [4 m 15 s]; Source_FontImageSource [4 m 15 s]; Bugzilla28570Test [4 m 15 s]; Issue35127Test [4 m 16 s]; Bugzilla41415Test [4 m 16 s]; Bugzilla44461Test [4 m 16 s]; Bugzilla49069Test [4 m 15 s]; VerifyCheckBoxUnCheckedState [4 m 16 s]; VerifyCheckBoxCheckedState [4 m 16 s]; ClearingGroupedNoCrash [4 m 15 s]; NoBindingErrors [4 m 16 s]; DynamicallyLoadCollectionView [4 m 16 s]; CollectionViewItemsSourceTypesDisplayAndDontCrash [4 m 16 s]; KeepLastItemInView [4 m 15 s]; KeepScrollOffset [4 m 15 s]; AddingGroupToUnviewedGroupedCollectionViewShouldNotCrash [4 m 16 s]; AddingItemToUnviewedCollectionViewShouldNotCrash [4 m 16 s]; EmptyViewShouldNotCrash [4 m 16 s]; CollectionShouldInvalidateOnVisibilityChange [4 m 15 s]; MeasuringEmptyScrollViewDoesNotCrash [4 m 16 s]; ScrollViewNoContentTest [4 m 16 s]; ScrollViewObjectDisposedTest [4 m 15 s]; VerifySwipeViewApperance [4 m 16 s]; VerifyTimePickerAppearance [4 m 16 s]; FontFamily [4 m 16 s]; FontFamilyLoadsDynamically [4 m 16 s]; IsEnabled [4 m 16 s]; IsVisible [4 m 16 s]; SpanTapped [4 m 16 s]; IsEnabled [4 m 16 s]; IsVisible [4 m 16 s]; ScrollToElement1Start [4 m 16 s]; ScrollToElement2Center [4 m 16 s]; ScrollToElement3End [4 m 16 s]; ScrollToY [4 m 16 s]; ScrollToYTwice [4 m 16 s]; ScrollUpAndDownWithGestures [4 m 16 s]; DecreaseStepper [4 m 16 s]; IncreaseStepper [4 m 16 s]; VisitAndUpdateItemsSource("DataTemplate","VerticalListCode",19,6) [4 m 15 s]; VisitAndUpdateItemsSource("DataTemplate","HorizontalListCode",19,6) [4 m 15 s]; VisitAndUpdateItemsSource("DataTemplate","VerticalGridCode",19,6) [4 m 15 s]; VisitAndUpdateItemsSource("DataTemplate","HorizontalGridCode",19,6) [4 m 15 s]OneTimeSetUp: OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the command. Original error: The system cannot find the file specified; OneTimeSetUp: OpenQA.Selenium.UnknownErrorException : An unknown server-side error occurred while processing the comman...
📁 Fix files reverted (11 files)
eng/cake/dotnet.cakeeng/devices/windows.cakeeng/pipelines/common/ui-tests-build-sample.ymleng/pipelines/common/ui-tests-steps.ymleng/pipelines/common/ui-tests.ymlsrc/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targetssrc/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cssrc/Controls/src/Core/Hosting/AppHostBuilderExtensions.cssrc/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xamlsrc/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txtsrc/Core/src/RuntimeFeature.cs
New files (not reverted):
src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cssrc/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.Windows.cssrc/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cssrc/Controls/src/Core/Handlers/Items2/ReorderableItemsViewHandler2.Windows.cssrc/Controls/src/Core/Handlers/Items2/Windows/GroupableUniformGridLayout.cssrc/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cssrc/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cssrc/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cssrc/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cssrc/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cssrc/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.DragDrop.cssrc/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cssrc/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cssrc/Controls/src/Core/Handlers/Items2/Windows/TemplatedItemSourceFactory2.cs
📱 UI Tests — CollectionView,DragAndDrop,Layout,ScrollView
Detected UI test categories: CollectionView,DragAndDrop,Layout,ScrollView
📋 Pre-Flight — Context & Validation
Issue: Not explicitly linked in fetched metadata - CollectionView2 Windows handler follow-up context
PR: #34600 - [WinUI][CV2] CollectionView2 Handler Implementation for Windows
Platforms Affected: Windows
Files Changed: 18 implementation, 79 test
Key Findings
- PR implements and enables the WinUI CollectionView2 handler using ItemsRepeater/ItemsView infrastructure.
- GitHub CLI is unauthenticated in this environment, so context was gathered from public REST/diff data and the local PR review branch.
- Changed implementation areas include Windows Items2 handler, grouped flattening, drag/drop, selection, layout, templates, registration, and UI-test pipeline configuration.
- Candidate exploration should focus on alternatives to the PR's current implementation for grouped ScrollTo, grouped null replace handling, duplicate drag/drop identity, realized item cleanup, and header/footer selection filtering.
Code Review Summary
Verdict: NEEDS_CHANGES
Confidence: low
Errors: 4 | Warnings: 1 | Suggestions: 0
Key code review findings:
- ❌ ItemsViewHandler2.Windows.cs: grouped ScrollTo resolution only handles IList groups even though grouped rendering supports enumerable groups.
- ❌ GroupedItemTemplateCollection2.cs: grouped replace with a null new item can leave stale flattened UI and mismatched replacement payloads.
- ❌ MauiItemsView.DragDrop.cs: grouped drag/drop locates the source row by first equality match, so duplicate value-equal items can move the wrong source item.
- ❌ ItemsViewHandler2.Windows.cs: disconnect cleanup misses realized visible item views.
⚠️ CollectionViewHandler2.Windows.cs: group header/footer contexts can flow into MAUI selection synchronization.
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #34600 | Adds default-on Windows CollectionView2 handler and associated grouping/selection/drag-drop/layout/test infrastructure. | 100 files | Original PR; prior gate could not build/run and must not be rerun here. |
🔬 Code Review — Deep Analysis
Code Review — PR #34600
Independent Assessment
What this changes: Adds a Windows CV2 CollectionView handler using WinUI ItemsView/ItemsRepeater, selection/drag-drop/grouping/layout infrastructure, feature switch registration, and extra Windows UI-test pipeline legs/snapshots.
Inferred motivation: Enable the new CollectionView2 architecture on Windows while preserving CV1 test coverage via explicit CV1 pipeline configuration.
Reconciliation with PR Narrative
Author claims: Implements WinUI CV2 handler, fixes layout/selection/grouping/scrolling/data-template behavior, enables Windows by default, and adds CV1/CV2 UI-test coverage.
Agreement/disagreement: The broad intent matches the code. However, several current-code paths still contradict the claimed grouped collection, null item, drag/drop, cleanup, and CI-readiness guarantees.
Prior Review Reconciliation
| Prior ❌ Error Finding | Source | Status | Evidence |
|---|---|---|---|
Grouped ScrollTo only handles IList groups though rendering supports enumerable groups |
MauiBot | ❌ Unresolved | ItemsViewHandler2.Windows.cs:1785 and :1899 still skip groups not implementing IList. |
Grouped replace with null new item leaves stale UI / mismatched replace payload |
MauiBot | ❌ Unresolved | GroupedItemTemplateCollection2.cs:324-325 still continues on null replacement. |
| Grouped drag/drop finds first equality match and can move wrong duplicate item | MauiBot | ❌ Unresolved | MauiItemsView.DragDrop.cs:827 still uses `ReferenceEquals(...) |
| Earlier performance/observable-collection/null-item findings | PureWeen / kubaflo | ✅ Fixed / partially obsolete | Current code has cache/try-cleanup/null-item changes; not all earlier exact line findings still apply. |
| Build Analysis / CI failures | PureWeen | ❌ Unresolved | Public checks show Build Analysis, maui-pr, and maui-pr-uitests failures. |
Blast Radius Assessment
- Runs for all instances: yes — Windows
CollectionViewis enabled by default viaRuntimeFeature.IsWindowsCollectionView2HandlerEnabledByDefault = true. - Startup impact: medium — handler registration changes app-wide Windows handler selection.
- Static/shared state: yes — feature switch and WinUI style/resource changes affect broad Windows app behavior.
CI Status
- Required-check result:
gh pr checks --requiredunavailable: GitHub CLI is unauthenticated (gh auth login, exit 4). - Public check result: failing — public commit checks show Build Analysis failure,
maui-prfailure, andmaui-pr-uitestsfailure. - Classification: undetermined / not fully classifiable without authenticated
ghand deeper AzDO access. - Action taken: confidence capped low.
Findings
❌ Error — Grouped ScrollTo silently fails for enumerable groups
src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs:1785 and :1899
The grouped rendering path accepts enumerable group sources, but grouped scroll-index resolution still requires IList.
❌ Error — Replacing a grouped item with null corrupts collection notifications
src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs:324
On replace, null new items are skipped. That leaves the old flattened item in place while newItems has fewer entries than oldItems.
❌ Error — Grouped drag/drop can move the wrong duplicate item
src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.DragDrop.cs:827
The source row is located by first equality match. For duplicate value-equal items, drag/drop may mutate the wrong item.
❌ Error — Handler disconnect does not clean realized item views
src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs:305
Disconnect only cleans the recycle pool and header/footer/empty views. Realized visible item wrappers are not enumerated/disconnected on teardown.
⚠️ Warning — Group headers/footers can sync into MAUI selection
src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.Windows.cs:312
Selection unwraps every ItemTemplateContext2.Item, including header/footer contexts.
Failure-Mode Probing
- Group source is
IGrouping<TKey,T>: rendering can work, but groupedScrollToskips it because it is notIList. - Replace grouped item with
null: old item remains in flattened list while notification claims replacement with a shorter new list. - Duplicate values in grouped drag/drop: equality search selects first equal item, not necessarily dragged item.
- Navigate away with realized cells: recycle-pool cleanup misses visible wrappers, risking retained logical children/handlers.
Verdict: NEEDS_CHANGES
Confidence: low
Summary: The PR is a broad default-on Windows handler replacement with unresolved prior ❌ findings and additional lifecycle/selection concerns. CI is also red/undetermined under the available unauthenticated tooling, so this is not ready to merge.
🛠️ Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix | Enumerate Renderable Groups During ScrollTo | src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs | Targeted Windows UI test did not run; NETSDK1005 missing net10.0-windows10.0.19041.0 assets. | |
| 2 | try-fix | Null Context Replace Pairing | src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs | Targeted Windows UI test did not run; NETSDK1005 missing net10.0-windows10.0.19041.0 assets. | |
| 3 | try-fix | Capture Group-Local Drag Source | src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.DragDrop.cs | Targeted Windows UI test did not run; NETSDK1005 missing net10.0-windows10.0.19041.0 assets. | |
| 4 | try-fix | Detach Realized ElementWrappers on Disconnect | src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs |
Targeted Windows UI test did not run; NETSDK1005 missing net10.0-windows10.0.19041.0 assets. | |
| 5 | try-fix | Structural Selection Filtering | src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.Windows.cs src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs |
Targeted Windows UI test did not run; NETSDK1005 missing net10.0-windows10.0.19041.0 assets. | |
| 6 | try-fix | Grouped Entry Identity Keys | src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.Windows.cs src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs |
Targeted Windows UI test did not run; NETSDK1005 missing net10.0-windows10.0.19041.0 assets. | |
| PR | PR #34600 | Default-on Windows CollectionView2 handler with ItemsRepeater/ItemsView infrastructure. | 100 files | Original PR; prior gate could not build/run and was not rerun. |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 2 | No | NO NEW IDEAS |
| claude-opus-4.7 | 2 | Yes | Stable per-entry identity layer keyed by group reference, item index, and kind; run as candidate 6. |
| gpt-5.3-codex | 2 | Yes | Structurally non-selectable group header/footer contexts; run as candidate 5. |
| gpt-5.5 | 2 | No | NO NEW IDEAS |
Exhausted: Yes
Selected Fix: None — every alternative candidate was blocked before tests executed by the same missing Windows target-assets environment issue, so no candidate is demonstrably better than PR #34600's current fix.
Attempt Narratives
try-fix-1: Enumerate Renderable Groups During ScrollTo
Result: Blocked
Approach description:
Approach: Enumerate Renderable Groups During ScrollTo
Change grouped ScrollTo index resolution to treat any non-string IEnumerable group as renderable, matching GroupedItemTemplateCollection2 flattening. Use IList/ICollection fast paths when available, but fall back to enumerating a group only during explicit ScrollTo resolution.
Different from existing fix: The PR's current Windows Items2 grouped ScrollTo path resolves only IList groups. This candidate keeps the same handler location but changes the algorithm to mirror the flattened rendered source contract (IEnumerable groups), without touching drag/drop, cleanup, or grouped collection replacement logic.
Test results and analysis:
Analysis
Result: Blocked
What happened: The targeted Windows CollectionView HostApp command started, validated prerequisites, then failed during dotnet build before tests could run. The error was NETSDK1005: �rtifacts\obj\Controls.TestCases.HostApp\project.assets.json does not contain target
et10.0-windows10.0.19041.0.
Why it was blocked: The failure occurred before executing tests and does not identify a compile error in the candidate diff. Per invocation constraints, the Windows UI test environment could not build/run the provided command, so this attempt is recorded as Blocked rather than Pass.
Self-review: 0 findings — clean; no concrete violations found against logic/correctness, CollectionView grouped data correctness, Windows null safety, or hot-path allocation guidance for this explicit ScrollTo path.
Insights: Enumerating renderable groups in grouped ScrollTo aligns index resolution with GroupedItemTemplateCollection2 rendering, while preserving IList/ICollection fast paths for list-backed groups.
Diff:
diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs
index 8f0640c723..56058533de 100644
--- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs
+++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs
@@ -1782,38 +1782,30 @@ public abstract class ItemsViewHandler2<TItemsView> : ViewHandler<TItemsView, WI
foreach (var group in itemsSource)
{
- if (group is IList groupList)
+ if (!TryGetRenderableGroupItems(group, out var groupItems))
{
- if (currentGroupIndex == groupIndex)
- {
- // Found the target group
- if (hasGroupHeader)
- {
- flatIndex++; // Skip group header
- }
-
- // Check if itemIndex is within bounds of this group
- if (itemIndex < 0 || itemIndex >= groupList.Count)
- {
- return -1;
- }
+ currentGroupIndex++;
+ continue;
+ }
- // Return the calculated flat index
- return flatIndex + itemIndex;
+ if (currentGroupIndex == groupIndex)
+ {
+ if (itemIndex < 0)
+ {
+ return -1;
}
- // Count items in this group to move to next group
if (hasGroupHeader)
{
flatIndex++;
}
- flatIndex += groupList.Count;
- if (hasGroupFooter)
- {
- flatIndex++;
- }
+
+ return TryGetGroupItemOffset(groupItems, itemIndex, out var itemOffset)
+ ? flatIndex + itemOffset
+ : -1;
}
+ flatIndex += GetFlatGroupLength(groupItems, hasGroupHeader, hasGroupFooter);
currentGroupIndex++;
}
@@ -1821,6 +1813,130 @@ public abstract class ItemsViewHandler2<TItemsView> : ViewHandler<TItemsView, WI
return -1;
}
+ static bool TryGetRenderableGroupItems(object? group, out IEnumerable groupItems)
+ {
+ if (group is not string && group is IEnumerable items)
+ {
+ groupItems = items;
+ return true;
+ }
+
+ groupItems = Array.Empty<object>();
+ return false;
+ }
+
+ static int GetFlatGroupLength(IEnumerable groupItems, bool hasGroupHeader, bool hasGroupFooter)
+ {
+ int length = 0;
+ if (hasGroupHeader)
+ {
+ length++;
+ }
+
+ length += GetGroupItemCount(groupItems);
+
+ if (hasGroupFooter)
+ {
+ length++;
+ }
+
+ return length;
+ }
+
+ static int GetGroupItemCount(IEnumerable groupItems)
+ {
+ if (groupItems is ICollection collection)
+ {
+ return collection.Count;
+ }
+
+ int count = 0;
+ foreach (var unused in groupItems)
+ {
+ count++;
+ }
+
+ return count;
+ }
+
+ static bool HasAnyGroupItems(IEnumerable groupItems)
+ {
+ if (groupItems is ICollection collection)
+ {
+ return collection.Count > 0;
+ }
+
+ foreach (var unused in groupItems)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ static bool TryGetGroupItemOffset(IEnumerable groupItems, int itemIndex, out int itemOffset)
+ {
+ if (groupItems is IList list)
+ {
+ if (itemIndex < list.Count)
+ {
+ itemOffset = itemIndex;
+ return true;
+ }
+
+ itemOffset = -1;
+ return false;
+ }
+
+ int index = 0;
+ foreach (var unused in groupItems)
+ {
+ if (index == itemIndex)
+ {
+ itemOffset = index;
+ return true;
+ }
+
+ index++;
+ }
+
+ itemOffset = -1;
+ return false;
+ }
+
+ static bool TryFindItemOffsetInGroup(IEnumerable groupItems, object item, out int itemOffset)
+ {
+ if (groupItems is IList list)
+ {
+ for (int i = 0; i < list.Count; i++)
+ {
+ if (Equals(list[i], item))
+ {
+ itemOffset = i;
+ return true;
+ }
+ }
+
+ itemOffset = -1;
+ return false;
+ }
+
+ int index = 0;
+ foreach (var groupItem in groupItems)
+ {
+ if (Equals(groupItem, item))
+ {
+ itemOffset = index;
+ return true;
+ }
+
+ index++;
+ }
+
+ itemOffset = -1;
+ return false;
+ }
+
/// <summary>
/// Finds the index of an item in the collection view by searching through the flattened list.
/// Used for non-grouped ScrollToMode.Element requests.
@@ -1889,74 +2005,34 @@ public abstract class ItemsViewHandler2<TItemsView> : ViewHandler<TItemsView, WI
var hasGroupHeader = groupableItemsView.GroupHeaderTemplate is not null;
var hasGroupFooter = groupableItemsView.GroupFooterTemplate is not null;
- // Find the target group and its items by matching the group object
- IList? targetGroupItems = null;
- int flatIndexOfGroup = 0;
int currentFlatIndex = 0;
foreach (var g in itemsSource)
{
- if (g is not IList groupList)
+ if (!TryGetRenderableGroupItems(g, out var groupItems))
{
continue;
}
if (Equals(g, group))
{
- targetGroupItems = groupList;
- flatIndexOfGroup = currentFlatIndex;
- break;
- }
-
- // Advance past this group's entries in the flat list
- if (hasGroupHeader)
- {
- currentFlatIndex++;
- }
-
- currentFlatIndex += groupList.Count;
-
- if (hasGroupFooter)
- {
- currentFlatIndex++;
- }
- }
-
- if (targetGroupItems is null)
- {
- return -1;
- }
+ if (item is null)
+ {
+ if (hasGroupHeader)
+ {
+ return currentFlatIndex;
+ }
- // If item is null, scroll to the group header (if it exists)
- if (item is null)
- {
- if (hasGroupHeader)
- {
- return flatIndexOfGroup;
- }
+ return HasAnyGroupItems(groupItems) ? currentFlatIndex : -1;
+ }
- // No header template — scroll to the first item in the group instead
- if (targetGroupItems.Count > 0)
- {
- return flatIndexOfGroup;
+ int itemStartIndex = hasGroupHeader ? currentFlatIndex + 1 : currentFlatIndex;
+ return TryFindItemOffsetInGroup(groupItems, item, out var itemOffset)
+ ? itemStartIndex + itemOffset
+ : -1;
}
- return -1;
- }
-
- // Find the item within the target group
- int itemStartIndex = flatIndexOfGroup;
- if (hasGroupHeader)
- {
- itemStartIndex++;
- }
-
- for (int i = 0; i < targetGroupItems.Count; i++)
- {
- if (Equals(targetGroupItems[i], item))
- {
- return itemStartIndex + i;
- }
+ currentFlatIndex += GetFlatGroupLength(groupItems, hasGroupHeader, hasGroupFooter);
}
return -1;try-fix-2: Null Context Replace Pairing
Result: Blocked
Approach description:
Approach: Null Context Replace Pairing
Update grouped item replacement so a null replacement value is represented as a valid ItemTemplateContext2 whose Item is null, instead of skipping it. The replace path will require equal old/new range counts, update every flattened slot in the range, and emit a Replace notification with old and new payload lists of identical length.
Different from existing fix: PR #34600's current replace code skips null new items, producing mismatched notification payloads and stale flattened entries. This candidate treats null as data and preserves one old/new context pair per replaced index. It is also different from candidate 1, which focused on grouped ScrollTo resolution.
Test results and analysis:
Analysis
Result: Blocked
What happened: The targeted Windows UI test command started the HostApp build but failed before tests ran with NETSDK1005: project.assets.json does not have a target for
et10.0-windows10.0.19041.0.
Why it was blocked: This matches the prior environment/build setup blocker in the prompt. The candidate code was not exercised, so this attempt cannot be classified as Pass or logic Fail.
Self-review: 0 findings — clean; no findings recorded.
Insights: Treating null as a valid MAUI data item keeps grouped Replace payloads range-scoped and count-consistent: every old flattened context is replaced with exactly one new context, including a context whose Item is null.
Diff:
diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs
index 1a28430592..76c0794eef 100644
--- a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs
+++ b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs
@@ -48,7 +48,7 @@ internal class GroupedItemTemplateCollection2 : ObservableCollection<ItemTemplat
}
}
- ItemTemplateContext2 CreateItemContext(object item) =>
+ ItemTemplateContext2 CreateItemContext(object? item) =>
new(_itemTemplate, item, _container, mauiContext: _mauiContext);
ItemTemplateContext2? CreateHeaderContext(object group) =>
@@ -307,24 +307,29 @@ internal class GroupedItemTemplateCollection2 : ObservableCollection<ItemTemplat
if (e.NewItems is null || e.OldItems is null)
return;
- int replaceIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : 0);
- if (replaceIndex < 0 || replaceIndex + e.NewItems.Count - 1 >= Items.Count)
+ int replaceCount = e.NewItems.Count;
+ if (replaceCount != e.OldItems.Count)
+ {
+ ResetWithoutResubscribe();
+ return;
+ }
+
+ int replaceIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : e.OldStartingIndex >= 0 ? e.OldStartingIndex : 0);
+ if (replaceIndex < 0 || replaceIndex + replaceCount > Items.Count)
{
ResetWithoutResubscribe();
return;
}
- var oldItems = new List<ItemTemplateContext2>(e.NewItems.Count);
- var newItems = new List<ItemTemplateContext2>(e.NewItems.Count);
+
+ var oldItems = new List<ItemTemplateContext2>(replaceCount);
+ var newItems = new List<ItemTemplateContext2>(replaceCount);
_suppressNotifications = true;
- for (int i = 0; i < e.NewItems.Count; i++)
+ for (int i = 0; i < replaceCount; i++)
{
oldItems.Add(Items[replaceIndex + i]);
- var item = e.NewItems[i];
- if (item is null)
- continue;
- var newItem = CreateItemContext(e.NewItems[i]!);
+ var newItem = CreateItemContext(e.NewItems[i]);
newItems.Add(newItem);
Items[replaceIndex + i] = newItem;
}try-fix-3: Capture Group-Local Drag Source
Result: Blocked
Approach description:
Approach: Capture Group-Local Drag Source
Capture the authoritative ItemsRepeater flat index at DragStarting, immediately map it to the mutable source group and group-local item index, and use that stored group/index during grouped drop instead of rediscovering the source row by ReferenceEquals/Equals search.
This preserves the dragged row for value-equal duplicates because the grouped move is anchored to the visual container index from drag start, not the first equal item in the group.
Different from existing fix: PR #34600's current code stores only _draggedSourceIndex and PerformGroupedReorder still searches groups for the first ReferenceEquals/Equals match. Candidate 3 stores the source group reference and group-local index at drag start and makes grouped reorder use that captured position.
Test results and analysis:
Analysis
Result: Blocked
What happened: The candidate was implemented and the required Windows DragAndDrop test command was run. The HostApp build failed before tests executed with NETSDK1005: project.assets.json does not have a target for net10.0-windows10.0.19041.0.
Why it blocked: This matches the known Windows UI test environment target-assets blocker from prior candidates, so the fix logic could not be validated empirically in this environment.
Self-review: 0 findings — clean; no critical/major issues found against the final diff.
Insights: Capturing the source group and group-local item index at DragStarting avoids grouped source rediscovery by first equality match and should preserve the dragged duplicate row when tests can run.
Diff:
diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.DragDrop.cs b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.DragDrop.cs
index 469ec2e175..4e4edf1c75 100644
--- a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.DragDrop.cs
+++ b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.DragDrop.cs
@@ -28,6 +28,9 @@ internal partial class MauiItemsView
// Used by PerformReorder to disambiguate value-equal duplicates (record structs,
// boxed primitives, repeated entries) where IndexOfItem would return the first match.
int _draggedSourceIndex = -1;
+ IList? _draggedSourceGroup;
+ int _draggedSourceGroupIndex = -1;
+ int _draggedSourceItemIndex = -1;
ItemContainer? _sourceContainer;
int _insertionIndex = -1;
bool _insertAfter;
@@ -430,7 +433,8 @@ internal partial class MauiItemsView
}
_draggedItem = item;
- _draggedSourceIndex = GetContainerIndex(itemContainer);
+ _draggedSourceIndex = GetRepeaterElementIndex(itemContainer);
+ CaptureGroupedDragSource(_draggedSourceIndex);
_sourceContainer = itemContainer;
args.Data.Properties.Add("DragSource", "MauiItemsView");
@@ -807,42 +811,20 @@ internal partial class MauiItemsView
bool hasHeaders = groupableView.GroupHeaderTemplate is not null;
bool hasFooters = groupableView.GroupFooterTemplate is not null;
- // Find which group the dragged item belongs to.
- // Groups may be IEnumerable-only (e.g., IGrouping<K,V>), so enumerate rather
- // than requiring IList for the search. IList is still required for mutation.
- int sourceGroupIndex = -1;
- int sourceItemIndex = -1;
- IList? sourceGroup = null;
+ int sourceGroupIndex = _draggedSourceGroupIndex;
+ int sourceItemIndex = _draggedSourceItemIndex;
+ IList? sourceGroup = _draggedSourceGroup;
- for (int g = 0; g < groupsList.Count; g++)
+ if (sourceGroupIndex < 0 ||
+ sourceItemIndex < 0 ||
+ sourceGroup is null ||
+ sourceItemIndex >= sourceGroup.Count)
{
- if (groupsList[g] is not IEnumerable groupItems)
- {
- continue;
- }
-
- int i = 0;
- foreach (var groupItem in groupItems)
- {
- if (ReferenceEquals(groupItem, _draggedItem) || Equals(groupItem, _draggedItem))
- {
- sourceGroupIndex = g;
- sourceItemIndex = i;
- sourceGroup = groupsList[g] as IList;
- break;
- }
-
- i++;
- }
-
- if (sourceGroupIndex >= 0)
- {
- break;
- }
+ return false;
}
- // sourceGroup being null means the group is not mutable — reorder not possible.
- if (sourceGroupIndex < 0 || sourceItemIndex < 0 || sourceGroup is null)
+ var sourceItem = sourceGroup[sourceItemIndex];
+ if (!ReferenceEquals(sourceItem, _draggedItem) && !Equals(sourceItem, _draggedItem))
{
return false;
}
@@ -860,10 +842,7 @@ internal partial class MauiItemsView
continue;
}
- // Use ICollection.Count when available (O(1)); otherwise enumerate (O(n)).
- int groupItemCount = groupsList[g] is ICollection coll
- ? coll.Count
- : groupItems.Cast<object>().Count();
+ int groupItemCount = GetEnumerableCount(groupsList[g], groupItems);
int groupStart = flatPos;
@@ -904,9 +883,7 @@ internal partial class MauiItemsView
{
if (groupsList[g] is IEnumerable groupItems)
{
- int groupItemCount = groupsList[g] is ICollection coll
- ? coll.Count
- : groupItems.Cast<object>().Count();
+ int groupItemCount = GetEnumerableCount(groupsList[g], groupItems);
targetGroupIndex = g;
targetItemIndex = groupItemCount;
@@ -1107,6 +1084,100 @@ internal partial class MauiItemsView
return FindAllContainers().FirstOrDefault(c => GetContainerIndex(c) == index);
}
+ int GetRepeaterElementIndex(ItemContainer itemContainer)
+ {
+ var repeater = ItemsRepeaterControl;
+ if (repeater is not null)
+ {
+ int repeaterIndex = repeater.GetElementIndex(itemContainer);
+ if (repeaterIndex >= 0)
+ {
+ return repeaterIndex;
+ }
+ }
+
+ if (itemContainer.Tag is int tagIndex)
+ {
+ return tagIndex;
+ }
+
+ return -1;
+ }
+
+ void CaptureGroupedDragSource(int flatIndex)
+ {
+ _draggedSourceGroup = null;
+ _draggedSourceGroupIndex = -1;
+ _draggedSourceItemIndex = -1;
+
+ if (flatIndex < 0 ||
+ _mauiVirtualView is not GroupableItemsView { IsGrouped: true } groupableView ||
+ _mauiVirtualView.ItemsSource is not IList groupsList)
+ {
+ return;
+ }
+
+ bool hasHeaders = groupableView.GroupHeaderTemplate is not null;
+ bool hasFooters = groupableView.GroupFooterTemplate is not null;
+ int flatPos = 0;
+
+ for (int g = 0; g < groupsList.Count; g++)
+ {
+ object? group = groupsList[g];
+ if (group is not IEnumerable groupItems)
+ {
+ continue;
+ }
+
+ if (hasHeaders)
+ {
+ if (flatIndex == flatPos)
+ {
+ return;
+ }
+
+ flatPos++;
+ }
+
+ int groupItemCount = GetEnumerableCount(group, groupItems);
+ if (flatIndex >= flatPos && flatIndex < flatPos + groupItemCount)
+ {
+ _draggedSourceGroup = group as IList;
+ _draggedSourceGroupIndex = g;
+ _draggedSourceItemIndex = flatIndex - flatPos;
+ return;
+ }
+
+ flatPos += groupItemCount;
+
+ if (hasFooters)
+ {
+ if (flatIndex == flatPos)
+ {
+ return;
+ }
+
+ flatPos++;
+ }
+ }
+ }
+
+ static int GetEnumerableCount(object? source, IEnumerable enumerable)
+ {
+ if (source is ICollection collection)
+ {
+ return collection.Count;
+ }
+
+ int count = 0;
+ foreach (var _ in enumerable)
+ {
+ count++;
+ }
+
+ return count;
+ }
+
/// <summary>
/// Finds the realized container whose binding context equals <paramref name="targetItem"/>
/// by identity. Unlike <see cref="FindContainerByIndex"/>, this is safe to call from
@@ -1306,6 +1377,9 @@ internal partial class MauiItemsView
_draggedItem = null;
_draggedSourceIndex = -1;
+ _draggedSourceGroup = null;
+ _draggedSourceGroupIndex = -1;
+ _draggedSourceItemIndex = -1;
_insertionIndex = -1;
_insertAfter = false;
StopAutoScroll();try-fix-4: Detach Realized ElementWrappers on Disconnect
Result: Blocked
Approach description:
Approach: Detach Realized ElementWrappers on Disconnect
Add a full-teardown path to the Windows CV2 item factory that walks the currently realized WinUI ItemContainer tree during handler disconnect and detaches each ElementWrapper's MAUI view: unwire lifecycle observers, disconnect handlers, clear binding/data contexts, remove the logical child, clear wrapper content, and sever the ItemContainer child reference.
Different from existing fix: PR #34600's current disconnect path cleans collection subscriptions, the recycle pool, and header/footer/empty views, but avoids CleanUpCollectionViewSource() and therefore does not explicitly detach realized visible item wrappers. This candidate keeps that avoidance of CollectionViewSource.Source mutation, but adds a separate idempotent realized-element teardown path focused only on lifecycle cleanup.
Test results and analysis:
Analysis
Result: Blocked
What happened: The targeted Windows CollectionView host-app test command started successfully but the build failed before tests ran.
Why it was blocked: The environment hit the same known NETSDK1005 blocker as prior candidates: project.assets.json does not contain the
et10.0-windows10.0.19041.0 target for Controls.TestCases.HostApp.csproj. Because the test environment cannot build the requested Windows UI test target, this candidate could not be empirically validated.
Self-review: 0 findings — clean, no findings.
Insights: The candidate isolates realized visible item cleanup from CollectionViewSource teardown by walking realized ItemContainer wrappers and severing MAUI logical child, handler, data context, content, and platform child references during DisconnectHandler.
Diff:
diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs
index 8f0640c723..4cbab0abfa 100644
--- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs
+++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs
@@ -302,6 +302,7 @@ public abstract class ItemsViewHandler2<TItemsView> : ViewHandler<TItemsView, WI
}
+ _itemFactory?.CleanUpRealizedElements(platformView);
_itemFactory?.CleanUp();
_itemFactory = null;
diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs
index 97408f2374..836316defe 100644
--- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs
+++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs
@@ -244,23 +244,41 @@ internal partial class ItemFactory(ItemsView view) : IElementFactory
{
foreach (var container in kvp.Value)
{
- var wrapper = container?.Child as ElementWrapper;
- var wrapperView = wrapper?.VirtualView as View;
- if (wrapperView is not null)
- {
- wrapperView.BindingContext = null;
- _view.RemoveLogicalChild(wrapperView);
- }
-
- if (wrapper is not null)
- {
- wrapper.DataContext = null;
- }
+ CleanUpContainer(container);
}
}
_recyclePool.Clear();
}
+
+ /// <summary>
+ /// Detaches realized item containers still present in the platform visual tree.
+ /// </summary>
+ internal void CleanUpRealizedElements(Microsoft.UI.Xaml.Controls.ItemsView platformView)
+ {
+ var realizedContainers = new List<ItemContainer>();
+ foreach (var container in platformView.GetChildren<ItemContainer>())
+ {
+ if (container is not null)
+ {
+ realizedContainers.Add(container);
+ }
+ }
+
+ for (int i = 0; i < realizedContainers.Count; i++)
+ {
+ CleanUpContainer(realizedContainers[i]);
+ }
+ }
+
+ void CleanUpContainer(ItemContainer? container)
+ {
+ if (container?.Child is ElementWrapper wrapper)
+ {
+ wrapper.CleanUpVirtualView(_view);
+ container.Child = null;
+ }
+ }
}
/// <summary>
@@ -294,6 +312,27 @@ internal partial class ElementWrapper : ContentControl
}
}
+ /// <summary>
+ /// Fully detaches the hosted MAUI view during handler teardown.
+ /// </summary>
+ internal void CleanUpVirtualView(ItemsView itemsView)
+ {
+ Loaded -= OnLoadedCreatePlatformView;
+ UnwireContentSizeObserver();
+
+ if (VirtualView is View mauiView)
+ {
+ mauiView.BindingContext = null;
+ mauiView.DisconnectHandlers();
+ itemsView.RemoveLogicalChild(mauiView);
+ }
+
+ Content = null;
+ DataContext = null;
+ VirtualView = null;
+ IsHeaderOrFooter = false;
+ }
+
public ElementWrapper(IMauiContext context)
{
_context = context;try-fix-5: Structural Selection Filtering
Result: Blocked
Approach description:
Approach: Structural Selection Filtering
Make ItemTemplateContext2 expose an explicit IsSelectable role and route every Windows Items2 selection synchronization path through that role. Header/footer contexts remain renderable with their existing templates and binding contexts, but they are ignored when platform selection flows back to MAUI and skipped when MAUI selection is projected to WinUI indices.
Different from existing fix: PR #34600 already carries header/footer metadata for display and container styling. This candidate uses that metadata at the selection boundary instead of changing ScrollTo, null replacement, drag/drop source identity, or disconnect cleanup. It structurally filters selection synchronization without treating null item values as header/footer.
Test results and analysis:
Analysis
Result: Blocked
What happened: The targeted Windows CollectionView host-app command started but failed during build before tests ran. The build reported NETSDK1005 because �rtifacts\obj\Controls.TestCases.HostApp\project.assets.json does not contain the
et10.0-windows10.0.19041.0 target.
Why it was blocked: This is the same missing-assets condition reported for prior candidates, so candidate behavior could not be empirically validated in this environment.
Self-review: 0 findings — clean; no critical/major issues found in the final diff.
Insights: The structural selection filter remains a viable candidate to test once the Windows HostApp restore/build assets include the required target framework.
Diff:
diff --git a/src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.Windows.cs
index 8e4b49e2df..f28144d5a8 100644
--- a/src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.Windows.cs
+++ b/src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.Windows.cs
@@ -309,9 +309,11 @@ public partial class CollectionViewHandler2 : ReorderableItemsViewHandler2<Reord
if (PlatformView is null || ItemsView is null)
return;
- var selectedItem = PlatformView.SelectedItem is ItemTemplateContext2 itemPair
- ? itemPair.Item
- : PlatformView.SelectedItem;
+ if (!TryGetSelectableItem(PlatformView.SelectedItem, out var selectedItem))
+ {
+ UpdatePlatformSelection();
+ return;
+ }
ItemsView.SelectionChanged -= VirtualSelectionChanged;
ItemsView.SelectedItem = selectedItem;
@@ -393,9 +395,10 @@ public partial class CollectionViewHandler2 : ReorderableItemsViewHandler2<Reord
var result = new HashSet<object>();
foreach (var item in PlatformView.SelectedItems)
{
- var selectedItem = item is ItemTemplateContext2 itc ? itc.Item : item;
- if (selectedItem is not null)
- result.Add(selectedItem);
+ if (TryGetSelectableItem(item, out var selectedItem))
+ {
+ result.Add(selectedItem!);
+ }
}
return result;
}
@@ -452,11 +455,7 @@ public partial class CollectionViewHandler2 : ReorderableItemsViewHandler2<Reord
int index = 0;
foreach (var nativeItem in itemList)
{
- if (nativeItem is ItemTemplateContext2 itemPair && ItemsView.SelectedItems.Contains(itemPair.Item))
- {
- PlatformView.Select(index);
- }
- else if (ItemsView.SelectedItems.Contains(nativeItem))
+ if (TryGetSelectableItem(nativeItem, out var item) && ItemsView.SelectedItems.Contains(item))
{
PlatformView.Select(index);
}
@@ -482,8 +481,7 @@ public partial class CollectionViewHandler2 : ReorderableItemsViewHandler2<Reord
int index = 0;
foreach (var nativeItem in itemList)
{
- var actualItem = nativeItem is ItemTemplateContext2 itc ? itc.Item : nativeItem;
- if (object.Equals(actualItem, targetItem))
+ if (TryGetSelectableItem(nativeItem, out var actualItem) && object.Equals(actualItem, targetItem))
{
return index;
}
@@ -491,6 +489,18 @@ public partial class CollectionViewHandler2 : ReorderableItemsViewHandler2<Reord
}
return -1;
}
+
+ static bool TryGetSelectableItem(object? nativeItem, out object? item)
+ {
+ if (nativeItem is ItemTemplateContext2 context)
+ {
+ item = context.Item;
+ return context.IsSelectable;
+ }
+
+ item = nativeItem;
+ return true;
+ }
}
/// <summary>
diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs
index 97408f2374..58ad8ff85c 100644
--- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs
+++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs
@@ -119,7 +119,8 @@ internal partial class ItemFactory(ItemsView view) : IElementFactory
if (view is VisualElement visualElement &&
_view is SelectableItemsView selectableItemsView)
{
- bool isSelected = selectableItemsView.SelectionMode != SelectionMode.None &&
+ bool isSelected = templateContext.IsSelectable &&
+ selectableItemsView.SelectionMode != SelectionMode.None &&
(selectableItemsView.SelectionMode == SelectionMode.Single
? object.Equals(selectableItemsView.SelectedItem, templateContext.Item)
: selectableItemsView.SelectedItems.Contains(templateContext.Item));
diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs
index cd2c2837ca..07fafa2d26 100644
--- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs
+++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs
@@ -37,6 +37,9 @@ internal class ItemTemplateContext2
/// <summary>Whether this context represents a group footer.</summary>
public bool IsFooter { get; }
+ /// <summary>Whether this context represents a selectable data item.</summary>
+ public bool IsSelectable => !IsHeader && !IsFooter;
+
public ItemTemplateContext2(DataTemplate mauiDataTemplate, object? item, BindableObject container,
double? height = null, double? width = null, Thickness? itemSpacing = null,
bool isHeader = false, bool isFooter = false, IMauiContext? mauiContext = null)try-fix-6: Grouped Entry Identity Keys
Result: Blocked
Approach description:
Approach: Grouped Entry Identity Keys
Add a small grouped-entry identity layer to ItemTemplateContext2 and GroupedItemTemplateCollection2: each flattened grouped entry carries its group reference, item index, and structural kind (item/header/footer). Grouped collection changes update those identity fields, and grouped ScrollTo resolves against the flattened entry contexts instead of re-walking the raw source with Equals.
This is intentionally surgical: it does not refactor the handler pipeline or selection model. It preserves null data items as valid item contexts and keeps header/footer identity structural.
Different from existing fix: PR #34600's current code and candidates 1-5 patch individual raw-equality paths (enumerable ScrollTo, null replacement, drag source capture, disconnect cleanup, header/footer filtering). This candidate introduces explicit per-entry grouped identity metadata and routes grouped ScrollTo through that metadata rather than raw group/item equality.
Test results and analysis:
Analysis
Result: Blocked
What happened: The candidate was implemented and the required Windows CollectionView test command was run. The HostApp build stopped before compiling the changed handler code with NETSDK1005: project.assets.json does not contain
et10.0-windows10.0.19041.0 for Controls.TestCases.HostApp.
Why it was blocked: This matches the prior attempts' environment/setup blocker. The test command could not build/run the Windows UI test host, so this candidate could not be empirically validated.
Self-review: 0 findings — clean; no critical/major issues found in the final diff during inline review.
Insights: The identity-key abstraction can be kept localized to grouped ItemTemplateContext2 entries and GroupedItemTemplateCollection2, but fully covering drag and selection duplicates would require additional source-context plumbing and may be too broad for a single surgical attempt.
Diff:
diff --git a/src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.Windows.cs
index 8e4b49e2df..a8db913e35 100644
--- a/src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.Windows.cs
+++ b/src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.Windows.cs
@@ -310,7 +310,7 @@ public partial class CollectionViewHandler2 : ReorderableItemsViewHandler2<Reord
return;
var selectedItem = PlatformView.SelectedItem is ItemTemplateContext2 itemPair
- ? itemPair.Item
+ ? itemPair.EntryKind == ItemTemplateContextKind2.Item ? itemPair.Item : null
: PlatformView.SelectedItem;
ItemsView.SelectionChanged -= VirtualSelectionChanged;
@@ -393,9 +393,17 @@ public partial class CollectionViewHandler2 : ReorderableItemsViewHandler2<Reord
var result = new HashSet<object>();
foreach (var item in PlatformView.SelectedItems)
{
- var selectedItem = item is ItemTemplateContext2 itc ? itc.Item : item;
- if (selectedItem is not null)
- result.Add(selectedItem);
+ if (item is ItemTemplateContext2 itc)
+ {
+ if (itc.EntryKind == ItemTemplateContextKind2.Item && itc.Item is not null)
+ {
+ result.Add(itc.Item);
+ }
+ }
+ else if (item is not null)
+ {
+ result.Add(item);
+ }
}
return result;
}
@@ -452,7 +460,9 @@ public partial class CollectionViewHandler2 : ReorderableItemsViewHandler2<Reord
int index = 0;
foreach (var nativeItem in itemList)
{
- if (nativeItem is ItemTemplateContext2 itemPair && ItemsView.SelectedItems.Contains(itemPair.Item))
+ if (nativeItem is ItemTemplateContext2 itemPair &&
+ itemPair.EntryKind == ItemTemplateContextKind2.Item &&
+ ItemsView.SelectedItems.Contains(itemPair.Item))
{
PlatformView.Select(index);
}
@@ -480,16 +490,40 @@ public partial class CollectionViewHandler2 : ReorderableItemsViewHandler2<Reord
static int FindItemIndexInSource(ICollectionView itemList, object targetItem)
{
int index = 0;
+ int valueEqualFallback = -1;
foreach (var nativeItem in itemList)
{
- var actualItem = nativeItem is ItemTemplateContext2 itc ? itc.Item : nativeItem;
- if (object.Equals(actualItem, targetItem))
+ if (nativeItem is ItemTemplateContext2 itc)
+ {
+ if (itc.EntryKind != ItemTemplateContextKind2.Item)
+ {
+ index++;
+ continue;
+ }
+
+ if (ReferenceEquals(itc.Item, targetItem))
+ {
+ return index;
+ }
+
+ if (valueEqualFallback < 0 && object.Equals(itc.Item, targetItem))
+ {
+ valueEqualFallback = index;
+ }
+ }
+ else if (ReferenceEquals(nativeItem, targetItem))
{
return index;
}
+ else if (valueEqualFallback < 0 && object.Equals(nativeItem, targetItem))
+ {
+ valueEqualFallback = index;
+ }
+
index++;
}
- return -1;
+
+ return valueEqualFallback;
}
}
diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs
index 8f0640c723..40db60548d 100644
--- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs
+++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs
@@ -1875,6 +1875,11 @@ public abstract class ItemsViewHandler2<TItemsView> : ViewHandler<TItemsView, WI
return -1;
}
+ if (_collectionViewSource.Source is GroupedItemTemplateCollection2 groupedCollection)
+ {
+ return groupedCollection.FindGroupedEntryIndex(item, group);
+ }
+
if (ItemsView is not GroupableItemsView groupableItemsView)
{
return -1;
@@ -1901,7 +1906,7 @@ public abstract class ItemsViewHandler2<TItemsView> : ViewHandler<TItemsView, WI
continue;
}
- if (Equals(g, group))
+ if (ReferenceEquals(g, group))
{
targetGroupItems = groupList;
flatIndexOfGroup = currentFlatIndex;
diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs
index 1a28430592..58f3d9c79b 100644
--- a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs
+++ b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
-using System.Linq;
namespace Microsoft.Maui.Controls.Handlers.Items2;
/// <summary>
@@ -48,17 +47,20 @@ internal class GroupedItemTemplateCollection2 : ObservableCollection<ItemTemplat
}
}
- ItemTemplateContext2 CreateItemContext(object item) =>
- new(_itemTemplate, item, _container, mauiContext: _mauiContext);
+ ItemTemplateContext2 CreateItemContext(object? item, object group, int groupItemIndex) =>
+ new(_itemTemplate, item, _container, mauiContext: _mauiContext,
+ group: group, groupItemIndex: groupItemIndex, entryKind: ItemTemplateContextKind2.Item);
ItemTemplateContext2? CreateHeaderContext(object group) =>
_groupHeaderTemplate is not null
- ? new(_groupHeaderTemplate, group, _container, null, null, null, isHeader: true, isFooter: false, mauiContext: _mauiContext)
+ ? new(_groupHeaderTemplate, group, _container, null, null, null, isHeader: true, isFooter: false,
+ mauiContext: _mauiContext, group: group, entryKind: ItemTemplateContextKind2.Header)
: null;
ItemTemplateContext2? CreateFooterContext(object group) =>
_groupFooterTemplate is not null
- ? new(_groupFooterTemplate, group, _container, null, null, null, isHeader: false, isFooter: true, mauiContext: _mauiContext)
+ ? new(_groupFooterTemplate, group, _container, null, null, null, isHeader: false, isFooter: true,
+ mauiContext: _mauiContext, group: group, entryKind: ItemTemplateContextKind2.Footer)
: null;
void SubscribeToGroups(IEnumerable? groups)
@@ -135,9 +137,10 @@ internal class GroupedItemTemplateCollection2 : ObservableCollection<ItemTemplat
Items.Add(header);
}
+ int groupItemIndex = 0;
foreach (var item in itemsList)
{
- Items.Add(CreateItemContext(item));
+ Items.Add(CreateItemContext(item, group, groupItemIndex++));
}
var footer = CreateFooterContext(group);
@@ -202,7 +205,7 @@ internal class GroupedItemTemplateCollection2 : ObservableCollection<ItemTemplat
if (group is ICollection collection)
flatIndex += collection.Count;
else
- flatIndex += items.Cast<object>().Count();
+ flatIndex += CountItems(items);
if (_groupFooterTemplate is not null)
flatIndex++;
@@ -229,19 +232,19 @@ internal class GroupedItemTemplateCollection2 : ObservableCollection<ItemTemplat
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
- HandleGroupItemsAdd(e, flatIndex, groupList);
+ HandleGroupItemsAdd(e, flatIndex, groupList, group);
break;
case NotifyCollectionChangedAction.Remove:
- HandleGroupItemsRemove(e, flatIndex);
+ HandleGroupItemsRemove(e, flatIndex, group);
break;
case NotifyCollectionChangedAction.Replace:
- HandleGroupItemsReplace(e, flatIndex);
+ HandleGroupItemsReplace(e, flatIndex, group);
break;
case NotifyCollectionChangedAction.Move:
- HandleGroupItemsMove(e, flatIndex);
+ HandleGroupItemsMove(e, flatIndex, group);
break;
case NotifyCollectionChangedAction.Reset:
@@ -250,7 +253,7 @@ internal class GroupedItemTemplateCollection2 : ObservableCollection<ItemTemplat
}
}
- void HandleGroupItemsAdd(NotifyCollectionChangedEventArgs e, int flatIndex, IList groupList)
+ void HandleGroupItemsAdd(NotifyCollectionChangedEventArgs e, int flatIndex, IList groupList, object group)
{
if (e.NewItems is null)
return;
@@ -265,19 +268,21 @@ internal class GroupedItemTemplateCollection2 : ObservableCollection<ItemTemplat
var newItems = new List<ItemTemplateContext2>(e.NewItems.Count);
_suppressNotifications = true;
+ int groupItemIndex = e.NewStartingIndex >= 0 ? e.NewStartingIndex : groupList.Count - e.NewItems.Count;
foreach (var item in e.NewItems)
{
- var newItem = CreateItemContext(item);
+ var newItem = CreateItemContext(item, group, groupItemIndex++);
newItems.Add(newItem);
Items.Insert(insertIndex++, newItem);
}
_suppressNotifications = false;
+ UpdateGroupItemIndices(group);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Add, newItems, insertIndex - newItems.Count));
}
- void HandleGroupItemsRemove(NotifyCollectionChangedEventArgs e, int flatIndex)
+ void HandleGroupItemsRemove(NotifyCollectionChangedEventArgs e, int flatIndex, object group)
{
if (e.OldItems is null)
return;
@@ -297,12 +302,13 @@ internal class GroupedItemTemplateCollection2 : ObservableCollection<ItemTemplat
Items.RemoveAt(removeIndex);
}
_suppressNotifications = false;
+ UpdateGroupItemIndices(group);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Remove, removedItems, removeIndex));
}
- void HandleGroupItemsReplace(NotifyCollectionChangedEventArgs e, int flatIndex)
+ void HandleGroupItemsReplace(NotifyCollectionChangedEventArgs e, int flatIndex, object group)
{
if (e.NewItems is null || e.OldItems is null)
return;
@@ -317,24 +323,24 @@ internal class GroupedItemTemplateCollection2 : ObservableCollection<ItemTemplat
var newItems = new List<ItemTemplateContext2>(e.NewItems.Count);
_suppressNotifications = true;
+ int groupItemIndex = e.NewStartingIndex >= 0 ? e.NewStartingIndex : 0;
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]!);
+ var newItem = CreateItemContext(item, group, groupItemIndex++);
newItems.Add(newItem);
Items[replaceIndex + i] = newItem;
}
_suppressNotifications = false;
+ UpdateGroupItemIndices(group);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Replace, newItems, oldItems, replaceIndex));
}
- void HandleGroupItemsMove(NotifyCollectionChangedEventArgs e, int flatIndex)
+ void HandleGroupItemsMove(NotifyCollectionChangedEventArgs e, int flatIndex, object group)
{
if (e.OldItems is null)
return;
@@ -357,6 +363,7 @@ internal class GroupedItemTemplateCollection2 : ObservableCollection<ItemTemplat
movedItems.Add(item);
}
_suppressNotifications = false;
+ UpdateGroupItemIndices(group);
// Fire Remove + Add instead of Move.
// CsWinRT translates CollectionChanged(Move) -> VectorChanged(Reset), which causes
@@ -380,6 +387,93 @@ internal class GroupedItemTemplateCollection2 : ObservableCollection<ItemTemplat
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
+ internal int FindGroupedEntryIndex(object? item, object group)
+ {
+ int headerIndex = -1;
+ int firstItemIndex = -1;
+ int valueEqualFallback = -1;
+
+ for (int i = 0; i < Items.Count; i++)
+ {
+ var context = Items[i];
+ if (!ReferenceEquals(context.Group, group))
+ {
+ continue;
+ }
+
+ if (context.EntryKind == ItemTemplateContextKind2.Header)
+ {
+ headerIndex = i;
+ continue;
+ }
+
+ if (context.EntryKind != ItemTemplateContextKind2.Item)
+ {
+ continue;
+ }
+
+ if (firstItemIndex < 0)
+ {
+ firstItemIndex = i;
+ }
+
+ if (item is null)
+ {
+ continue;
+ }
+
+ if (ReferenceEquals(context.Item, item))
+ {
+ return i;
+ }
+
+ if (valueEqualFallback < 0 && Equals(context.Item, item))
+ {
+ valueEqualFallback = i;
+ }
+ }
+
+ if (item is null)
+ {
+ return headerIndex >= 0 ? headerIndex : firstItemIndex;
+ }
+
+ return valueEqualFallback;
+ }
+
+ static int CountItems(IEnumerable items)
+ {
+ int count = 0;
+ foreach (var item in items)
+ {
+ count++;
+ }
+ return count;
+ }
+
+ void UpdateGroupItemIndices(object group)
+ {
+ int groupItemIndex = 0;
+ for (int i = 0; i < Items.Count; i++)
+ {
+ var context = Items[i];
+ if (!ReferenceEquals(context.Group, group))
+ {
+ continue;
+ }
+
+ if (context.EntryKind == ItemTemplateContextKind2.Item)
+ {
+ context.UpdateGroupedEntry(group, groupItemIndex++, ItemTemplateContextKind2.Item);
+ }
+ else if (context.EntryKind == ItemTemplateContextKind2.Header ||
+ context.EntryKind == ItemTemplateContextKind2.Footer)
+ {
+ context.UpdateGroupedEntry(group, -1, context.EntryKind);
+ }
+ }
+ }
+
/// <summary>
/// Full reset that also resubscribes to all groups.
/// Used for Replace and Reset actions where group references may have changed.
diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs
index cd2c2837ca..9ae1e20bf8 100644
--- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs
+++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs
@@ -2,6 +2,13 @@
namespace Microsoft.Maui.Controls.Handlers.Items2;
+internal enum ItemTemplateContextKind2
+{
+ Item,
+ Header,
+ Footer
+}
+
/// <summary>
/// Pairs a data item with its <see cref="DataTemplate"/> and layout metadata.
/// Used by the WinUI ItemsView/ItemsRepeater as the data context for each realized element.
@@ -31,15 +38,25 @@ internal class ItemTemplateContext2
/// <summary>The spacing between items.</summary>
public Thickness ItemSpacing { get; }
+ /// <summary>The grouped entry kind represented by this context.</summary>
+ public ItemTemplateContextKind2 EntryKind { get; private set; }
+
/// <summary>Whether this context represents a group header.</summary>
- public bool IsHeader { get; }
+ public bool IsHeader => EntryKind == ItemTemplateContextKind2.Header;
/// <summary>Whether this context represents a group footer.</summary>
- public bool IsFooter { get; }
+ public bool IsFooter => EntryKind == ItemTemplateContextKind2.Footer;
+
+ /// <summary>The source group for grouped item contexts.</summary>
+ public object? Group { get; private set; }
+
+ /// <summary>The index of this data item within <see cref="Group"/>, or -1 for header/footer entries.</summary>
+ public int GroupItemIndex { get; private set; } = -1;
public ItemTemplateContext2(DataTemplate mauiDataTemplate, object? item, BindableObject container,
double? height = null, double? width = null, Thickness? itemSpacing = null,
- bool isHeader = false, bool isFooter = false, IMauiContext? mauiContext = null)
+ bool isHeader = false, bool isFooter = false, IMauiContext? mauiContext = null,
+ object? group = null, int groupItemIndex = -1, ItemTemplateContextKind2 entryKind = ItemTemplateContextKind2.Item)
{
MauiDataTemplate = mauiDataTemplate;
Item = item;
@@ -49,7 +66,19 @@ internal class ItemTemplateContext2
ItemWidth = width ?? 0;
ItemSpacing = itemSpacing ?? default;
- IsHeader = isHeader;
- IsFooter = isFooter;
+ Group = group;
+ GroupItemIndex = groupItemIndex;
+ EntryKind = isHeader
+ ? ItemTemplateContextKind2.Header
+ : isFooter
+ ? ItemTemplateContextKind2.Footer
+ : entryKind;
+ }
+
+ internal void UpdateGroupedEntry(object group, int groupItemIndex, ItemTemplateContextKind2 entryKind)
+ {
+ Group = group;
+ GroupItemIndex = groupItemIndex;
+ EntryKind = entryKind;
}
}🏁 Report — Final Recommendation
Comparative Report — PR #34600
Testing signal
No candidate has a successful Windows regression run in the available artifacts. The raw PR gate is inconclusive, and every STEP 5a try-fix candidate was blocked before tests executed by the same NETSDK1005 missing net10.0-windows10.0.19041.0 target-assets environment issue. No candidate is ranked lower for failed regression tests because none recorded a regression-test failure.
Candidate comparison
| Rank | Candidate | Test result | Assessment |
|---|---|---|---|
| 1 | pr-plus-reviewer |
Inconclusive / unverified | Best candidate. It preserves the PR's Windows CV2 architecture and applies the expert reviewer's three actionable fixes: enumerable grouped ScrollTo, null-preserving grouped Replace, and header/footer selection filtering. |
| 2 | try-fix-2 |
Blocked | Strong focused fix for the grouped null Replace bug. Lower than pr-plus-reviewer because it does not address grouped ScrollTo or selection leakage. |
| 3 | try-fix-1 |
Blocked | Strong focused fix for grouped ScrollTo with enumerable groups. Lower than pr-plus-reviewer because it does not address null Replace or selection leakage. |
| 4 | try-fix-5 |
Blocked | Focused fix for selection filtering. Lower than pr-plus-reviewer because it does not address the two stronger grouped data-source issues. |
| 5 | try-fix-6 |
Blocked | Broader grouped identity approach that partly overlaps ScrollTo/null/selection concerns, but it is more invasive and still leaves drag/selection identity coverage incomplete per its own notes. |
| 6 | try-fix-3 |
Blocked | Addresses grouped drag/drop duplicate identity, but the expert review did not identify this as an actionable blocker in the current PR diff. |
| 7 | try-fix-4 |
Blocked | Addresses realized element cleanup, but the expert review did not identify this as an actionable blocker in the current PR diff. |
| 8 | pr |
Inconclusive / unverified | Raw PR remains valuable as the base implementation, but it has three unresolved expert-review major findings. |
Winning candidate
pr-plus-reviewer wins. It keeps the submitted PR implementation and applies only targeted reviewer feedback for concrete correctness issues found against the PR diff. Because it is still a PR-derived fix, the winner manifest marks it as isPRFix: true and leaves candidateDiff empty.
🧭 Next Steps — review latest findings
No alternative fix was selected for this run. Review the session findings and CI results before merging.
This comment has been minimized.
This comment has been minimized.
Tests Failure Analysis
Test Failure Review: Likely unrelated - click to expandOverall verdict: Likely unrelated This PR is labeled
Recommended actionNo PR-caused failures were identified. All failures are on iOS/macOS/Android platforms in areas outside this Windows-only PR's scope, and the Evidence detailsPR scope: 138 changed files, 113 test files; all platform-specific code is under Build 1474985 (maui-pr): AzDO — 7 failed timeline records. AOT failure message: "Unexpected warning files detected! Unexpected warning file paths are: ILC" with `IL3050: Using member 'Microsoft.Maui.Handlers.HybridWebViewHandler.SchemeHandler.Handler.get' which has 'RequiresDynamicCode'". Recent base-branch builds on same definition: 1475412 (failed), 1473152 (failed), 1472845 (failed), 1472462 (failed), 1470817 (failed) — all failed, confirming pre-existing issue. Build 1475001 (maui-pr-uitests): AzDO — 30 failed/cancelled timeline records. MacCatalyst visual test failures ( Limitations: No AzDO bearer token was available; |
Introduction
This PR implements the CollectionView2 (CV2) handler for Windows (WinUI), bringing the new
ItemsRepeater-based CollectionView architecture to the Windows platform. This is part of the ongoing effort to replace the legacyListView-based CollectionView implementation with a modern, performant, and feature-complete handler across all platforms.The CV2 handler on Windows leverages WinUI's
ItemsRepeatercontrol as the virtualizing layout engine, providing better performance, smoother scrolling, and more consistent behavior aligned with the CV2 implementations on Android, iOS, and MacCatalyst.Description of Changes
New Windows CV2 Handler Architecture
ItemsViewHandler2.Windows.cs— Core handler implementation mapping allCollectionViewproperties and commands to the WinUIItemsRepeater-based view. Handles layout mapping (LinearItemsLayout→StackLayout,GridItemsLayout→UniformGridLayout), scroll management, selection, snapping, incremental loading, andRefreshViewintegration.CollectionViewHandler2.Windows.cs— CollectionView-specific handler derived from the base items view handler, implementing selection modes (None, Single, Multiple), visual states, pre-selection, and pointer-based item interaction.MauiItemsView.cs— Custom WinUIUserControlhosting theItemsRepeaterinside aScrollViewer, with support for Header/Footer, EmptyView overlay, andRefreshContainerwrapping.GroupableUniformGridLayout.cs— Custom layout that extendsUniformGridLayoutbehavior to support grouped collection scenarios with proper group header/footer sizing.Item Source & Templating Pipeline
ItemFactory.cs—IElementFactoryimplementation that creates WinUI elements from MAUIDataTemplate/DataTemplateSelector, manages recycling, and handles grouped vs flat item sources.ObservableItemTemplateCollection2.cs— BridgesINotifyCollectionChangedsource collections toItemsRepeater-compatible observable collections, translating insert/remove/replace/move/reset operations.GroupedItemTemplateCollection2.cs— Flattens grouped source data (with group headers, items, and group footers) into a single linear collection consumable byItemsRepeater, tracking group boundary indices.TemplatedItemSourceFactory2.cs— Factory that selects the appropriate templated collection type (observable, grouped, or enumerable context) based on the items source characteristics.ItemTemplateContext2.cs,ItemTemplateContextEnumerable2.cs,ItemTemplateContextList2.cs— Context wrappers for item template binding.Supporting Changes
ItemsViewExtensions.Windows.cs— New extension class with helper methods for converting MAUI layout types, snap point settings, and sizing strategies to their WinUI equivalents.ItemsViewStyles.xaml— Updated WinUI resource dictionary with refined styles and templates for CV2 items, group headers/footers, header/footer, and empty view containers.AppHostBuilderExtensions.cs— Registered the CV2 handler mappings for Windows in the MAUI host builder, gated behind theCollectionView2runtime feature flag.RuntimeFeature.cs— Added theCollectionView2feature switch for conditional handler activation.PublicAPI.Unshipped.txt— Added 43 new public API entries for the Windows CV2 surface area.Build property integration — Updated
.csprojfiles and build targets to include theUseCollectionView2MSBuild property for opt-in activation.Issues Fixed
The following issues are addressed by this implementation and the associated bug fixes:
IEnumerablesource rendering not working in CollectionViewMajor Functional Improvements
GridItemsLayoutspan width; fixedLayoutOptionsalignment; fixedCornerRadiusrendering; fixed item margin and pointer-over visuals.PullToRefreshwithScrollViewer; fixedKeepScrollOffset/KeepItemsInView; fixedScrollToon unloaded view; fixed horizontal flicker.ScrollTofor grouped items.DataTemplatechange.DataTemplateSelectornot updating onCollectionChanged.Platforms Affected
UI Difference from CV1
Selection UI
Selection appearance differs from CV1, where the selected item is now highlighted using a border that fully wraps the item, instead of the previous visual style.
Output Screenshot
Drag and Drop Implementation
CV1DragandDrop.mp4
AnimationFinal.mp4
Testing