[iOS] Fix: CollectionView does not clear selection when SelectedItem is set to null#30420
Conversation
bhavanesh2001
left a comment
There was a problem hiding this comment.
#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()) |
There was a problem hiding this comment.
Won't this prevent item selection, if SelectedItem is pre-defined? please verify
There was a problem hiding this comment.
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.
|
/rebase |
830b23d to
3f227f5
Compare
src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs
Show resolved
Hide resolved
3f227f5 to
d5cac86
Compare
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 30420Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 30420" |
There was a problem hiding this comment.
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
PerformBatchUpdateswhen 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 |
d5cac86 to
bc8629b
Compare
kubaflo
left a comment
There was a problem hiding this comment.
🤖 AI Summary
📊 Expand Full Review
🔍 Pre-Flight — Context & Validation
📝 Review Session — added 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.SelectItemwas always called insidePerformBatchUpdatescompletion handler- This completion handler fires after all other actions complete
- When user sets
SelectedItem = nullinSelectionChanged, the deferred batch update re-selects the item afterward, making null assignment ineffective
Fix Approach (per PR):
- Add
IsLoaded()check (checksUIView.Window != null) to distinguish initial load vs runtime - For initial load (
!IsLoaded()): KeepPerformBatchUpdatesto defer until items are generated - For runtime (
IsLoaded()): DirectSelectItemcall 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 Session — added 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.cssrc/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs
🔧 Fix — Analysis & Comparison
📝 Review Session — added 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 Session — added 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 = nullinSelectionChanged26187) - Root cause confirmed:
SelectItemwas wrapped inPerformBatchUpdatescompletion, which fires AFTERSelectionChanged. SettingSelectedItem = nullinSelectionChangedhad 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.csandItems2/iOS/SelectableItemsViewController2.cs - Tests added:
Issue30363.csin 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:
if (ItemsSource is EmptySource) return;if (!ReferenceEquals(ItemsView.ItemsSource, originalSource)) return;- Recalculate
updatedIndexafter delay if (updatedIndex == null) return;- 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.cssrc/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cssrc/Controls/tests/TestCases.HostApp/Issues/Issue30363.cssrc/Controls/tests/TestCases.HostApp/Issues/Issue26187.cssrc/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):
if (ItemsSource is EmptySource) return;if (!ReferenceEquals(ItemsView.ItemsSource, originalSource)) return;- Index recalculation after source changes
- 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/andItems2/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.csfiles 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. |
…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"> |
…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"> |
…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"> |
…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"> |
…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"> |
…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"> |
…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"> |
## 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
…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"> |
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
PerformBatchUpdatescompletion handler, which is triggered after all other actions are completed. As a result, whenSelectedItemis set to null in theSelectionChangedevent handler, the deferred selection insidePerformBatchUpdateswould fire afterward and re-select the item, making the null assignment ineffective.The original implementation (from PR #25555) always wrapped
SelectItemcalls inPerformBatchUpdatesto 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 ifUIView.Window != null):For initial load (!IsLoaded()):
PerformBatchUpdatesto defer until items are generatedFor runtime changes (IsLoaded()):
PerformBatchUpdateswrapperSelectedItem = null) to take effect immediately without being overridden by deferred selectionThis resolves the issue where the selected item was not being cleared when
SelectedItemis set to null during runtime.Key Technical Details
IsLoaded() Extension Method:
UIView.Window != nullLifecycle Distinction:
Why Immediate Selection Works:
Direct
SelectItemcalls execute synchronously in the current call stack, before any completion handlers fire. This prevents the race condition where user code setsSelectedItem = null, but the deferredPerformBatchUpdatescompletion handler re-selects the item afterward.Files Changed
src/Controls/src/Core/Handlers/Items/iOS/SelectableItemsViewController.cs- Added IsLoaded() check (deprecated handler)src/Controls/src/Core/Handlers/Items2/iOS/SelectableItemsViewController2.cs- Added IsLoaded() check (current handler)src/Controls/tests/TestCases.HostApp/Issues/Issue30363.cs- New UI test demonstrating the fixsrc/Controls/tests/TestCases.HostApp/Issues/Issue26187.cs- Updated existing testsrc/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue30363.cs- Appium test with screenshot verificationWhat NOT to Do (for future agents)
Edge Cases
ItemsSource is EmptySourcebefore selectionReferenceEquals(ItemsView.ItemsSource, originalSource)Issues Fixed
Fixes #30363
Fixes #26187
Regression PR
This fix addresses a regression introduced in PR #25555, which added
PerformBatchUpdatesto ensure selection timing but didn't account for runtime selection clearing scenarios.Platforms Tested
Screenshots
Issue #30363:
30363-Before-Fix.mov
30363-After-Fix.mov
Issue #26187:
26187-Before-Fix.mov
26187-After-Fix.mov