Skip to content

[iOS] Fix: CollectionView does not clear selection when SelectedItem is set to null#30420

Merged
kubaflo merged 4 commits intodotnet:inflight/currentfrom
Tamilarasan-Paranthaman:fix-30363
Mar 3, 2026
Merged

[iOS] Fix: CollectionView does not clear selection when SelectedItem is set to null#30420
kubaflo merged 4 commits intodotnet:inflight/currentfrom
Tamilarasan-Paranthaman:fix-30363

Conversation

@Tamilarasan-Paranthaman
Copy link
Copy Markdown
Member

@Tamilarasan-Paranthaman Tamilarasan-Paranthaman commented Jul 3, 2025

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Root Cause

CollectionView.SelectItem was being called inside the PerformBatchUpdates completion handler, which is triggered after all other actions are completed. As a result, when SelectedItem is set to null in the SelectionChanged event handler, the deferred selection inside PerformBatchUpdates would fire afterward and re-select the item, making the null assignment ineffective.

The original implementation (from PR #25555) always wrapped SelectItem calls in PerformBatchUpdates to ensure selection happened after collection view items generation was completed. This worked for initial load scenarios but caused a timing issue for runtime selection changes.

Description of Change

The fix introduces conditional logic based on the view's loaded state using CollectionView.IsLoaded() (which checks if UIView.Window != null):

For initial load (!IsLoaded()):

For runtime changes (IsLoaded()):

  • Selection executes immediately without PerformBatchUpdates wrapper
  • Includes all existing safety checks: EmptySource verification, reference equality, index recalculation, and item equality validation
  • Allows user code (like SelectedItem = null) to take effect immediately without being overridden by deferred selection

This resolves the issue where the selected item was not being cleared when SelectedItem is set to null during runtime.

Key Technical Details

IsLoaded() Extension Method:

  • Definition: UIView.Window != null
  • Indicates whether the view is attached to the window hierarchy
  • Used to distinguish between initial load (preselection) vs. runtime selection changes

Lifecycle Distinction:

  • Initial load: View isn't attached, items still being laid out → defer selection
  • Runtime: View is active, items stable → select immediately to avoid race conditions

Why Immediate Selection Works:
Direct SelectItem calls execute synchronously in the current call stack, before any completion handlers fire. This prevents the race condition where user code sets SelectedItem = null, but the deferred PerformBatchUpdates completion handler re-selects the item afterward.

Files Changed

  1. src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs - Added IsLoaded() check (deprecated handler)
  2. src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs - Added IsLoaded() check (current handler)
  3. src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs - New UI test demonstrating the fix
  4. src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs - Updated existing test
  5. src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs - Appium test with screenshot verification
  6. Snapshots for iOS and Android

What NOT to Do (for future agents)

  • Don't always use PerformBatchUpdates for selection - This causes deferred execution that can override user code
  • Don't remove PerformBatchUpdates entirely - Initial load scenarios still need it for proper item generation timing
  • Don't ignore the view's loaded state - The lifecycle context (initial vs. runtime) is critical for correct timing

Edge Cases

Scenario Risk Mitigation
EmptySource disposal Medium Runtime path checks ItemsSource is EmptySource before selection
ItemsSource changes during selection Medium Runtime path verifies ReferenceEquals(ItemsView.ItemsSource, originalSource)
Collection mutations (add/delete) Medium Runtime path recalculates index and verifies item equality at updated position
Initial preselection timing Low Preserved PerformBatchUpdates for !IsLoaded() case

Issues Fixed

Fixes #30363
Fixes #26187

Regression PR

This fix addresses a regression introduced in PR #25555, which added PerformBatchUpdates to ensure selection timing but didn't account for runtime selection clearing scenarios.

Platforms Tested

  • iOS
  • Mac
  • Android
  • Windows

Screenshots

Issue #30363:

Before Fix After Fix
30363-Before-Fix.mov
30363-After-Fix.mov

Issue #26187:

Before Fix After Fix
26187-Before-Fix.mov
26187-After-Fix.mov

@dotnet-policy-service dotnet-policy-service bot added community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration labels Jul 3, 2025
@jsuarezruiz jsuarezruiz added area-controls-collectionview CollectionView, CarouselView, IndicatorView platform/ios labels Jul 3, 2025
Copy link
Copy Markdown
Contributor

@bhavanesh2001 bhavanesh2001 left a comment

Choose a reason for hiding this comment

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

#29940 is in inflight/current now, Could you verify if those changes are sufficient to address the issues you're referring to here?

{
// Ensure the selected index is updated after the collection view's items generation is completed
CollectionView.PerformBatchUpdates(null, _ =>
if (!CollectionView.IsLoaded())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Won't this prevent item selection, if SelectedItem is pre-defined? please verify

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It will not prevent item pre-selection as long as CollectionView.SelectedItem is not set to null in the CollectionView.SelectionChanged event, as in the case reported by the user.

@Tamilarasan-Paranthaman
Copy link
Copy Markdown
Member Author

#29940 is in inflight/current now, Could you verify if those changes are sufficient to address the issues you're referring to here?

@bhavanesh2001, I have checked both issues, #30363 and #26187, with the changes made in PR #29940. However, the changes in #29940 do not address these issues.

@Tamilarasan-Paranthaman Tamilarasan-Paranthaman marked this pull request as ready for review July 4, 2025 12:46
@Tamilarasan-Paranthaman Tamilarasan-Paranthaman requested a review from a team as a code owner July 4, 2025 12:46
@rmarinho
Copy link
Copy Markdown
Member

rmarinho commented Jul 7, 2025

/rebase

Copilot AI review requested due to automatic review settings December 9, 2025 08:33
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Dec 9, 2025

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 30420

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 30420"

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes an iOS-specific bug where CollectionView selection could not be cleared by setting SelectedItem to null in the SelectionChanged event handler. The issue occurred because the selection logic was being deferred inside PerformBatchUpdates, which re-selected items after they were meant to be cleared.

Key Changes:

  • Modified selection logic to only use PerformBatchUpdates when the CollectionView is not yet loaded (preselection scenario)
  • For loaded CollectionViews, selection is now applied directly without batching
  • Applied fix consistently to both deprecated Items and current Items2 handler implementations

Reviewed changes

Copilot reviewed 5 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs Added IsLoaded() check to conditionally wrap SelectItem call in PerformBatchUpdates only during initial load
src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs Applied same fix to deprecated Items handler for consistency
src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs Added UI test page demonstrating selection clearing behavior
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs Added NUnit UI test to verify selection clearing via screenshot validation
src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs Cleaned up test code - removed unused properties, changed async void to void
src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/CollectionViewSelectionShouldClear.png iOS screenshot baseline for visual regression testing
src/Controls/tests/TestCases.Android.Tests/snapshots/android/CollectionViewSelectionShouldClear.png Android screenshot baseline for visual regression testing

@rmarinho rmarinho added s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-lose Author adopted the agent's fix and it turned out to be bad s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Feb 16, 2026
@kubaflo kubaflo added s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates and removed s/agent-fix-lose Author adopted the agent's fix and it turned out to be bad labels Feb 20, 2026
@kubaflo kubaflo added s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) and removed s/agent-approved AI agent recommends approval - PR fix is correct and optimal labels Mar 2, 2026
@dotnet dotnet deleted a comment from rmarinho Mar 2, 2026
Copy link
Copy Markdown
Contributor

@kubaflo kubaflo left a comment

Choose a reason for hiding this comment

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

🤖 AI Summary

📊 Expand Full Review
🔍 Pre-Flight — Context & Validation
📝 Review Sessionadded snapshots · bc8629b

Issue: #30363, #26187 - CollectionView does not clear selection when SelectedItem is set to null on iOS
Platforms Affected: iOS, MacCatalyst (fix touches both CV1 and CV2 iOS handlers)
Files Changed: 2 implementation files, 4 test files, 2 snapshot files

Summary

This PR fixes an iOS-specific bug where CollectionView selection could not be cleared by setting SelectedItem to null in the SelectionChanged event handler. The issue caused a visual regression where items appeared selected even after being programmatically deselected.

Root Cause (per PR):

  • CollectionView.SelectItem was always called inside PerformBatchUpdates completion handler
  • This completion handler fires after all other actions complete
  • When user sets SelectedItem = null in SelectionChanged, the deferred batch update re-selects the item afterward, making null assignment ineffective

Fix Approach (per PR):

  • Add IsLoaded() check (checks UIView.Window != null) to distinguish initial load vs runtime
  • For initial load (!IsLoaded()): Keep PerformBatchUpdates to defer until items are generated
  • For runtime (IsLoaded()): Direct SelectItem call with full safety checks (EmptySource, ReferenceEquals, index recalculation, item equality)
  • Applied consistently to both CV1 (Items/iOS/SelectableItemsViewController.cs) and CV2 (Items2/iOS/SelectableItemsViewController2.cs)

PR Discussion

File:Line Reviewer Comment Author Response Status
SelectableItemsViewController2.cs:59 bhavanesh2001 "Won't this prevent item selection if SelectedItem is pre-defined?" Pre-selection still works as long as user doesn't set SelectedItem=null in NEEDS GATE VERIFICATION SelectionChanged
SelectableItemsViewController.cs:58 jsuarezruiz "Create a shared helper method since fix is same in CV1 and CV2" Code changes are minimal; kept in respective places consistent with other CODE STYLE - not a correctness issue logic

Copilot review: Generated no inline comments; found changes correct

Edge Cases

  • Pre-selection still works (SelectedItem set before load, !IsLoaded() path)
  • Selection clearing in SelectionChanged event (primary bug scenario)
  • Multiple rapid selection changes
  • EmptySource disposal check (in runtime path)
  • ItemsSource reference equality check (in runtime path)
  • Index recalculation on collection mutation (in runtime path)

Files Classification

Fix Files:

  • src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs (CV1, deprecated) (+9/-2)
  • src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs (CV2, current) (+9/-2)

Test Files:

  • src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs (new UI test page, +60)
  • src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs (updated existing test, +2/-9)
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs (new NUnit test, +23)
  • src/Controls/tests/TestCases.iOS.Tests/snapshots/ios/CollectionViewSelectionShouldClear.png (snapshot)
  • src/Controls/tests/TestCases.Android.Tests/snapshots/android/CollectionViewSelectionShouldClear.png (snapshot)

Test Type: UI Tests with screenshot validation

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #30420 Add IsLoaded() check: use PerformBatchUpdates only during initial load (!IsLoaded()), direct SelectItem for loaded PENDING (Gate) 2 files (+18/-4) Original PR - applied to both CV1 and CV2 views

🚦 Gate — Test Verification
📝 Review Sessionadded snapshots · bc8629b

Result PASSED:
Platform: ios
Mode: Full Verification

  • Tests FAIL without fix (bug correctly detected)
  • Tests PASS with fix (fix correctly resolves issue)

Test: CollectionViewSelectionShouldClear (Issue30363)
Duration: ~5.5 minutes total (both runs)

Fix files verified:

  • src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs
  • src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs

🔧 Fix — Analysis & Comparison
📝 Review Sessionadded snapshots · bc8629b

Try-Fix Summary

Attempt Results

# Model Approach Result
PR Original IsLoaded() split: PerformBatchUpdates for !IsLoaded(), direct for PASS loaded
1 claude-sonnet-4.6 Equals guard inside PerformBatchUpdates completion PASS
2 claude-opus-4.6 _selectionGeneration counter (increment in ClearSelection PASS )
3 gpt-5.2 Reconcile from current ItemsView.SelectedItem at completion time PASS
4 gpt-5.3-codex Skip PerformBatchUpdates if item already natively selected PASS
5 gemini-3-pro-preview Compensating cleanup queued after pending selects in ClearSelection PASS
6 claude-sonnet-4.6 ShouldSelectItem delegate FAIL (UIKit does not call for programmatic selection) override
7 gpt-5.3-codex DesiredSelectedItem state machine + ViewDidLayoutSubviews PASS
8 gpt-5.2 WillDisplayCell cell lifecycle tracking PASS
9 gemini-3-pro-preview Remove PerformBatchUpdates entirely, call SelectItem directly PASS

Total: 9 attempts, 8 PASS, 1 FAIL
Cross-pollination: 3 rounds completed (max reached)

Key Findings

Root Cause Confirmed

SelectItem was wrapped in PerformBatchUpdates completion (added in PR #25555 for initial load timing). The completion fires AFTER SelectionChanged event, so when user sets SelectedItem = null in SelectionChanged, the deferred completion re-selects the item.

Critical UIKit Discovery

ShouldSelectItem delegate is NOT called for programmatic UICollectionView.SelectItem() only user taps. Any fix must operate in C# managed code or inside the PerformBatchUpdates completion block.calls

Best Alternative Fix

Attempt 1 (Equals guard inside completion) is the minimal-risk alternative:

  • +7 lines per file (smallest passing change besides Attempt 9)
  • No new fields, no lifecycle changes
  • Guards completion against re-selecting an item that was cleared during the async gap

Attempt 9 (remove PerformBatchUpdates entirely) is the simplest structurally but loses the timing guarantee for initial preselection (SelectedItem set before first layout).

PR Fix Assessment

The PR's IsLoaded() approach is semantically correct and explicitly handles both cases:

  • !IsLoaded(): keep PerformBatchUpdates (initial load needs async timing)
  • IsLoaded(): call directly (runtime changes are safe to apply immediately)

Recommendation: PR's fix is the right approach. The only improvement would be adding the Attempt 1 Equals guard as additional defense-in-depth inside the completion.

Cross-Pollination Rounds

Round claude-sonnet claude-opus gpt-5.2 gpt-5.3-codex gemini
1 NEW IDEAS NEW IDEAS NEW IDEAS NEW IDEAS NEW IDEAS
2 NEW IDEAS NO NEW IDEAS NEW IDEAS NEW IDEAS NEW IDEAS
3 NEW IDEAS NO NEW IDEAS NEW IDEAS NEW IDEAS NEW IDEAS

3-round maximum reached. Solution space thoroughly explored.

Selected Fix: PR's IsLoaded() approach (conditional PerformBatchUpdates)

The PR's fix correctly identifies the lifecycle boundary (initial load vs runtime) and handles each case appropriately. Best alternative is Attempt 1 (Equals guard, +7 lines) as defense-in-depth if the PR author wants additional safety.

Exhausted: 3 cross-pollination rounds completed (max reached), solution space thoroughly explored.Yes

Result: 8/9 PASS


📋 Report — Final Recommendation
📝 Review Sessionadded snapshots · bc8629b

PR #30420 Agent Review Report

Summary

PR: [iOS] Fix: CollectionView does not clear selection when SelectedItem is set to null
Gate PASSED :
Try-Fix: 9 attempts, 8 PASS, 1 FAIL
Recommendation APPROVE with minor suggestions:


Phase 1: Pre-Flight

  • Issue: #30363 (and # CollectionView selection persists after SelectedItem = null in SelectionChanged26187)
  • Root cause confirmed: SelectItem was wrapped in PerformBatchUpdates completion, which fires AFTER SelectionChanged. Setting SelectedItem = null in SelectionChanged had no effect because the deferred completion re-selected the item.
  • Regression: Introduced by PR #25555 (added PerformBatchUpdates for initial load timing)
  • Fix targets: Items/iOS/SelectableItemsViewController.cs and Items2/iOS/SelectableItemsViewController2.cs
  • Tests added: Issue30363.cs in HostApp and Shared.Tests

Phase 2: Gate PASSED

Tests FAIL without fix, PASS with fix on iOS simulator.

Test: Issue30363. taps item, verifies selection cleared via screenshot.CollectionViewSelectionShouldClear


Phase 3: Try-Fix (9 attempts, 3 cross-poll rounds)

# Model Approach Result
PR Original IsLoaded() split: PerformBatchUpdates only for ! PASS IsLoaded()
1 claude-sonnet Equals guard inside completion block PASS
2 claude-opus _selectionGeneration counter PASS
3 gpt-5.2 Reconcile from current SelectedItem at completion time PASS
4 gpt-5.3-codex Skip if already natively selected PASS
5 gemini Compensating cleanup queued in ClearSelection PASS
6 claude-sonnet ShouldSelectItem FAIL override
7 gpt-5.3-codex DesiredSelectedItem state machine PASS
8 gpt-5.2 WillDisplayCell lifecycle tracking PASS
9 gemini Remove PerformBatchUpdates entirely PASS

Key discovery (Attempt 6): ShouldSelectItem delegate is NOT called for programmatic UICollectionView. UIKit only calls it for user taps. Any fix must operate in C# managed code.SelectItem()

Best alternative: Attempt 1 (Equals guard, +7 lines) provides defense-in-depth without architectural changes.

The PR's approach is the right it explicitly distinguishes initial load from runtime and preserves the PerformBatchUpdates timing guarantee for initial preselection.design


Phase 4: PR Finalize

Title Assessment

Current: [iOS] Fix: CollectionView does not clear selection when SelectedItem is set to null
Assessment: Good. Platform prefix, clear description. The "Fix:" sub-prefix is optional but acceptable.

Description Assessment Keep As-IsExcellent

The description has all required elements:

  • NOTE block (user testing artifacts)
  • Root cause with specific code path explanation
  • Description of Change with conditional logic breakdown
  • Key Technical Details (IsLoaded() definition, lifecycle distinction)
  • Files Changed (all 6)
  • What NOT to Do (3 entries)
  • Edge Cases table
  • Issues Fixed (#30363, #26187)
  • Regression PR (#25555)
  • Platforms Tested (all 4)
  • Before/after screenshots (videos)

Code Review Findings

Files: Both SelectableItemsViewController.cs and SelectableItemsViewController2.cs

The original PerformBatchUpdates completion had 5 safety guards before calling SelectItem:

  1. if (ItemsSource is EmptySource) return;
  2. if (!ReferenceEquals(ItemsView.ItemsSource, originalSource)) return;
  3. Recalculate updatedIndex after delay
  4. if (updatedIndex == null) return;
  5. Item equality check if (!Equals(updatedItem, selectedItem)) return;

The PR's !IsLoaded() branch removes ALL of them, calling CollectionView.SelectItem(index, ...) using the index captured at method entry (before any async gap), with no guards against ItemsSource disposal or collection mutations.directly

Risk: Low in practice (initial load rarely has concurrent mutations), but the guards were deliberately added and their removal is undocumented. If items are reordered during initial load, the wrong item may be visually selected even if it passes index equality.

Recommendation: Move the safety guards into the !IsLoaded() completion handler too (same as the IsLoaded() path), or add a code comment explaining why they're intentionally omitted for the initial load case.

// Suggested for !IsLoaded() path:
CollectionView.PerformBatchUpdates(null, _ =>
{
    if (ItemsSource is EmptySource)
        return;
    var originalSource = ItemsSource;
    if (!ReferenceEquals(ItemsView.ItemsSource, originalSource))
        return;
    var updatedIndex = IndexPath(selectedItem);
    if (updatedIndex == null)
        return;
    var updatedItem = GetItem(updatedIndex);
    if (!Equals(updatedItem, selectedItem))
        return;
    CollectionView.SelectItem(updatedIndex, true, UICollectionViewScrollPosition.None);
});

Unobserved Task in Issue26187.cs

File: src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs

The PR changed async void CollectionView_SelectionChanged with await Navigation.PushAsync(...) to void CollectionView_SelectionChanged with Navigation.PushAsync(...) (no await). If navigation throws, the exception is unobserved.

Recommendation: Use _ = Navigation.PushAsync(new NewPage(issue)); to explicitly discard, or keep async void.

File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs

VerifyScreenshot() called immediately after App.Tap("cvItem") with no retry timeout. Selection animations could cause timing differences between runs.

Recommendation:

VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2));

Positives

  • Both CV1 () and CV2 () handlers updated identically
  • extension method is semantically clear and correctly defined
  • All existing safety guards preserved in the (runtime) path
  • Excellent PR description with root cause, edge cases, and failure guidance
  • Cross-platform: fix applies to both deprecated and current handler

Recommendation

** APPROVE with suggestions**

The PR's core fix is correct, well-tested, and well-documented. The main concern is the missing safety guards in the !IsLoaded() completion handler. This is a low-risk issue (initial load mutations are rare) but should be addressed to maintain correctness guarantees.


📋 Expand PR Finalization Review
Title: ✅ Good

Current: [iOS] Fix: CollectionView does not clear selection when SelectedItem is set to null

Description: ✅ Excellent

Description needs updates. See details below.

Code Review: ✅ Passed

Code Review — PR #30420

Files reviewed:

  • src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs
  • src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs
  • src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs
  • src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs

🟡 Suggestions

1. Initial-load path lost safety guards from original implementation

Files: Both SelectableItemsViewController.cs and SelectableItemsViewController2.cs

Problem:
The original code had these safety checks inside the PerformBatchUpdates completion handler (which ran for all scenarios):

  1. if (ItemsSource is EmptySource) return;
  2. if (!ReferenceEquals(ItemsView.ItemsSource, originalSource)) return;
  3. Index recalculation after source changes
  4. Item equality check at the new index

After the PR, the !IsLoaded() (initial load) branch wraps SelectItem in PerformBatchUpdates but without any of these guards. All guards are now exclusively in the else/runtime branch.

Before:

CollectionView.PerformBatchUpdates(null, _ =>
{
    if (ItemsSource is EmptySource)
        return;
    // ... reference equality check ...
    // ... index recalculation ...
    CollectionView.SelectItem(index, true, UICollectionViewScrollPosition.None);
});

After:

if (!CollectionView.IsLoaded())
{
    CollectionView.PerformBatchUpdates(null, _ =>
    {
        // NO safety checks here
        CollectionView.SelectItem(index, true, UICollectionViewScrollPosition.None);
    });
}
else
{
    // All safety checks here
    if (ItemsSource is EmptySource) return;
    // ...
    CollectionView.SelectItem(index, true, UICollectionViewScrollPosition.None);
}

Risk: Low for typical use, but if ItemsSource is disposed or replaced before the view is loaded (edge case), a crash or stale-index selection could occur.

Recommendation: Consider also guarding the initial-load path with at minimum the EmptySource check, or add a comment explicitly acknowledging the trade-off:

if (!CollectionView.IsLoaded())
{
    CollectionView.PerformBatchUpdates(null, _ =>
    {
        // Safety checks are omitted for initial load since the source is
        // expected to be stable; runtime path includes full validation.
        CollectionView.SelectItem(index, true, UICollectionViewScrollPosition.None);
    });
}

2. Navigation.PushAsync changed from awaited to fire-and-forget in Issue26187.cs

File: src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs

Problem:

// Before
async void CollectionView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (e.CurrentSelection.FirstOrDefault() is string issue)
    {
        await Navigation.PushAsync(new NewPage(issue));
    }
    // ...
}

// After
void CollectionView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (e.CurrentSelection.FirstOrDefault() is string issue)
    {
        Navigation.PushAsync(new NewPage(issue));  // fire-and-forget
    }
    // ...
}

Risk: Low for test code, but if PushAsync throws, the exception will be unobserved. In a UI test context, this could mask failures.

Recommendation: This is test-only code and the risk is minimal. Acceptable as-is, but _ = Navigation.PushAsync(...) would make the intent explicit.


3. UI test uses non-unique AutomationId for CollectionView items

File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs
Related: src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs

Problem:
The DataTemplate sets AutomationId = "cvItem" on a label inside each cell. All 5 items share the same AutomationId. App.WaitForElement("cvItem") and App.Tap("cvItem") will always interact with the first matching element.

Risk: Low — tapping the first item is sufficient to reproduce the bug. The test still validates the scenario correctly via screenshot.

Recommendation: This is a common pattern in CollectionView UI tests and is acceptable. No change required.


4. UI test relies solely on screenshot verification without explicit deselection assertion

File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs

Problem:

public void CollectionViewSelectionShouldClear()
{
    App.WaitForElement("cvItem");
    App.Tap("cvItem");
    VerifyScreenshot();
}

The test taps an item (triggering SelectionChanged which sets SelectedItem = null) and then takes a screenshot. If the visual difference between "item selected" and "item deselected" is subtle (e.g., highlight color), the screenshot comparison may not robustly catch regressions.

Risk: Low for current scenarios — screenshot baselines are pre-captured and will catch visual changes. However, if items have no visible selection highlight, the screenshot wouldn't distinguish pass from fail without the baseline.

Recommendation: Consider adding an intermediate step to verify the item is visually deselected, e.g., checking a label text update or using retryTimeout for screenshot stability:

App.WaitForElement("cvItem");
App.Tap("cvItem");
VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2));

✅ Looks Good

  • Symmetry: Both Items/ and Items2/ handler files are updated identically, maintaining consistency across deprecated and current iOS handlers (as expected per collectionview-handler-detection.instructions.md — Items/ is for Android/Windows, Items2/ is for iOS/MacCatalyst; both iOS implementations are correctly updated).
  • Logic is correct: The IsLoaded() condition (UIView.Window != null) correctly distinguishes initial load from runtime. The description's rationale is sound.
  • No breaking API changes: The fix is purely internal/behavioral.
  • Both issues covered: #30363 (new test) and #26187 (existing test updated) are both addressed.
  • Android snapshot added: Confirms Android behavior is unchanged (regression test).
  • Platform scope: Fix is correctly scoped to iOS only ([iOS] prefix, .iOS.cs files only).

@Tamilarasan-Paranthaman
Copy link
Copy Markdown
Member Author

🤖 AI Summary

📊 Expand Full Review

Recommendation

** APPROVE with suggestions**

The PR's core fix is correct, well-tested, and well-documented. The main concern is the missing safety guards in the !IsLoaded() completion handler. This is a low-risk issue (initial load mutations are rare) but should be addressed to maintain correctness guarantees.

📋 Expand PR Finalization Review
Title: ✅ Good

Current: [iOS] Fix: CollectionView does not clear selection when SelectedItem is set to null

Description: ✅ Excellent

Description needs updates. See details below.

Code Review: ✅ Passed

Code Review — PR #30420

Files reviewed:

  • src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs
  • src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs
  • src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs
  • src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs

🟡 Suggestions

1. Initial-load path lost safety guards from original implementation

Files: Both SelectableItemsViewController.cs and SelectableItemsViewController2.cs

Problem: The original code had these safety checks inside the PerformBatchUpdates completion handler (which ran for all scenarios):

  1. if (ItemsSource is EmptySource) return;
  2. if (!ReferenceEquals(ItemsView.ItemsSource, originalSource)) return;
  3. Index recalculation after source changes
  4. Item equality check at the new index

After the PR, the !IsLoaded() (initial load) branch wraps SelectItem in PerformBatchUpdates but without any of these guards. All guards are now exclusively in the else/runtime branch.

Before:

CollectionView.PerformBatchUpdates(null, _ =>
{
    if (ItemsSource is EmptySource)
        return;
    // ... reference equality check ...
    // ... index recalculation ...
    CollectionView.SelectItem(index, true, UICollectionViewScrollPosition.None);
});

After:

if (!CollectionView.IsLoaded())
{
    CollectionView.PerformBatchUpdates(null, _ =>
    {
        // NO safety checks here
        CollectionView.SelectItem(index, true, UICollectionViewScrollPosition.None);
    });
}
else
{
    // All safety checks here
    if (ItemsSource is EmptySource) return;
    // ...
    CollectionView.SelectItem(index, true, UICollectionViewScrollPosition.None);
}

Risk: Low for typical use, but if ItemsSource is disposed or replaced before the view is loaded (edge case), a crash or stale-index selection could occur.

Recommendation: Consider also guarding the initial-load path with at minimum the EmptySource check, or add a comment explicitly acknowledging the trade-off:

if (!CollectionView.IsLoaded())
{
    CollectionView.PerformBatchUpdates(null, _ =>
    {
        // Safety checks are omitted for initial load since the source is
        // expected to be stable; runtime path includes full validation.
        CollectionView.SelectItem(index, true, UICollectionViewScrollPosition.None);
    });
}

2. Navigation.PushAsync changed from awaited to fire-and-forget in Issue26187.cs

File: src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs

Problem:

// Before
async void CollectionView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (e.CurrentSelection.FirstOrDefault() is string issue)
    {
        await Navigation.PushAsync(new NewPage(issue));
    }
    // ...
}

// After
void CollectionView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (e.CurrentSelection.FirstOrDefault() is string issue)
    {
        Navigation.PushAsync(new NewPage(issue));  // fire-and-forget
    }
    // ...
}

Risk: Low for test code, but if PushAsync throws, the exception will be unobserved. In a UI test context, this could mask failures.

Recommendation: This is test-only code and the risk is minimal. Acceptable as-is, but _ = Navigation.PushAsync(...) would make the intent explicit.

3. UI test uses non-unique AutomationId for CollectionView items

File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs Related: src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs

Problem: The DataTemplate sets AutomationId = "cvItem" on a label inside each cell. All 5 items share the same AutomationId. App.WaitForElement("cvItem") and App.Tap("cvItem") will always interact with the first matching element.

Risk: Low — tapping the first item is sufficient to reproduce the bug. The test still validates the scenario correctly via screenshot.

Recommendation: This is a common pattern in CollectionView UI tests and is acceptable. No change required.

4. UI test relies solely on screenshot verification without explicit deselection assertion

File: src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs

Problem:

public void CollectionViewSelectionShouldClear()
{
    App.WaitForElement("cvItem");
    App.Tap("cvItem");
    VerifyScreenshot();
}

The test taps an item (triggering SelectionChanged which sets SelectedItem = null) and then takes a screenshot. If the visual difference between "item selected" and "item deselected" is subtle (e.g., highlight color), the screenshot comparison may not robustly catch regressions.

Risk: Low for current scenarios — screenshot baselines are pre-captured and will catch visual changes. However, if items have no visible selection highlight, the screenshot wouldn't distinguish pass from fail without the baseline.

Recommendation: Consider adding an intermediate step to verify the item is visually deselected, e.g., checking a label text update or using retryTimeout for screenshot stability:

App.WaitForElement("cvItem");
App.Tap("cvItem");
VerifyScreenshot(retryTimeout: TimeSpan.FromSeconds(2));

✅ Looks Good

  • Symmetry: Both Items/ and Items2/ handler files are updated identically, maintaining consistency across deprecated and current iOS handlers (as expected per collectionview-handler-detection.instructions.md — Items/ is for Android/Windows, Items2/ is for iOS/MacCatalyst; both iOS implementations are correctly updated).
  • Logic is correct: The IsLoaded() condition (UIView.Window != null) correctly distinguishes initial load from runtime. The description's rationale is sound.
  • No breaking API changes: The fix is purely internal/behavioral.
  • Both issues covered: CollectionView not being able to remove selected item highlight on iOS #30363 (new test) and [MAUI] Select items traces are preserved #26187 (existing test updated) are both addressed.
  • Android snapshot added: Confirms Android behavior is unchanged (regression test).
  • Platform scope: Fix is correctly scoped to iOS only ([iOS] prefix, .iOS.cs files only).

@kubaflo, as suggested by the agent, the safety guards have been moved into the !IsLoaded() completion handler as well (similar to the IsLoaded() path), and the code has been further optimized to remove duplicates.

Also, retryTimeout has been added to VerifyScreenshot.

@kubaflo kubaflo added s/agent-suggestions-implemented Maintainer applies when PR author adopts agent's recommendation s/agent-fix-implemented PR author implemented the agent suggested fix s/agent-fix-win AI found a better alternative fix than the PR and removed s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates labels Mar 3, 2026
@kubaflo kubaflo changed the base branch from main to inflight/current March 3, 2026 12:19
@kubaflo kubaflo merged commit c048134 into dotnet:inflight/current Mar 3, 2026
25 of 29 checks passed
github-actions bot pushed a commit that referenced this pull request Mar 3, 2026
…is set to null (#30420)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!


### Root Cause

CollectionView.SelectItem was being called inside the
`PerformBatchUpdates` completion handler, which is triggered after all
other actions are completed. As a result, when `SelectedItem` is set to
null in the `SelectionChanged` event handler, the deferred selection
inside `PerformBatchUpdates` would fire afterward and re-select the
item, making the null assignment ineffective.

The original implementation (from PR #25555) always wrapped `SelectItem`
calls in `PerformBatchUpdates` to ensure selection happened after
collection view items generation was completed. This worked for initial
load scenarios but caused a timing issue for runtime selection changes.

### Description of Change

The fix introduces conditional logic based on the view's loaded state
using `CollectionView.IsLoaded()` (which checks if `UIView.Window !=
null`):

**For initial load (!IsLoaded()):**
- Selection still uses `PerformBatchUpdates` to defer until items are
generated
- This preserves the original intent from PR #25555

**For runtime changes (IsLoaded()):**
- Selection executes immediately without `PerformBatchUpdates` wrapper
- Includes all existing safety checks: EmptySource verification,
reference equality, index recalculation, and item equality validation
- Allows user code (like `SelectedItem = null`) to take effect
immediately without being overridden by deferred selection

This resolves the issue where the selected item was not being cleared
when `SelectedItem` is set to null during runtime.

### Key Technical Details

**IsLoaded() Extension Method:**
- Definition: `UIView.Window != null`
- Indicates whether the view is attached to the window hierarchy
- Used to distinguish between initial load (preselection) vs. runtime
selection changes

**Lifecycle Distinction:**
- **Initial load**: View isn't attached, items still being laid out →
defer selection
- **Runtime**: View is active, items stable → select immediately to
avoid race conditions

**Why Immediate Selection Works:**
Direct `SelectItem` calls execute synchronously in the current call
stack, before any completion handlers fire. This prevents the race
condition where user code sets `SelectedItem = null`, but the deferred
`PerformBatchUpdates` completion handler re-selects the item afterward.

### Files Changed

1.
`src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs`
- Added IsLoaded() check (deprecated handler)
2.
`src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs`
- Added IsLoaded() check (current handler)
3. `src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs` - New UI
test demonstrating the fix
4. `src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs` - Updated
existing test
5.
`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs` -
Appium test with screenshot verification
6. Snapshots for iOS and Android

### What NOT to Do (for future agents)

- ❌ **Don't always use PerformBatchUpdates for selection** - This causes
deferred execution that can override user code
- ❌ **Don't remove PerformBatchUpdates entirely** - Initial load
scenarios still need it for proper item generation timing
- ❌ **Don't ignore the view's loaded state** - The lifecycle context
(initial vs. runtime) is critical for correct timing

### Edge Cases

| Scenario | Risk | Mitigation |
|----------|------|------------|
| EmptySource disposal | Medium | Runtime path checks `ItemsSource is
EmptySource` before selection |
| ItemsSource changes during selection | Medium | Runtime path verifies
`ReferenceEquals(ItemsView.ItemsSource, originalSource)` |
| Collection mutations (add/delete) | Medium | Runtime path recalculates
index and verifies item equality at updated position |
| Initial preselection timing | Low | Preserved PerformBatchUpdates for
!IsLoaded() case |

### Issues Fixed

Fixes #30363
Fixes #26187

### Regression PR

This fix addresses a regression introduced in PR #25555, which added
`PerformBatchUpdates` to ensure selection timing but didn't account for
runtime selection clearing scenarios.

### Platforms Tested

- [x] iOS
- [x] Mac
- [x] Android
- [x] Windows

### Screenshots

**Issue #30363:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/09891481-5e3a-476d-a058-b6f828335a63">
| <video
src="https://github.com/user-attachments/assets/6bad46a2-acbf-498a-a45c-e08c84f4a32a">
|

**Issue #26187:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/95245bc1-5772-4cc1-9947-c371a4c35586">
| <video
src="https://github.com/user-attachments/assets/1474b60e-d552-4a05-9461-fb513e3ef5b0">
|
HarishKumarSF4517 pushed a commit to HarishKumarSF4517/maui that referenced this pull request Mar 5, 2026
…is set to null (dotnet#30420)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!


### Root Cause

CollectionView.SelectItem was being called inside the
`PerformBatchUpdates` completion handler, which is triggered after all
other actions are completed. As a result, when `SelectedItem` is set to
null in the `SelectionChanged` event handler, the deferred selection
inside `PerformBatchUpdates` would fire afterward and re-select the
item, making the null assignment ineffective.

The original implementation (from PR dotnet#25555) always wrapped `SelectItem`
calls in `PerformBatchUpdates` to ensure selection happened after
collection view items generation was completed. This worked for initial
load scenarios but caused a timing issue for runtime selection changes.

### Description of Change

The fix introduces conditional logic based on the view's loaded state
using `CollectionView.IsLoaded()` (which checks if `UIView.Window !=
null`):

**For initial load (!IsLoaded()):**
- Selection still uses `PerformBatchUpdates` to defer until items are
generated
- This preserves the original intent from PR dotnet#25555

**For runtime changes (IsLoaded()):**
- Selection executes immediately without `PerformBatchUpdates` wrapper
- Includes all existing safety checks: EmptySource verification,
reference equality, index recalculation, and item equality validation
- Allows user code (like `SelectedItem = null`) to take effect
immediately without being overridden by deferred selection

This resolves the issue where the selected item was not being cleared
when `SelectedItem` is set to null during runtime.

### Key Technical Details

**IsLoaded() Extension Method:**
- Definition: `UIView.Window != null`
- Indicates whether the view is attached to the window hierarchy
- Used to distinguish between initial load (preselection) vs. runtime
selection changes

**Lifecycle Distinction:**
- **Initial load**: View isn't attached, items still being laid out →
defer selection
- **Runtime**: View is active, items stable → select immediately to
avoid race conditions

**Why Immediate Selection Works:**
Direct `SelectItem` calls execute synchronously in the current call
stack, before any completion handlers fire. This prevents the race
condition where user code sets `SelectedItem = null`, but the deferred
`PerformBatchUpdates` completion handler re-selects the item afterward.

### Files Changed

1.
`src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs`
- Added IsLoaded() check (deprecated handler)
2.
`src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs`
- Added IsLoaded() check (current handler)
3. `src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs` - New UI
test demonstrating the fix
4. `src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs` - Updated
existing test
5.
`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs` -
Appium test with screenshot verification
6. Snapshots for iOS and Android

### What NOT to Do (for future agents)

- ❌ **Don't always use PerformBatchUpdates for selection** - This causes
deferred execution that can override user code
- ❌ **Don't remove PerformBatchUpdates entirely** - Initial load
scenarios still need it for proper item generation timing
- ❌ **Don't ignore the view's loaded state** - The lifecycle context
(initial vs. runtime) is critical for correct timing

### Edge Cases

| Scenario | Risk | Mitigation |
|----------|------|------------|
| EmptySource disposal | Medium | Runtime path checks `ItemsSource is
EmptySource` before selection |
| ItemsSource changes during selection | Medium | Runtime path verifies
`ReferenceEquals(ItemsView.ItemsSource, originalSource)` |
| Collection mutations (add/delete) | Medium | Runtime path recalculates
index and verifies item equality at updated position |
| Initial preselection timing | Low | Preserved PerformBatchUpdates for
!IsLoaded() case |

### Issues Fixed

Fixes dotnet#30363
Fixes dotnet#26187

### Regression PR

This fix addresses a regression introduced in PR dotnet#25555, which added
`PerformBatchUpdates` to ensure selection timing but didn't account for
runtime selection clearing scenarios.

### Platforms Tested

- [x] iOS
- [x] Mac
- [x] Android
- [x] Windows

### Screenshots

**Issue dotnet#30363:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/09891481-5e3a-476d-a058-b6f828335a63">
| <video
src="https://github.com/user-attachments/assets/6bad46a2-acbf-498a-a45c-e08c84f4a32a">
|

**Issue dotnet#26187:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/95245bc1-5772-4cc1-9947-c371a4c35586">
| <video
src="https://github.com/user-attachments/assets/1474b60e-d552-4a05-9461-fb513e3ef5b0">
|
PureWeen pushed a commit that referenced this pull request Mar 11, 2026
…is set to null (#30420)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!


### Root Cause

CollectionView.SelectItem was being called inside the
`PerformBatchUpdates` completion handler, which is triggered after all
other actions are completed. As a result, when `SelectedItem` is set to
null in the `SelectionChanged` event handler, the deferred selection
inside `PerformBatchUpdates` would fire afterward and re-select the
item, making the null assignment ineffective.

The original implementation (from PR #25555) always wrapped `SelectItem`
calls in `PerformBatchUpdates` to ensure selection happened after
collection view items generation was completed. This worked for initial
load scenarios but caused a timing issue for runtime selection changes.

### Description of Change

The fix introduces conditional logic based on the view's loaded state
using `CollectionView.IsLoaded()` (which checks if `UIView.Window !=
null`):

**For initial load (!IsLoaded()):**
- Selection still uses `PerformBatchUpdates` to defer until items are
generated
- This preserves the original intent from PR #25555

**For runtime changes (IsLoaded()):**
- Selection executes immediately without `PerformBatchUpdates` wrapper
- Includes all existing safety checks: EmptySource verification,
reference equality, index recalculation, and item equality validation
- Allows user code (like `SelectedItem = null`) to take effect
immediately without being overridden by deferred selection

This resolves the issue where the selected item was not being cleared
when `SelectedItem` is set to null during runtime.

### Key Technical Details

**IsLoaded() Extension Method:**
- Definition: `UIView.Window != null`
- Indicates whether the view is attached to the window hierarchy
- Used to distinguish between initial load (preselection) vs. runtime
selection changes

**Lifecycle Distinction:**
- **Initial load**: View isn't attached, items still being laid out →
defer selection
- **Runtime**: View is active, items stable → select immediately to
avoid race conditions

**Why Immediate Selection Works:**
Direct `SelectItem` calls execute synchronously in the current call
stack, before any completion handlers fire. This prevents the race
condition where user code sets `SelectedItem = null`, but the deferred
`PerformBatchUpdates` completion handler re-selects the item afterward.

### Files Changed

1.
`src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs`
- Added IsLoaded() check (deprecated handler)
2.
`src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs`
- Added IsLoaded() check (current handler)
3. `src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs` - New UI
test demonstrating the fix
4. `src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs` - Updated
existing test
5.
`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs` -
Appium test with screenshot verification
6. Snapshots for iOS and Android

### What NOT to Do (for future agents)

- ❌ **Don't always use PerformBatchUpdates for selection** - This causes
deferred execution that can override user code
- ❌ **Don't remove PerformBatchUpdates entirely** - Initial load
scenarios still need it for proper item generation timing
- ❌ **Don't ignore the view's loaded state** - The lifecycle context
(initial vs. runtime) is critical for correct timing

### Edge Cases

| Scenario | Risk | Mitigation |
|----------|------|------------|
| EmptySource disposal | Medium | Runtime path checks `ItemsSource is
EmptySource` before selection |
| ItemsSource changes during selection | Medium | Runtime path verifies
`ReferenceEquals(ItemsView.ItemsSource, originalSource)` |
| Collection mutations (add/delete) | Medium | Runtime path recalculates
index and verifies item equality at updated position |
| Initial preselection timing | Low | Preserved PerformBatchUpdates for
!IsLoaded() case |

### Issues Fixed

Fixes #30363
Fixes #26187

### Regression PR

This fix addresses a regression introduced in PR #25555, which added
`PerformBatchUpdates` to ensure selection timing but didn't account for
runtime selection clearing scenarios.

### Platforms Tested

- [x] iOS
- [x] Mac
- [x] Android
- [x] Windows

### Screenshots

**Issue #30363:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/09891481-5e3a-476d-a058-b6f828335a63">
| <video
src="https://github.com/user-attachments/assets/6bad46a2-acbf-498a-a45c-e08c84f4a32a">
|

**Issue #26187:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/95245bc1-5772-4cc1-9947-c371a4c35586">
| <video
src="https://github.com/user-attachments/assets/1474b60e-d552-4a05-9461-fb513e3ef5b0">
|
github-actions bot pushed a commit that referenced this pull request Mar 11, 2026
…is set to null (#30420)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!


### Root Cause

CollectionView.SelectItem was being called inside the
`PerformBatchUpdates` completion handler, which is triggered after all
other actions are completed. As a result, when `SelectedItem` is set to
null in the `SelectionChanged` event handler, the deferred selection
inside `PerformBatchUpdates` would fire afterward and re-select the
item, making the null assignment ineffective.

The original implementation (from PR #25555) always wrapped `SelectItem`
calls in `PerformBatchUpdates` to ensure selection happened after
collection view items generation was completed. This worked for initial
load scenarios but caused a timing issue for runtime selection changes.

### Description of Change

The fix introduces conditional logic based on the view's loaded state
using `CollectionView.IsLoaded()` (which checks if `UIView.Window !=
null`):

**For initial load (!IsLoaded()):**
- Selection still uses `PerformBatchUpdates` to defer until items are
generated
- This preserves the original intent from PR #25555

**For runtime changes (IsLoaded()):**
- Selection executes immediately without `PerformBatchUpdates` wrapper
- Includes all existing safety checks: EmptySource verification,
reference equality, index recalculation, and item equality validation
- Allows user code (like `SelectedItem = null`) to take effect
immediately without being overridden by deferred selection

This resolves the issue where the selected item was not being cleared
when `SelectedItem` is set to null during runtime.

### Key Technical Details

**IsLoaded() Extension Method:**
- Definition: `UIView.Window != null`
- Indicates whether the view is attached to the window hierarchy
- Used to distinguish between initial load (preselection) vs. runtime
selection changes

**Lifecycle Distinction:**
- **Initial load**: View isn't attached, items still being laid out →
defer selection
- **Runtime**: View is active, items stable → select immediately to
avoid race conditions

**Why Immediate Selection Works:**
Direct `SelectItem` calls execute synchronously in the current call
stack, before any completion handlers fire. This prevents the race
condition where user code sets `SelectedItem = null`, but the deferred
`PerformBatchUpdates` completion handler re-selects the item afterward.

### Files Changed

1.
`src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs`
- Added IsLoaded() check (deprecated handler)
2.
`src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs`
- Added IsLoaded() check (current handler)
3. `src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs` - New UI
test demonstrating the fix
4. `src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs` - Updated
existing test
5.
`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs` -
Appium test with screenshot verification
6. Snapshots for iOS and Android

### What NOT to Do (for future agents)

- ❌ **Don't always use PerformBatchUpdates for selection** - This causes
deferred execution that can override user code
- ❌ **Don't remove PerformBatchUpdates entirely** - Initial load
scenarios still need it for proper item generation timing
- ❌ **Don't ignore the view's loaded state** - The lifecycle context
(initial vs. runtime) is critical for correct timing

### Edge Cases

| Scenario | Risk | Mitigation |
|----------|------|------------|
| EmptySource disposal | Medium | Runtime path checks `ItemsSource is
EmptySource` before selection |
| ItemsSource changes during selection | Medium | Runtime path verifies
`ReferenceEquals(ItemsView.ItemsSource, originalSource)` |
| Collection mutations (add/delete) | Medium | Runtime path recalculates
index and verifies item equality at updated position |
| Initial preselection timing | Low | Preserved PerformBatchUpdates for
!IsLoaded() case |

### Issues Fixed

Fixes #30363
Fixes #26187

### Regression PR

This fix addresses a regression introduced in PR #25555, which added
`PerformBatchUpdates` to ensure selection timing but didn't account for
runtime selection clearing scenarios.

### Platforms Tested

- [x] iOS
- [x] Mac
- [x] Android
- [x] Windows

### Screenshots

**Issue #30363:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/09891481-5e3a-476d-a058-b6f828335a63">
| <video
src="https://github.com/user-attachments/assets/6bad46a2-acbf-498a-a45c-e08c84f4a32a">
|

**Issue #26187:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/95245bc1-5772-4cc1-9947-c371a4c35586">
| <video
src="https://github.com/user-attachments/assets/1474b60e-d552-4a05-9461-fb513e3ef5b0">
|
@PureWeen PureWeen mentioned this pull request Mar 17, 2026
PureWeen pushed a commit that referenced this pull request Mar 19, 2026
…is set to null (#30420)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!


### Root Cause

CollectionView.SelectItem was being called inside the
`PerformBatchUpdates` completion handler, which is triggered after all
other actions are completed. As a result, when `SelectedItem` is set to
null in the `SelectionChanged` event handler, the deferred selection
inside `PerformBatchUpdates` would fire afterward and re-select the
item, making the null assignment ineffective.

The original implementation (from PR #25555) always wrapped `SelectItem`
calls in `PerformBatchUpdates` to ensure selection happened after
collection view items generation was completed. This worked for initial
load scenarios but caused a timing issue for runtime selection changes.

### Description of Change

The fix introduces conditional logic based on the view's loaded state
using `CollectionView.IsLoaded()` (which checks if `UIView.Window !=
null`):

**For initial load (!IsLoaded()):**
- Selection still uses `PerformBatchUpdates` to defer until items are
generated
- This preserves the original intent from PR #25555

**For runtime changes (IsLoaded()):**
- Selection executes immediately without `PerformBatchUpdates` wrapper
- Includes all existing safety checks: EmptySource verification,
reference equality, index recalculation, and item equality validation
- Allows user code (like `SelectedItem = null`) to take effect
immediately without being overridden by deferred selection

This resolves the issue where the selected item was not being cleared
when `SelectedItem` is set to null during runtime.

### Key Technical Details

**IsLoaded() Extension Method:**
- Definition: `UIView.Window != null`
- Indicates whether the view is attached to the window hierarchy
- Used to distinguish between initial load (preselection) vs. runtime
selection changes

**Lifecycle Distinction:**
- **Initial load**: View isn't attached, items still being laid out →
defer selection
- **Runtime**: View is active, items stable → select immediately to
avoid race conditions

**Why Immediate Selection Works:**
Direct `SelectItem` calls execute synchronously in the current call
stack, before any completion handlers fire. This prevents the race
condition where user code sets `SelectedItem = null`, but the deferred
`PerformBatchUpdates` completion handler re-selects the item afterward.

### Files Changed

1.
`src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs`
- Added IsLoaded() check (deprecated handler)
2.
`src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs`
- Added IsLoaded() check (current handler)
3. `src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs` - New UI
test demonstrating the fix
4. `src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs` - Updated
existing test
5.
`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs` -
Appium test with screenshot verification
6. Snapshots for iOS and Android

### What NOT to Do (for future agents)

- ❌ **Don't always use PerformBatchUpdates for selection** - This causes
deferred execution that can override user code
- ❌ **Don't remove PerformBatchUpdates entirely** - Initial load
scenarios still need it for proper item generation timing
- ❌ **Don't ignore the view's loaded state** - The lifecycle context
(initial vs. runtime) is critical for correct timing

### Edge Cases

| Scenario | Risk | Mitigation |
|----------|------|------------|
| EmptySource disposal | Medium | Runtime path checks `ItemsSource is
EmptySource` before selection |
| ItemsSource changes during selection | Medium | Runtime path verifies
`ReferenceEquals(ItemsView.ItemsSource, originalSource)` |
| Collection mutations (add/delete) | Medium | Runtime path recalculates
index and verifies item equality at updated position |
| Initial preselection timing | Low | Preserved PerformBatchUpdates for
!IsLoaded() case |

### Issues Fixed

Fixes #30363
Fixes #26187

### Regression PR

This fix addresses a regression introduced in PR #25555, which added
`PerformBatchUpdates` to ensure selection timing but didn't account for
runtime selection clearing scenarios.

### Platforms Tested

- [x] iOS
- [x] Mac
- [x] Android
- [x] Windows

### Screenshots

**Issue #30363:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/09891481-5e3a-476d-a058-b6f828335a63">
| <video
src="https://github.com/user-attachments/assets/6bad46a2-acbf-498a-a45c-e08c84f4a32a">
|

**Issue #26187:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/95245bc1-5772-4cc1-9947-c371a4c35586">
| <video
src="https://github.com/user-attachments/assets/1474b60e-d552-4a05-9461-fb513e3ef5b0">
|
github-actions bot pushed a commit that referenced this pull request Mar 20, 2026
…is set to null (#30420)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!


### Root Cause

CollectionView.SelectItem was being called inside the
`PerformBatchUpdates` completion handler, which is triggered after all
other actions are completed. As a result, when `SelectedItem` is set to
null in the `SelectionChanged` event handler, the deferred selection
inside `PerformBatchUpdates` would fire afterward and re-select the
item, making the null assignment ineffective.

The original implementation (from PR #25555) always wrapped `SelectItem`
calls in `PerformBatchUpdates` to ensure selection happened after
collection view items generation was completed. This worked for initial
load scenarios but caused a timing issue for runtime selection changes.

### Description of Change

The fix introduces conditional logic based on the view's loaded state
using `CollectionView.IsLoaded()` (which checks if `UIView.Window !=
null`):

**For initial load (!IsLoaded()):**
- Selection still uses `PerformBatchUpdates` to defer until items are
generated
- This preserves the original intent from PR #25555

**For runtime changes (IsLoaded()):**
- Selection executes immediately without `PerformBatchUpdates` wrapper
- Includes all existing safety checks: EmptySource verification,
reference equality, index recalculation, and item equality validation
- Allows user code (like `SelectedItem = null`) to take effect
immediately without being overridden by deferred selection

This resolves the issue where the selected item was not being cleared
when `SelectedItem` is set to null during runtime.

### Key Technical Details

**IsLoaded() Extension Method:**
- Definition: `UIView.Window != null`
- Indicates whether the view is attached to the window hierarchy
- Used to distinguish between initial load (preselection) vs. runtime
selection changes

**Lifecycle Distinction:**
- **Initial load**: View isn't attached, items still being laid out →
defer selection
- **Runtime**: View is active, items stable → select immediately to
avoid race conditions

**Why Immediate Selection Works:**
Direct `SelectItem` calls execute synchronously in the current call
stack, before any completion handlers fire. This prevents the race
condition where user code sets `SelectedItem = null`, but the deferred
`PerformBatchUpdates` completion handler re-selects the item afterward.

### Files Changed

1.
`src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs`
- Added IsLoaded() check (deprecated handler)
2.
`src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs`
- Added IsLoaded() check (current handler)
3. `src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs` - New UI
test demonstrating the fix
4. `src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs` - Updated
existing test
5.
`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs` -
Appium test with screenshot verification
6. Snapshots for iOS and Android

### What NOT to Do (for future agents)

- ❌ **Don't always use PerformBatchUpdates for selection** - This causes
deferred execution that can override user code
- ❌ **Don't remove PerformBatchUpdates entirely** - Initial load
scenarios still need it for proper item generation timing
- ❌ **Don't ignore the view's loaded state** - The lifecycle context
(initial vs. runtime) is critical for correct timing

### Edge Cases

| Scenario | Risk | Mitigation |
|----------|------|------------|
| EmptySource disposal | Medium | Runtime path checks `ItemsSource is
EmptySource` before selection |
| ItemsSource changes during selection | Medium | Runtime path verifies
`ReferenceEquals(ItemsView.ItemsSource, originalSource)` |
| Collection mutations (add/delete) | Medium | Runtime path recalculates
index and verifies item equality at updated position |
| Initial preselection timing | Low | Preserved PerformBatchUpdates for
!IsLoaded() case |

### Issues Fixed

Fixes #30363
Fixes #26187

### Regression PR

This fix addresses a regression introduced in PR #25555, which added
`PerformBatchUpdates` to ensure selection timing but didn't account for
runtime selection clearing scenarios.

### Platforms Tested

- [x] iOS
- [x] Mac
- [x] Android
- [x] Windows

### Screenshots

**Issue #30363:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/09891481-5e3a-476d-a058-b6f828335a63">
| <video
src="https://github.com/user-attachments/assets/6bad46a2-acbf-498a-a45c-e08c84f4a32a">
|

**Issue #26187:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/95245bc1-5772-4cc1-9947-c371a4c35586">
| <video
src="https://github.com/user-attachments/assets/1474b60e-d552-4a05-9461-fb513e3ef5b0">
|
github-actions bot pushed a commit that referenced this pull request Mar 22, 2026
…is set to null (#30420)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!


### Root Cause

CollectionView.SelectItem was being called inside the
`PerformBatchUpdates` completion handler, which is triggered after all
other actions are completed. As a result, when `SelectedItem` is set to
null in the `SelectionChanged` event handler, the deferred selection
inside `PerformBatchUpdates` would fire afterward and re-select the
item, making the null assignment ineffective.

The original implementation (from PR #25555) always wrapped `SelectItem`
calls in `PerformBatchUpdates` to ensure selection happened after
collection view items generation was completed. This worked for initial
load scenarios but caused a timing issue for runtime selection changes.

### Description of Change

The fix introduces conditional logic based on the view's loaded state
using `CollectionView.IsLoaded()` (which checks if `UIView.Window !=
null`):

**For initial load (!IsLoaded()):**
- Selection still uses `PerformBatchUpdates` to defer until items are
generated
- This preserves the original intent from PR #25555

**For runtime changes (IsLoaded()):**
- Selection executes immediately without `PerformBatchUpdates` wrapper
- Includes all existing safety checks: EmptySource verification,
reference equality, index recalculation, and item equality validation
- Allows user code (like `SelectedItem = null`) to take effect
immediately without being overridden by deferred selection

This resolves the issue where the selected item was not being cleared
when `SelectedItem` is set to null during runtime.

### Key Technical Details

**IsLoaded() Extension Method:**
- Definition: `UIView.Window != null`
- Indicates whether the view is attached to the window hierarchy
- Used to distinguish between initial load (preselection) vs. runtime
selection changes

**Lifecycle Distinction:**
- **Initial load**: View isn't attached, items still being laid out →
defer selection
- **Runtime**: View is active, items stable → select immediately to
avoid race conditions

**Why Immediate Selection Works:**
Direct `SelectItem` calls execute synchronously in the current call
stack, before any completion handlers fire. This prevents the race
condition where user code sets `SelectedItem = null`, but the deferred
`PerformBatchUpdates` completion handler re-selects the item afterward.

### Files Changed

1.
`src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs`
- Added IsLoaded() check (deprecated handler)
2.
`src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs`
- Added IsLoaded() check (current handler)
3. `src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs` - New UI
test demonstrating the fix
4. `src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs` - Updated
existing test
5.
`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs` -
Appium test with screenshot verification
6. Snapshots for iOS and Android

### What NOT to Do (for future agents)

- ❌ **Don't always use PerformBatchUpdates for selection** - This causes
deferred execution that can override user code
- ❌ **Don't remove PerformBatchUpdates entirely** - Initial load
scenarios still need it for proper item generation timing
- ❌ **Don't ignore the view's loaded state** - The lifecycle context
(initial vs. runtime) is critical for correct timing

### Edge Cases

| Scenario | Risk | Mitigation |
|----------|------|------------|
| EmptySource disposal | Medium | Runtime path checks `ItemsSource is
EmptySource` before selection |
| ItemsSource changes during selection | Medium | Runtime path verifies
`ReferenceEquals(ItemsView.ItemsSource, originalSource)` |
| Collection mutations (add/delete) | Medium | Runtime path recalculates
index and verifies item equality at updated position |
| Initial preselection timing | Low | Preserved PerformBatchUpdates for
!IsLoaded() case |

### Issues Fixed

Fixes #30363
Fixes #26187

### Regression PR

This fix addresses a regression introduced in PR #25555, which added
`PerformBatchUpdates` to ensure selection timing but didn't account for
runtime selection clearing scenarios.

### Platforms Tested

- [x] iOS
- [x] Mac
- [x] Android
- [x] Windows

### Screenshots

**Issue #30363:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/09891481-5e3a-476d-a058-b6f828335a63">
| <video
src="https://github.com/user-attachments/assets/6bad46a2-acbf-498a-a45c-e08c84f4a32a">
|

**Issue #26187:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/95245bc1-5772-4cc1-9947-c371a4c35586">
| <video
src="https://github.com/user-attachments/assets/1474b60e-d552-4a05-9461-fb513e3ef5b0">
|
@kubaflo kubaflo added the s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) label Mar 23, 2026
PureWeen added a commit that referenced this pull request Mar 24, 2026
## What's Coming

.NET MAUI inflight/candidate introduces significant improvements across
all platforms with focus on quality, performance, and developer
experience. This release includes 66 commits with various improvements,
bug fixes, and enhancements.


## Activityindicator
- [Android] Implemented material3 support for ActivityIndicator by
@Dhivya-SF4094 in #33481
  <details>
  <summary>🔧 Fixes</summary>

- [Implement material3 support for
ActivityIndicator](#33479)
  </details>

- [iOS] Fix: ActivityIndicator IsRunning ignores IsVisible when set to
true by @bhavanesh2001 in #28983
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] [ActivityIndicator] `IsRunning` ignores `IsVisible` when set to
`true`](#28968)
  </details>

## Button
- [iOS] Button RTL text and image overlap - fix by @kubaflo in
#29041

## Checkbox
- [iOS/MacCatalyst] Fix CheckBox foreground color not resetting when set
to null by @Ahamed-Ali in #34284
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] Color of the checkBox control is not properly worked on dynamic
scenarios](#34278)
  </details>

## CollectionView
- [iOS] Fix: CollectionView does not clear selection when SelectedItem
is set to null by @Tamilarasan-Paranthaman in
#30420
  <details>
  <summary>🔧 Fixes</summary>

- [CollectionView not being able to remove selected item highlight on
iOS](#30363)
- [[MAUI] Select items traces are
preserved](#26187)
  </details>

- [iOS] CV2 ItemsLayout update by @kubaflo in
#28675
  <details>
  <summary>🔧 Fixes</summary>

- [CollectionView CollectionViewHandler2 doesnt change ItemsLayout on
DataTrigger](#28656)
- [iOS CollectionView doesn't respect a change to ItemsLayout when using
Items2.CollectionViewHandler2](#31259)
  </details>

- [iOS][CV2] Fix CollectionView renders large empty space at bottom of
view by @devanathan-vaithiyanathan in
#31215
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] [MacCatalyst] CollectionView renders large empty space at
bottom of view](#17799)
- [[iOS/Mac] CollectionView2 EmptyView takes up large horizontal space
even when the content is
small](#33201)
  </details>

- [iOS] Fixed issue where group Header/Footer template was set to all
items when IsGrouped was true for an ObservableCollection by
@Tamilarasan-Paranthaman in #29144
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] Group Header/Footer Repeated for All Items When IsGrouped is
True for ObservableCollection in
CollectionView](#29141)
  </details>

- [Android] Fix CollectionView selection crash with HeaderTemplate by
@NirmalKumarYuvaraj in #34275
  <details>
  <summary>🔧 Fixes</summary>

- [[Bug] [Android] System.ArgumentOutOfRangeException: Index was out of
range. Must be non-negative and less than the size of the collection.
Parameter name: index](#34247)
  </details>

## DateTimePicker
- [iOS] Fix TimePicker AM/PM frequently changes when the app is closed
and reopened by @devanathan-vaithiyanathan in
#31066
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] TimePicker AM/PM frequently changes when the app is closed and
reopened](#30837)
- [Maui 10 iOS TimePicker Strange Characters in place of
AM/PM](#33722)
  </details>

- Android TimePicker ignores 24 hour system setting when using Format
Property - fix by @kubaflo in #28797
  <details>
  <summary>🔧 Fixes</summary>

- [Android TimePicker ignores 24 hour system setting when using Format
Property](#28784)
  </details>

## Drawing
- [iOS, Mac, Windows] GraphicsView: Fix Background/BackgroundColor not
updating by @NirmalKumarYuvaraj in
#31254
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS, Mac, Windows] GraphicsView does not change the
Background/BackgroundColor](#31239)
  </details>

- [iOS] GraphicsView DrawString - fix by @kubaflo in
#26304
  <details>
  <summary>🔧 Fixes</summary>

- [DrawString not rendering in
iOS.](#24450)
- [GraphicsView DrawString not rendering in
iOS](#8486)
- [DrawString doesn't work on
maccatalyst](#4993)
  </details>

- [Android] - Fix Shadow Rendering For Transparent Fill, Stroke (Lines),
and Text on Shapes by @prakashKannanSf3972 in
#29528
  <details>
  <summary>🔧 Fixes</summary>

- [Ellipse Transparency Not Rendered When Drawing Arc Inside the Ellipse
Using GraphicsView on
Android](#29394)
  </details>

- Revert "[iOS, Mac, Windows] GraphicsView: Fix
Background/BackgroundColor not updating (#31254)" by @Ahamed-Ali via
@Copilot in #34508

## Entry
- [iOS 26] Fix Entry MaxLength not enforced due to new multi-range
delegate by @kubaflo in #32045
  <details>
  <summary>🔧 Fixes</summary>

- [iOS 26 - The MaxLength property value is not respected on an Entry
control.](#32016)
- [.NET MAUI Entry Maximum Length not working on iOS and
macOS](#33316)
  </details>

- [iOS] Fixed Entry with IsPassword toggling loses previously entered
text by @SubhikshaSf4851 in #30572
  <details>
  <summary>🔧 Fixes</summary>

- [Entry with IsPassword toggling loses previously entered text on iOS
when IsPassword is
re-enabled](#30085)
  </details>

## Essentials
- Fix for FilePicker PickMultipleAsync nullable reference type by
@SuthiYuvaraj in #33163
  <details>
  <summary>🔧 Fixes</summary>

- [FilePicker PickMultipleAsync nullable reference
type](#33114)
  </details>

- Replace deprecated NetworkReachability with NWPathMonitor on iOS/macOS
by @jfversluis via @Copilot in #32354
  <details>
  <summary>🔧 Fixes</summary>

- [NetworkReachability is obsolete on iOS/maccatalyst
17.4+](#32312)
- [Use NWPathMonitor on iOS for Essentials
Connectivity](#2574)
  </details>

## Essentials Connectivity
- Update Android Connectivity implementation to use modern APIs by
@jfversluis via @Copilot in #30348
  <details>
  <summary>🔧 Fixes</summary>

- [Update the Android Connectivity implementation to user modern
APIs](#30347)
  </details>

## Flyout
- [iOS] Fixed Flyout icon not updating when root page changes using
InsertPageBefore by @Vignesh-SF3580 in
#29924
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] Flyout icon not replaced by back button when root page is
changed using
InsertPageBefore](#29921)
  </details>

## Flyoutpage
- [iOS] Flyout Items Not Displayed in RightToLeft FlowDirection in
Landscape - fix by @kubaflo in #26762
  <details>
  <summary>🔧 Fixes</summary>

- [Flyout Items Not Displayed in RightToLeft FlowDirection on iOS in
Landscape Orientation and Hamburger Icon Positioned
Incorrectly](#26726)
  </details>

## Image
- [Android] Implemented Material3 support for Image by @Dhivya-SF4094 in
#33661
  <details>
  <summary>🔧 Fixes</summary>

- [Implement Material3 support for
Image](#33660)
  </details>

## Keyboard
- [iOS] Fix gap at top of view after rotating device while Entry
keyboard is visible by @praveenkumarkarunanithi in
#34328
  <details>
  <summary>🔧 Fixes</summary>

- [Focusing and entering texts on entry control causes a gap at the top
after rotating simulator.](#33407)
  </details>

## Label
- [Android] Support for images inside HTML label by @kubaflo in
#21679
  <details>
  <summary>🔧 Fixes</summary>

- [Label with HTML TextType does not display images on
Android](#21044)
  </details>

- [fix] ContentLabel Moved to a nested class to prevent CS0122 in
external source generators by @SubhikshaSf4851 in
#34514
  <details>
  <summary>🔧 Fixes</summary>

- [[MAUI] Building Maui App with sample content results CS0122
errors.](#34512)
  </details>

## Layout
- Optimize ordering of children in Flex layout by @symbiogenesis in
#21961

- [Android] Fix control size properties not available during Loaded
event by @Vignesh-SF3580 in #31590
  <details>
  <summary>🔧 Fixes</summary>

- [CollectionView on Android does not provide height, width, logical
children once loaded, works fine on
Windows](#14364)
- [Control's Loaded event invokes before calling its measure override
method.](#14160)
  </details>

## Mediapicker
- [iOS/Android] MediaPicker: Fix image orientation when RotateImage=true
by @michalpobuta in #33892
  <details>
  <summary>🔧 Fixes</summary>

- [MediaPicker.PickPhotosAsync does not preserve image
orientation](#32650)
  </details>

## Modal
- [Windows] Fix modal page keyboard focus not shifting to newly opened
modal by @jfversluis in #34212
  <details>
  <summary>🔧 Fixes</summary>

- [Keyboard focus does not shift to a newly opened modal page: Pressing
enter clicks the button on the page beneath the modal
page](#22938)
  </details>

## Navigation
- [iOS26] Apply view margins in title view by @kubaflo in
#32205
  <details>
  <summary>🔧 Fixes</summary>

- [NavigationPage TitleView iOS
26](#32200)
  </details>

- [iOS] System.NullReferenceException at
NavigationRenderer.SetStatusBarStyle() by @kubaflo in
#29564
  <details>
  <summary>🔧 Fixes</summary>

- [System.NullReferenceException at
NavigationRenderer.SetStatusBarStyle()](#29535)
  </details>

- [iOS 26] Fix back button color not applied for NavigationPage by
@Shalini-Ashokan in #34326
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] Color not applied to the Back button text or image on iOS
26](#33966)
  </details>

## Picker
- Fix Picker layout on Mac Catalyst 26+ by @kubaflo in
#33146
  <details>
  <summary>🔧 Fixes</summary>

- [[MacOS 26] Text on picker options are not centered on macOS
26.1](#33229)
  </details>

## Progressbar
- [Android] Implemented Material3 support for ProgressBar by
@SyedAbdulAzeemSF4852 in #33926
  <details>
  <summary>🔧 Fixes</summary>

- [Implement Material3 support for
Progressbar](#33925)
  </details>

## RadioButton
- [iOS, Mac] Fix for RadioButton TextColor for plain Content not working
by @HarishwaranVijayakumar in #31940
  <details>
  <summary>🔧 Fixes</summary>

- [RadioButton: TextColor for plain Content not working on
iOS](#18011)
  </details>

- [All Platforms] Fix RadioButton warning when ControlTemplate is set
with View content by @kubaflo in
#33839
  <details>
  <summary>🔧 Fixes</summary>

- [Seeking clarification on RadioButton + ControlTemplate + Content
documentation](#33829)
  </details>

- Visual state change for disabled RadioButton by @kubaflo in
#23471
  <details>
  <summary>🔧 Fixes</summary>

- [RadioButton disabled UI issue -
iOS](#18668)
  </details>

## SafeArea
- [Android] Fix for TabbedPage BottomNavigation BarBackgroundColor not
extending to system navigation bar by @praveenkumarkarunanithi in
#33428
  <details>
  <summary>🔧 Fixes</summary>

- [[Android] TabbedPage BottomNavigation BarBackgroundColor does not
extend to system navigation bar area in Edge-to-Edge
mode](#33344)
  </details>

## ScrollView
- [Android] ScrollView: Fix HorizontalScrollBarVisibility not updating
immediately at runtime by @SubhikshaSf4851 in
#33528
  <details>
  <summary>🔧 Fixes</summary>

- [Runtime Scrollbar visibility not updating correctly on Android and
macOS platforms.](#33400)
  </details>

- Fixed crash when calling ItemsView.ScrollTo on unloaded CollectionView
by @kubaflo in #25444
  <details>
  <summary>🔧 Fixes</summary>

- [App crashes when calling ItemsView.ScrollTo on unloaded
CollectionView](#23014)
  </details>

## Shell
- [Shell] Update logic for iOS large title display in ShellItemRenderer
by @kubaflo in #33246

- [iOS][Shell] Fix navigation lifecycle and back button for More tab (>5
tabs) by @kubaflo in #27932
  <details>
  <summary>🔧 Fixes</summary>

- [OnAppearing and OnNavigatedTo does not work when using extended
Tabbar (tabbar with more than 5 tabs) on
IOS.](#27799)
- [Shell.BackButtonBehavior does not work when using extended Tabbar
(tabbar with more than 5 tabs)on
IOS.](#27800)
- [Shell TabBar More button causes ViewModel command binding
disconnection on back
navigation](#30862)
- [Content page onappearing not firing if tabs are on the more tab on
IOS](#31166)
  </details>

- [iOS 26] Fix tab bar ghosting when navigating from modal to tabbed
Shell content by @SubhikshaSf4851 in
#34254
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] Tab bar ghosting issue on iOS 26 (liquid
glass)](#34143)
  </details>

- Fix for Shell tab visibility not updating when navigating back
multiple pages by @BagavathiPerumal in
#34403
  <details>
  <summary>🔧 Fixes</summary>

- [Changing Shell Tab Visibility when navigating back multiple pages
ignores Shell Tab
Visibility](#33351)
  </details>

- [iOS/Mac] Fixed OnBackButtonPressed not firing for Shell Navigation
Bar Button by @Dhivya-SF4094 in
#34401
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] OnBackButtonPressed not firing for Shell Navigation Bar
button](#34190)
  </details>

## Slider
- [iOS] Fix for Slider ThumbImageSource is not centered properly on iOS
26 by @HarishwaranVijayakumar in
#34019
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS 26] Slider ThumbImageSource is not centered
properly](#33967)
  </details>

- [Android] Fix improper rendering of ThumbimageSource in Slider by
@NirmalKumarYuvaraj in #34064
  <details>
  <summary>🔧 Fixes</summary>

- [[Slider] MAUI Slider thumb image is big on
android](#13258)
  </details>

## Stepper
- [iOS] Fix Stepper layout overlap in landscape on iOS 26 by
@Vignesh-SF3580 in #34325
  <details>
  <summary>🔧 Fixes</summary>

- [[.NET10] D10 - Customize cursor position - Rotating simulator makes
the button and label
overlap](#34273)
  </details>

## SwipeView
- [iOS] SwipeView: Honor FontImageSource.Color in SwipeItem icon by
@kubaflo in #27389
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS] SwipeView: SwipeItem.IconImageSource.FontImageSource color
value not honored](#27377)
  </details>

## Switch
- [Android] Fix Switch thumb shadow missing when ThumbColor is set by
@Shalini-Ashokan in #33960
  <details>
  <summary>🔧 Fixes</summary>

- [Android Switch Control Thumb
Shadow](#19676)
  </details>

## Toolbar
- [iOS/Mac Catalyst 26] Fix Shell.ForegroundColor not applied to
ToolbarItems by @SyedAbdulAzeemSF4852 in
#34085
  <details>
  <summary>🔧 Fixes</summary>

- [[iOS26] Shell.ForegroundColor is not applied to
ToolbarItems](#34083)
  </details>

- [Android] VoiceOver on Toolbar Item by @kubaflo in
#29596
  <details>
  <summary>🔧 Fixes</summary>

- [VoiceOver on Toolbar
Item](#29573)
- [SemanticProperties do not work on
ToolbarItems](#23623)
  </details>


<details>
<summary>🧪 Testing (11)</summary>

- [Testing] Additional Feature Matrix Test Cases for CollectionView by
@TamilarasanSF4853 in #32432
- [Testing] Feature Matrix UITest Cases for VisualStateManager by
@LogishaSelvarajSF4525 in #34146
- [Testing] Feature Matrix UITest Cases for Clip by @TamilarasanSF4853
in #34121
- [Testing] Feature matrix UITest Cases for Map Control by
@HarishKumarSF4517 in #31656
- [Testing] Feature matrix UITest Cases for Visual Transform Control by
@HarishKumarSF4517 in #32799
- [Testing] Feature Matrix UITest Cases for Shell Pages by
@NafeelaNazhir in #33945
- [Testing] Feature Matrix UITest Cases for Triggers by
@HarishKumarSF4517 in #34152
- [Testing] Refactoring Feature Matrix UITest Cases for CheckBox Control
by @LogishaSelvarajSF4525 in #34283
- Resolve UI test Build Sample failures - Candidate March 16 by
@Ahamed-Ali in #34442
- Fix the failures in the Candidate branch- March 16 by @Ahamed-Ali in
#34453
  <details>
  <summary>🔧 Fixes</summary>

  - [March 16th, Candidate](#34437)
  </details>
- Fixed the iOS 18.5 Candidate failures (March 16,2026) by @Ahamed-Ali
in #34593
  <details>
  <summary>🔧 Fixes</summary>

  - [March 16th, Candidate](#34437)
  </details>

</details>

<details>
<summary>📦 Other (2)</summary>

- Fixed candidate test failures caused by PR #33428. by @Ahamed-Ali in
#34515
  <details>
  <summary>🔧 Fixes</summary>

- [[.NET10] On Android, there's a big space at the top for I, M and N2 &
N3](#34509)
  </details>
- Revert "[iOS] Button RTL text and image overlap - fix (#29041)" in
b0497af

</details>

<details>
<summary>📝 Issue References</summary>

Fixes #2574, Fixes #4993, Fixes #8486, Fixes #13258, Fixes #14160, Fixes
#14364, Fixes #17799, Fixes #18011, Fixes #18668, Fixes #19676, Fixes
#21044, Fixes #22938, Fixes #23014, Fixes #23623, Fixes #24450, Fixes
#26187, Fixes #26726, Fixes #27377, Fixes #27799, Fixes #27800, Fixes
#28656, Fixes #28784, Fixes #28968, Fixes #29141, Fixes #29394, Fixes
#29535, Fixes #29573, Fixes #29921, Fixes #30085, Fixes #30347, Fixes
#30363, Fixes #30837, Fixes #30862, Fixes #31166, Fixes #31239, Fixes
#31259, Fixes #32016, Fixes #32200, Fixes #32312, Fixes #32650, Fixes
#33114, Fixes #33201, Fixes #33229, Fixes #33316, Fixes #33344, Fixes
#33351, Fixes #33400, Fixes #33407, Fixes #33479, Fixes #33660, Fixes
#33722, Fixes #33829, Fixes #33925, Fixes #33966, Fixes #33967, Fixes
#34083, Fixes #34143, Fixes #34190, Fixes #34247, Fixes #34273, Fixes
#34278, Fixes #34437, Fixes #34509, Fixes #34512

</details>

**Full Changelog**:
main...inflight/candidate
KarthikRajaKalaimani pushed a commit to KarthikRajaKalaimani/maui that referenced this pull request Mar 30, 2026
…is set to null (dotnet#30420)

<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!


### Root Cause

CollectionView.SelectItem was being called inside the
`PerformBatchUpdates` completion handler, which is triggered after all
other actions are completed. As a result, when `SelectedItem` is set to
null in the `SelectionChanged` event handler, the deferred selection
inside `PerformBatchUpdates` would fire afterward and re-select the
item, making the null assignment ineffective.

The original implementation (from PR dotnet#25555) always wrapped `SelectItem`
calls in `PerformBatchUpdates` to ensure selection happened after
collection view items generation was completed. This worked for initial
load scenarios but caused a timing issue for runtime selection changes.

### Description of Change

The fix introduces conditional logic based on the view's loaded state
using `CollectionView.IsLoaded()` (which checks if `UIView.Window !=
null`):

**For initial load (!IsLoaded()):**
- Selection still uses `PerformBatchUpdates` to defer until items are
generated
- This preserves the original intent from PR dotnet#25555

**For runtime changes (IsLoaded()):**
- Selection executes immediately without `PerformBatchUpdates` wrapper
- Includes all existing safety checks: EmptySource verification,
reference equality, index recalculation, and item equality validation
- Allows user code (like `SelectedItem = null`) to take effect
immediately without being overridden by deferred selection

This resolves the issue where the selected item was not being cleared
when `SelectedItem` is set to null during runtime.

### Key Technical Details

**IsLoaded() Extension Method:**
- Definition: `UIView.Window != null`
- Indicates whether the view is attached to the window hierarchy
- Used to distinguish between initial load (preselection) vs. runtime
selection changes

**Lifecycle Distinction:**
- **Initial load**: View isn't attached, items still being laid out →
defer selection
- **Runtime**: View is active, items stable → select immediately to
avoid race conditions

**Why Immediate Selection Works:**
Direct `SelectItem` calls execute synchronously in the current call
stack, before any completion handlers fire. This prevents the race
condition where user code sets `SelectedItem = null`, but the deferred
`PerformBatchUpdates` completion handler re-selects the item afterward.

### Files Changed

1.
`src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs`
- Added IsLoaded() check (deprecated handler)
2.
`src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs`
- Added IsLoaded() check (current handler)
3. `src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs` - New UI
test demonstrating the fix
4. `src/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs` - Updated
existing test
5.
`src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs` -
Appium test with screenshot verification
6. Snapshots for iOS and Android

### What NOT to Do (for future agents)

- ❌ **Don't always use PerformBatchUpdates for selection** - This causes
deferred execution that can override user code
- ❌ **Don't remove PerformBatchUpdates entirely** - Initial load
scenarios still need it for proper item generation timing
- ❌ **Don't ignore the view's loaded state** - The lifecycle context
(initial vs. runtime) is critical for correct timing

### Edge Cases

| Scenario | Risk | Mitigation |
|----------|------|------------|
| EmptySource disposal | Medium | Runtime path checks `ItemsSource is
EmptySource` before selection |
| ItemsSource changes during selection | Medium | Runtime path verifies
`ReferenceEquals(ItemsView.ItemsSource, originalSource)` |
| Collection mutations (add/delete) | Medium | Runtime path recalculates
index and verifies item equality at updated position |
| Initial preselection timing | Low | Preserved PerformBatchUpdates for
!IsLoaded() case |

### Issues Fixed

Fixes dotnet#30363
Fixes dotnet#26187

### Regression PR

This fix addresses a regression introduced in PR dotnet#25555, which added
`PerformBatchUpdates` to ensure selection timing but didn't account for
runtime selection clearing scenarios.

### Platforms Tested

- [x] iOS
- [x] Mac
- [x] Android
- [x] Windows

### Screenshots

**Issue dotnet#30363:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/09891481-5e3a-476d-a058-b6f828335a63">
| <video
src="https://github.com/user-attachments/assets/6bad46a2-acbf-498a-a45c-e08c84f4a32a">
|

**Issue dotnet#26187:**

| Before Fix | After Fix |
|----------|----------|
| <video
src="https://github.com/user-attachments/assets/95245bc1-5772-4cc1-9947-c371a4c35586">
| <video
src="https://github.com/user-attachments/assets/1474b60e-d552-4a05-9461-fb513e3ef5b0">
|
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-controls-collectionview CollectionView, CarouselView, IndicatorView collectionview-cv2 community ✨ Community Contribution partner/syncfusion Issues / PR's with Syncfusion collaboration platform/ios s/agent-fix-implemented PR author implemented the agent suggested fix s/agent-fix-win AI found a better alternative fix than the PR s/agent-review-incomplete AI agent could not complete all phases (blocker, timeout, error) s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) s/agent-suggestions-implemented Maintainer applies when PR author adopts agent's recommendation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CollectionView not being able to remove selected item highlight on iOS [MAUI] Select items traces are preserved

8 participants