ScrollView's ScrollToAsync doesn't complete when called thrice with the same value#27300
ScrollView's ScrollToAsync doesn't complete when called thrice with the same value#27300KarthikRajaKalaimani wants to merge 4 commits intodotnet:mainfrom
Conversation
|
@dotnet-policy-service agree company="Syncfusion, Inc." |
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
|
Hi guys, Any update on this PR? Thank you. |
|
/rebase |
306ee52 to
60d08d1
Compare
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
60d08d1 to
11a48e6
Compare
11a48e6 to
44d1865
Compare
|
/rebase |
44d1865 to
ce1d567
Compare
…s extraction (#33813) <!-- 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! ## Summary This PR significantly enhances the PR agent workflow with improvements across **two development phases**: ### Phase 1: Original Enhancements (Commits 1-8) 1. **Multi-model try-fix exploration** - Uses 5 different AI models to explore alternative solutions 2. **Environment blocker handling** - Strict rules to stop and ask user when environment issues occur 3. **Review automation script** - PowerShell script that invokes Copilot CLI directly for PR reviews 4. **Branch safety rules** - Prevents agent from switching branches during reviews 5. **Template formatting rules** - Exact template adherence for downstream script compatibility ### Phase 2: Consolidation & Simplification (Commit 9) After multi-model review (5 models: Claude Sonnet 4.5, Claude Opus 4.5, GPT-5.2, GPT-5.2-Codex, Gemini 3 Pro), the following improvements were made: 6. **Shared rules extraction** - Created `SHARED-RULES.md` to eliminate duplication across files 7. **Simplified git policy** - Agent never runs git commands; always assumes correct branch 8. **State file handling** - Changed "commit" to "save" (state files are gitignored) 9. **Reduced verbosity** - Compressed cross-pollination section, converted PLAN-TEMPLATE to checklist --- ## Commits ### 1. `80d7e412c2` - Update pr agent with multi-model try-fix workflow **Why:** The original PR agent only used a single model for exploring fixes. Different AI models have different strengths and may find solutions others miss. **Changes:** - Added Phase 4 multi-model workflow using 5 models: - `claude-sonnet-4.5`, `claude-opus-4.5`, `gpt-5.2`, `gpt-5.2-codex`, `gemini-3-pro-preview` - Cross-pollination loop: Share results between models to spark new ideas - Continue until all models confirm "no more approaches to explore" --- ### 2. `69cc6af403` - Address Copilot review suggestions **Why:** Initial PR review feedback suggested improvements. **Changes:** - Minor formatting and clarity improvements to pr.md --- ### 3. `fe55c3fd21` - Add rules for template formatting and skill script usage **Why:** Downstream scripts depend on exact regex patterns in state files. Agents were "improving" templates by adding attributes like `open` which broke parsing. **Changes:** - Added "Follow Templates EXACTLY" rule - no adding attributes, no improving formats - Added "Use Skills' Scripts" rule - run provided PowerShell scripts, don't bypass with manual commands --- ### 4. `debbee608e` - Add 'Stop on Environment Blockers' rule to PR agent **Why:** Agent was continuing through phases when environment issues (missing Appium, WinAppDriver errors) prevented completion, leading to incomplete reviews. **Changes:** - Added explicit blocker handling section to pr.md - Common blockers: Appium drivers, WinAppDriver, Xcode, emulators, port conflicts - Must STOP, report the blocker, and ask user how to proceed - Never mark phase as blocked and continue to next phase --- ### 5. `ad29f6a796` - Add PR review plan template and Review-PR.ps1 script **Why:** Need a reusable template for consistent PR reviews and a script to automate invocation. **Changes:** - Created `.github/agents/pr/PLAN-TEMPLATE.md` - Reusable 5-phase review plan - Created `.github/scripts/Review-PR.ps1` - Script to prepare environment and invoke Copilot CLI --- ### 6. `886ea2aa8e` - Improve blocker handling and fix Review-PR.ps1 for Copilot CLI **Why:** During PR #27300 review, agent spent 10+ tool calls troubleshooting WinAppDriver instead of stopping after first failure. **Changes:** - Added strict retry limits table: | Blocker Type | Max Retries | Action | |--------------|-------------|--------| | Server errors (500, timeout) | 0 | STOP immediately | | Missing tools | 1 install | STOP and ask | | Port conflicts | 1 kill | STOP and ask | | WinAppDriver errors | 0 | STOP immediately | - Added "What I tried" section to blocker report template - New prohibitions: Never spend more than 2-3 tool calls on same blocker --- ### 7. `d67da75e85` - Update Review-PR.ps1 to invoke Copilot CLI directly **Why:** Initially thought Copilot CLI was interactive-only. Discovered it supports `-i <prompt>` and `-p <prompt>` for programmatic invocation. **Changes:** - Script now invokes `copilot --agent pr -i "<prompt>"` directly - Validates both `gh` CLI and `copilot` CLI are installed - New `-NoInteractive` switch for `-p` mode (exits after completion) - Dry run mode shows exactly what would be invoked --- ### 8. `ed74c574a5` - Add 'Do NOT Switch Branches' rule to pr agent **Why:** During PR review testing, the pr agent ran `git checkout`, `git stash`, and other branch-switching commands, causing loss of local changes and confusion about which code was being reviewed. **Changes:** - Added explicit "Do NOT Switch Branches" rule to both pr.md and PLAN-TEMPLATE.md - Forbidden commands: `git checkout`, `git switch`, `gh pr checkout`, `git stash` - Agent must work on current branch as-is, using `git diff` or `gh pr diff` to see PR changes - Fixed variable expansion in Review-PR.ps1 prompt (double backticks for here-strings) --- ### 9. `632bfb7155` - Extract shared rules, simplify git policy, reduce duplication **Why:** Multi-model review (5 AI models) identified significant duplication (~200 lines) across files, conflicting "commit" terminology, and overly verbose sections. The git checkout prohibition also conflicted with workflow steps that mentioned git checkout. **Changes:** - **Created `SHARED-RULES.md`** (167 lines) - Single source of truth for: - Phase Completion Protocol - Follow Templates EXACTLY - No Direct Git Commands (absolute - agent never runs git) - Use Skills' Scripts - Stop on Environment Blockers (with retry limits) - Multi-Model Configuration (5 models list) - Platform Selection guidance - **Simplified git policy** - Agent is ALWAYS on correct branch, never runs git commands: - Removed git fetch/checkout from Phase 1 Pre-Flight - Phase 5: User handles commit/push/PR creation - Changed all "State file committed" → "State file saved" (gitignored files) - **Compressed content**: - Cross-pollination ASCII box: 51 → 20 lines - PLAN-TEMPLATE.md: Full docs → Pure checklist (226 → 112 lines) - pr.md: 662 → 535 lines (-19%) - post-gate.md: 403 → 302 lines (-25%) - Total reduction: 1291 → 1116 lines (-14%) - **Eliminated duplication**: - Blocker handling was in 3 files → now in SHARED-RULES.md only - Phase Completion Protocol was in 2 files → now in SHARED-RULES.md only - Model list was in 3 files → now in SHARED-RULES.md only --- ## Files Changed | File | Purpose | |------|---------| | `.github/agents/pr.md` | Main PR agent instructions (Phases 1-3) | | `.github/agents/pr/post-gate.md` | Phase 4-5 instructions (multi-model try-fix) | | `.github/agents/pr/PLAN-TEMPLATE.md` | **NEW** - Reusable 5-phase review checklist | | `.github/agents/pr/SHARED-RULES.md` | **NEW** - Extracted shared rules (single source of truth) | | `.github/scripts/Review-PR.ps1` | **NEW** - Script to invoke Copilot CLI for PR review | --- ## Usage ```powershell # Interactive mode (default) - stays open for follow-up pwsh .github/scripts/Review-PR.ps1 -PRNumber 33687 # Non-interactive mode - exits when done pwsh .github/scripts/Review-PR.ps1 -PRNumber 33687 -NoInteractive # Specific platform pwsh .github/scripts/Review-PR.ps1 -PRNumber 33687 -Platform ios # Skip merge if already on branch pwsh .github/scripts/Review-PR.ps1 -PRNumber 33687 -SkipMerge # Dry run to preview pwsh .github/scripts/Review-PR.ps1 -PRNumber 33687 -DryRun ``` --- ## Multi-Model Validation The final changes (commit 9) were validated by 5 AI models: | Model | Verdict | Key Feedback | |-------|---------|--------------| | Claude Sonnet 4.5 | ✅ READY TO MERGE | "All git command instructions successfully removed" | | Claude Opus 4.5 | ✅ READY TO MERGE | "Excellent refactoring, no conflicting guidance" | | GPT-5.2 | ✅ READY TO MERGE | "Progressive disclosure maintained" | | GPT-5.2-Codex | ✅ READY TO MERGE | "No instructions to run git commands remain" | | Gemini 3 Pro | ✅ READY TO MERGE | "Agent is instructed to STOP and ask user for commits" | --- ## Testing Tested by reviewing PR #27300 (ScrollView ScrollToAsync fix): - Pre-Flight phase completed successfully - Tests phase verified test files exist - Gate phase encountered WinAppDriver blocker → agent correctly stopped and asked - Blocker handling rules validated through real-world usage - **Branch safety verified**: Agent stayed on branch instead of switching --------- Co-authored-by: Jakub Florkowski <kubaflo123@gmail.com>
…s extraction (#33813) <!-- 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! ## Summary This PR significantly enhances the PR agent workflow with improvements across **two development phases**: ### Phase 1: Original Enhancements (Commits 1-8) 1. **Multi-model try-fix exploration** - Uses 5 different AI models to explore alternative solutions 2. **Environment blocker handling** - Strict rules to stop and ask user when environment issues occur 3. **Review automation script** - PowerShell script that invokes Copilot CLI directly for PR reviews 4. **Branch safety rules** - Prevents agent from switching branches during reviews 5. **Template formatting rules** - Exact template adherence for downstream script compatibility ### Phase 2: Consolidation & Simplification (Commit 9) After multi-model review (5 models: Claude Sonnet 4.5, Claude Opus 4.5, GPT-5.2, GPT-5.2-Codex, Gemini 3 Pro), the following improvements were made: 6. **Shared rules extraction** - Created `SHARED-RULES.md` to eliminate duplication across files 7. **Simplified git policy** - Agent never runs git commands; always assumes correct branch 8. **State file handling** - Changed "commit" to "save" (state files are gitignored) 9. **Reduced verbosity** - Compressed cross-pollination section, converted PLAN-TEMPLATE to checklist --- ## Commits ### 1. `80d7e412c2` - Update pr agent with multi-model try-fix workflow **Why:** The original PR agent only used a single model for exploring fixes. Different AI models have different strengths and may find solutions others miss. **Changes:** - Added Phase 4 multi-model workflow using 5 models: - `claude-sonnet-4.5`, `claude-opus-4.5`, `gpt-5.2`, `gpt-5.2-codex`, `gemini-3-pro-preview` - Cross-pollination loop: Share results between models to spark new ideas - Continue until all models confirm "no more approaches to explore" --- ### 2. `69cc6af403` - Address Copilot review suggestions **Why:** Initial PR review feedback suggested improvements. **Changes:** - Minor formatting and clarity improvements to pr.md --- ### 3. `fe55c3fd21` - Add rules for template formatting and skill script usage **Why:** Downstream scripts depend on exact regex patterns in state files. Agents were "improving" templates by adding attributes like `open` which broke parsing. **Changes:** - Added "Follow Templates EXACTLY" rule - no adding attributes, no improving formats - Added "Use Skills' Scripts" rule - run provided PowerShell scripts, don't bypass with manual commands --- ### 4. `debbee608e` - Add 'Stop on Environment Blockers' rule to PR agent **Why:** Agent was continuing through phases when environment issues (missing Appium, WinAppDriver errors) prevented completion, leading to incomplete reviews. **Changes:** - Added explicit blocker handling section to pr.md - Common blockers: Appium drivers, WinAppDriver, Xcode, emulators, port conflicts - Must STOP, report the blocker, and ask user how to proceed - Never mark phase as blocked and continue to next phase --- ### 5. `ad29f6a796` - Add PR review plan template and Review-PR.ps1 script **Why:** Need a reusable template for consistent PR reviews and a script to automate invocation. **Changes:** - Created `.github/agents/pr/PLAN-TEMPLATE.md` - Reusable 5-phase review plan - Created `.github/scripts/Review-PR.ps1` - Script to prepare environment and invoke Copilot CLI --- ### 6. `886ea2aa8e` - Improve blocker handling and fix Review-PR.ps1 for Copilot CLI **Why:** During PR #27300 review, agent spent 10+ tool calls troubleshooting WinAppDriver instead of stopping after first failure. **Changes:** - Added strict retry limits table: | Blocker Type | Max Retries | Action | |--------------|-------------|--------| | Server errors (500, timeout) | 0 | STOP immediately | | Missing tools | 1 install | STOP and ask | | Port conflicts | 1 kill | STOP and ask | | WinAppDriver errors | 0 | STOP immediately | - Added "What I tried" section to blocker report template - New prohibitions: Never spend more than 2-3 tool calls on same blocker --- ### 7. `d67da75e85` - Update Review-PR.ps1 to invoke Copilot CLI directly **Why:** Initially thought Copilot CLI was interactive-only. Discovered it supports `-i <prompt>` and `-p <prompt>` for programmatic invocation. **Changes:** - Script now invokes `copilot --agent pr -i "<prompt>"` directly - Validates both `gh` CLI and `copilot` CLI are installed - New `-NoInteractive` switch for `-p` mode (exits after completion) - Dry run mode shows exactly what would be invoked --- ### 8. `ed74c574a5` - Add 'Do NOT Switch Branches' rule to pr agent **Why:** During PR review testing, the pr agent ran `git checkout`, `git stash`, and other branch-switching commands, causing loss of local changes and confusion about which code was being reviewed. **Changes:** - Added explicit "Do NOT Switch Branches" rule to both pr.md and PLAN-TEMPLATE.md - Forbidden commands: `git checkout`, `git switch`, `gh pr checkout`, `git stash` - Agent must work on current branch as-is, using `git diff` or `gh pr diff` to see PR changes - Fixed variable expansion in Review-PR.ps1 prompt (double backticks for here-strings) --- ### 9. `632bfb7155` - Extract shared rules, simplify git policy, reduce duplication **Why:** Multi-model review (5 AI models) identified significant duplication (~200 lines) across files, conflicting "commit" terminology, and overly verbose sections. The git checkout prohibition also conflicted with workflow steps that mentioned git checkout. **Changes:** - **Created `SHARED-RULES.md`** (167 lines) - Single source of truth for: - Phase Completion Protocol - Follow Templates EXACTLY - No Direct Git Commands (absolute - agent never runs git) - Use Skills' Scripts - Stop on Environment Blockers (with retry limits) - Multi-Model Configuration (5 models list) - Platform Selection guidance - **Simplified git policy** - Agent is ALWAYS on correct branch, never runs git commands: - Removed git fetch/checkout from Phase 1 Pre-Flight - Phase 5: User handles commit/push/PR creation - Changed all "State file committed" → "State file saved" (gitignored files) - **Compressed content**: - Cross-pollination ASCII box: 51 → 20 lines - PLAN-TEMPLATE.md: Full docs → Pure checklist (226 → 112 lines) - pr.md: 662 → 535 lines (-19%) - post-gate.md: 403 → 302 lines (-25%) - Total reduction: 1291 → 1116 lines (-14%) - **Eliminated duplication**: - Blocker handling was in 3 files → now in SHARED-RULES.md only - Phase Completion Protocol was in 2 files → now in SHARED-RULES.md only - Model list was in 3 files → now in SHARED-RULES.md only --- ## Files Changed | File | Purpose | |------|---------| | `.github/agents/pr.md` | Main PR agent instructions (Phases 1-3) | | `.github/agents/pr/post-gate.md` | Phase 4-5 instructions (multi-model try-fix) | | `.github/agents/pr/PLAN-TEMPLATE.md` | **NEW** - Reusable 5-phase review checklist | | `.github/agents/pr/SHARED-RULES.md` | **NEW** - Extracted shared rules (single source of truth) | | `.github/scripts/Review-PR.ps1` | **NEW** - Script to invoke Copilot CLI for PR review | --- ## Usage ```powershell # Interactive mode (default) - stays open for follow-up pwsh .github/scripts/Review-PR.ps1 -PRNumber 33687 # Non-interactive mode - exits when done pwsh .github/scripts/Review-PR.ps1 -PRNumber 33687 -NoInteractive # Specific platform pwsh .github/scripts/Review-PR.ps1 -PRNumber 33687 -Platform ios # Skip merge if already on branch pwsh .github/scripts/Review-PR.ps1 -PRNumber 33687 -SkipMerge # Dry run to preview pwsh .github/scripts/Review-PR.ps1 -PRNumber 33687 -DryRun ``` --- ## Multi-Model Validation The final changes (commit 9) were validated by 5 AI models: | Model | Verdict | Key Feedback | |-------|---------|--------------| | Claude Sonnet 4.5 | ✅ READY TO MERGE | "All git command instructions successfully removed" | | Claude Opus 4.5 | ✅ READY TO MERGE | "Excellent refactoring, no conflicting guidance" | | GPT-5.2 | ✅ READY TO MERGE | "Progressive disclosure maintained" | | GPT-5.2-Codex | ✅ READY TO MERGE | "No instructions to run git commands remain" | | Gemini 3 Pro | ✅ READY TO MERGE | "Agent is instructed to STOP and ask user for commits" | --- ## Testing Tested by reviewing PR #27300 (ScrollView ScrollToAsync fix): - Pre-Flight phase completed successfully - Tests phase verified test files exist - Gate phase encountered WinAppDriver blocker → agent correctly stopped and asked - Blocker handling rules validated through real-world usage - **Branch safety verified**: Agent stayed on branch instead of switching --------- Co-authored-by: Jakub Florkowski <kubaflo123@gmail.com>
jfversluis
left a comment
There was a problem hiding this comment.
Code Review — PR #27300
Thanks for the fix! The diagnosis is correct — ScrollToAsync hangs when the ScrollView is already at the target position because the platform scroll-complete event never fires. The approach of calling ScrollFinished() in this case is the right solution.
However, the position check needs to happen before the scroll call on both platforms, not after. Here's why and what to change:
🔴 Windows: Race condition with async ChangeView
ChangeView() is asynchronous — it returns immediately while the actual scroll happens later. Checking offsets after ChangeView reads potentially stale/intermediate values. Additionally, if the position is already at the target, ChangeView may still fire ViewChanged → double ScrollFinished() call.
Current code (problematic):
handler.PlatformView.ChangeView(request.HorizontalOffset, request.VerticalOffset, null, request.Instant);
if(request.VerticalOffset == handler.PlatformView.VerticalOffset && request.HorizontalOffset == handler.PlatformView.HorizontalOffset)
{
handler.VirtualView.ScrollFinished();
}Suggested fix — check before, early return:
if (args is ScrollToRequest request)
{
if (request.VerticalOffset == handler.PlatformView.VerticalOffset
&& request.HorizontalOffset == handler.PlatformView.HorizontalOffset)
{
handler.VirtualView.ScrollFinished();
return;
}
handler.PlatformView.ChangeView(request.HorizontalOffset, request.VerticalOffset, null, request.Instant);
}This avoids calling ChangeView entirely when already at target — no race, no double-fire.
🔴 iOS: SetContentOffset to same position has undefined animation callback behavior
Calling SetContentOffset with animated: true when already at the target position may or may not fire scrollViewDidEndScrollingAnimation depending on iOS version. This means ScrollFinished() could be called twice — once from your new check and once from the animation callback.
Suggested fix — check before, skip SetContentOffset:
var minScrollHorizontal = Math.Clamp(request.HorizontalOffset, 0, availableScrollWidth);
var minScrollVertical = Math.Clamp(request.VerticalOffset, 0, availableScrollHeight);
bool alreadyAtTarget = uiScrollView.ContentOffset.Y == minScrollVertical
&& uiScrollView.ContentOffset.X == minScrollHorizontal;
if (!alreadyAtTarget)
uiScrollView.SetContentOffset(new CGPoint(minScrollHorizontal, minScrollVertical), !request.Instant);
if (request.Instant || alreadyAtTarget)
{
scrollView.ScrollFinished();
}This skips the SetContentOffset call entirely when already at target, eliminating ambiguous UIKit behavior.
🟡 Minor: Floating-point equality
Both platforms use == on doubles for position comparison. This works for the common case (exact integer values like scrolling to Y=100 twice), but could produce false negatives at content boundaries due to floating-point precision. Consider using a tolerance:
Math.Abs(request.VerticalOffset - handler.PlatformView.VerticalOffset) < 0.5🟡 Minor: Indentation
The Windows change uses 3-space indentation while the file uses tabs. Running dotnet format should fix this.
Summary
The fix direction is correct and the ScrollToYTwice test re-enablement is appropriate. Just needs the position checks moved before the platform scroll calls to avoid race conditions and double-fire scenarios. ScrollFinished() itself is safe to double-call (TrySetResult), but the cleaner pattern avoids the issue entirely.
…with the same value Fixed ScrollView's ScrollToAsync doesn't complete when called thrice with the same value.
ScrollView's ScrollToAsync doesn't complete when called thrice with the same value
Test case enabled for this issue
ce1d567 to
18ee050
Compare
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 27300Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 27300" |
I have modified the fix as per the suggestion. |
🤖 AI Summary📊 Expand Full Review🔍 Pre-Flight — Context & Validation📝 Review Session — Modified the fix ·
|
| Issue | Reviewer Says | Current Status |
|---|---|---|
| Windows race condition | Check before ChangeView, not after |
✅ Fixed in current code |
| iOS double-fire risk | Skip SetContentOffset when already at target |
✅ Fixed in current code |
| Floating-point equality | Use tolerance instead of == |
==) |
| Indentation | Windows/iOS use mixed indentation |
Files Changed
Fix files:
src/Core/src/Handlers/ScrollView/ScrollViewHandler.Windows.cs- Early return if already at target before callingChangeViewsrc/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs-alreadyAtTargetcheck skipsSetContentOffsetand callsScrollFinished()directly
Test files:
src/Controls/tests/TestCases.Shared.Tests/Tests/ScrollViewUITests.cs- Removed 3[FailsOn...]attributes fromScrollToYTwicetest
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #27300 | Check position before platform scroll; call ScrollFinished() if already at target | ⏳ PENDING (Gate) | ScrollViewHandler.iOS.cs, ScrollViewHandler.Windows.cs |
Updated per reviewer suggestions |
🚦 Gate — Test Verification
📝 Review Session — Modified the fix · 4ce7857
Result: ✅ PASSED
Platform: ios
Mode: Full Verification
Test Filter: ScrollToYTwice
- Tests FAIL without fix ✅ (bug correctly detected)
- Tests PASS with fix ✅ (fix correctly resolves the bug)
Fix files validated:
src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cssrc/Core/src/Handlers/ScrollView/ScrollViewHandler.Windows.cs
🔧 Fix — Analysis & Comparison
📝 Review Session — Modified the fix · 4ce7857
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #27300 | Pre-check platform offsets in iOS/Windows handlers; call ScrollFinished() if already at target; skip scroll call | ✅ PASS (Gate) | ScrollViewHandler.iOS.cs, ScrollViewHandler.Windows.cs |
Original PR - validated by Gate |
| 1 | claude-sonnet-4.6 | Cross-platform fix in ScrollView.cs: check ScrollX/ScrollY before invoking handler | ✅ PASS | ScrollView.cs (+12 lines) |
Single fix covers all platforms |
| 2 | claude-opus-4.6 | Timeout safety net (1s) on TaskCompletionSource in ScrollView.cs | ✅ PASS | ScrollView.cs (+9 lines) |
Adds up to 1s latency — not ideal for production |
| 3 | gpt-5.2 | CATransaction completion block on iOS + ChangeView return value check on Windows | ✅ PASS | iOS + Windows handlers | CATransaction completion fires even for no-op |
| 4 | gpt-5.3-codex | Post-call reconciliation: check if scroll position changed after calling scroll method | ✅ PASS | iOS + Windows handlers | Similar to pre-Reviewer-suggestion original PR code |
| 5 | gemini-3-pro | Post-SetContentOffset DispatchAsync to inspect Layer.AnimationKeys | ✅ PASS | ScrollViewHandler.iOS.cs |
Has race condition risk; complex |
| 6 | gemini-3-pro | Force animated=false when target matches current position in ScrollView.cs | ✅ PASS | ScrollView.cs (+7 lines) |
Elegant variant of Attempt 1 |
| 7 | gpt-5.3-codex | Coalesce duplicate requests: return same in-flight Task if same target | ✅ PASS | ScrollView.cs (+16 lines) |
Changes semantics: multiple callers share one Task |
| 8 | claude-sonnet-4.6 | Per-request TCS embedded in ScrollToRequest; handlers complete that TCS directly | ✅ PASS | ScrollToRequest.cs, ScrollView.cs, ScrollViewHandler.iOS.cs |
Architecturally clean; large change |
| 9 | claude-opus-4.6 | Replace SetContentOffset(animated:true) with UIView.Animate() - guaranteed callback | ✅ PASS | ScrollViewHandler.iOS.cs |
Changes scroll animation behavior on iOS |
| 10 | gpt-5.2 | Post-SetContentOffset DispatchAsync stability observer | ✅ PASS | ScrollViewHandler.iOS.cs |
Variant of Attempt 5; same race risk |
| 11 | gemini-3-pro | Clamped target resolution: clamp offsets to valid range on BOTH iOS and Windows before comparing | ✅ PASS | iOS + Windows handlers | Improves PR's Windows fix by clamping |
Exhausted: Yes — Round 5 cross-pollination: 4/5 models said NO NEW IDEAS; 1 remaining model (gpt-5.2) proposed CADisplayLink and UIViewPropertyAnimator which are variants of Attempts 5/9 respectively.
Selected Fix: PR's fix — The PR's platform-handler approach is the most appropriate fix at the correct abstraction level. It directly addresses the root cause where the platform scroll callback doesn't fire, using actual platform scroll position values (more accurate than cross-platform ScrollX/ScrollY). The iOS fix is complete and correct with proper clamping.
Key Findings
- All 11 alternative approaches passed, confirming the fix direction is sound
- The PR's iOS fix correctly clamps the target value before comparing (avoiding floating-point issues)
- The PR's Windows fix uses raw request values (not clamped) - minor potential edge case
- Both platform files have indentation issues that need formatting
Cross-Pollination Summary
| Round | claude-sonnet | claude-opus | gpt-5.2 | gpt-5.3-codex | gemini |
|---|---|---|---|---|---|
| 2 | NEW IDEA ✅ | NEW IDEA ✅ | NEW IDEA ✅ | NEW IDEA ✅ | NEW IDEA ✅ |
| 3 | NEW IDEA ✅ | NO NEW IDEAS | NEW IDEA ✅ | NEW IDEA ✅ | NEW IDEA ✅ |
| 4 | NO NEW IDEAS | NO NEW IDEAS | NEW IDEA | NO NEW IDEAS | NO NEW IDEAS |
| 5 | NO NEW IDEAS | NO NEW IDEAS | NO NEW IDEAS* | NO NEW IDEAS | NO NEW IDEAS |
*gpt-5.2 Round 5 proposed CADisplayLink (variant of Attempt 10) and UIViewPropertyAnimator (variant of Attempt 9) — not genuinely new approaches.
📋 Report — Final Recommendation
📝 Review Session — Modified the fix · 4ce7857
⚠️ Final Recommendation: REQUEST CHANGES
Summary
PR #27300 fixes ScrollToAsync hanging when called multiple times with the same target position. The fix is correct and the test passes. However, there are code quality issues that should be addressed before merge.
Root Cause
When ScrollToAsync is called with the same position the ScrollView is already at:
- On iOS:
UIScrollView.SetContentOffset(animated:true)executes but no animation starts, soscrollViewDidEndScrollingAnimationnever fires, leaving theTaskCompletionSourcepending forever. - On Windows:
ScrollViewer.ChangeView()completes but theViewChangedevent may not fire for no-op scrolls.
Fix Quality
The fix direction is correct: check if already at the target position BEFORE calling the platform scroll method, and call ScrollFinished() directly if so.
iOS fix (ScrollViewHandler.iOS.cs): ✅ Correct — properly clamps the target to valid scroll range, detects already-at-target, skips SetContentOffset, calls ScrollFinished().
Windows fix (ScrollViewHandler.Windows.cs):
- Uses raw
request.VerticalOffsetinstead of clamped values — edge case: requesting Y=2000 when max is Y=1000 won't match ScrollViewer.VerticalOffset (1000), causing ChangeView to be called unnecessarily. - 3-space indentation instead of tabs (file uses tabs elsewhere).
iOS indentation: if (!alreadyAtTarget) block uses 4 spaces instead of tabs.
Code Review Findings
🔴 Indentation Issues (both files)
The Windows change uses 3-space indentation and the iOS change uses 4-space indentation, while both files use tabs elsewhere. Running dotnet format would fix this.
// Windows - should use tabs:
- if(request.VerticalOffset == handler.PlatformView.VerticalOffset && ...)
- {
- handler.VirtualView.ScrollFinished(); // 3-space indent, not tab
- return;
- }
+ if (request.VerticalOffset == handler.PlatformView.VerticalOffset && ...)
+ {
+ handler.VirtualView.ScrollFinished(); // tab-aligned
+ return;
+ }🟡 Windows: Missing clamped value comparison
The iOS fix correctly compares against minScrollVertical (the clamped value), but the Windows fix compares against the raw request.VerticalOffset. If a user tries to scroll beyond content bounds (e.g., Y=2000 when max is 1000), the ScrollViewer.VerticalOffset would be 1000 but request.VerticalOffset is 2000 — they won't match, so ChangeView is called unnecessarily.
Suggested improvement (mirrors Attempt 11 from try-fix):
var maxHorizontal = Math.Max(handler.PlatformView.ExtentWidth - handler.PlatformView.ViewportWidth, 0);
var maxVertical = Math.Max(handler.PlatformView.ExtentHeight - handler.PlatformView.ViewportHeight, 0);
var horizontalOffset = Math.Clamp(request.HorizontalOffset, 0, maxHorizontal);
var verticalOffset = Math.Clamp(request.VerticalOffset, 0, maxVertical);
if (horizontalOffset == handler.PlatformView.HorizontalOffset && verticalOffset == handler.PlatformView.VerticalOffset)
{
handler.VirtualView.ScrollFinished();
return;
}
handler.PlatformView.ChangeView(horizontalOffset, verticalOffset, null, request.Instant);🟡 Missing space after if keyword (Windows)
if(request.VerticalOffset... should be if (request.VerticalOffset...
✅ What Looks Good
- iOS fix correctly clamps to valid scroll range before comparing
- Test re-enablement is appropriate — 3 FailsOn attributes removed
- Fix approach is minimal and targeted to the problem
Title Suggestion
[iOS][Windows] ScrollView: Fix ScrollToAsync hanging when called with same target position
Description Update Needed
- Missing required NOTE block at top
- "Description of Change" should describe the final implementation (check BEFORE calling SetContentOffset, skip the call when already at target) — the current text references the old approach
📋 Expand PR Finalization Review
Title: ✅ Good
Current: ScrollView's ScrollToAsync doesn't complete when called thrice with the same value
Description: ⚠️ Needs Update
Description needs updates. See details below.
Missing Elements:
**
Required NOTE block at top1.
Description of Change should clarify: the fix checks BEFORE calling the platform scroll API, skips the call when already at target, and calls ScrollFinished() directly2.
MacCatalyst should be explicitly noted3.
Recommendation: Add NOTE block + update Description of Change to accurately reflect the implementation. See recommended-description.md.
Phase 2: Code Review
See code-review.md for full findings.
Summary:
-
-
- iOS logic is correct and well-structured
-
- Test re-enablement is appropriate (3
FailsOn*attributes removed)
✨ Suggested PR Description
[!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
ScrollToAsync hangs when called with the same position the scroll view is already at. On iOS, UIScrollView.SetContentOffset does not trigger ScrollAnimationEnded when the content offset doesn't actually so ScrollFinished() is never called and the Task never completes. On Windows, ScrollViewer.ChangeView has the same behavior when already at the target offset.change
Description of Change
iOS (ScrollViewHandler.iOS.cs):
Before calling SetContentOffset, check whether the clamped target position already matches the current ContentOffset. If already at the target:
- Skip the
SetContentOffsetcall (no-op scroll) - Call
ScrollFinished()directly to complete the pendingTask
ScrollFinished) handles all other cases unchanged.
Windows (ScrollViewHandler.Windows.cs):
Before calling ChangeView, check whether the requested position matches the current VerticalOffset/HorizontalOffset. If already at the target, call ScrollFinished() and return early.
Tests (ScrollViewUITests.cs):
Removed three [FailsOn*WhenRunningOnXamarinUITest] attributes from ScrollToYTwice that were tracking this issue on iOS, Mac, and Windows.
Platforms Affected
- iOS (
.ios.cscovers both iOS and MacCatalyst) - MacCatalyst (via
ScrollViewHandler.iOS.cs) - Windows
Issues Fixed
Fixes #27250
Platforms Tested
- Android
- Windows
- iOS
- Mac
Code Review: ✅ Passed
Code Review: PR #27300
Windows: Missing value clamping before position comparison
File: src/Core/src/Handlers/ScrollView/ScrollViewHandler.Windows.cs
The Windows fix compares raw request.VerticalOffset and request.HorizontalOffset against the actual scroll position, without first clamping to the valid scroll range:
// Current (problematic):
if(request.VerticalOffset == handler.PlatformView.VerticalOffset &&
request.HorizontalOffset == handler.PlatformView.HorizontalOffset)Problem: If the user calls ScrollToAsync(0, 2000) when maximum vertical scroll is 1000, ScrollViewer.VerticalOffset will be 1000 (clamped by the platform) but request.VerticalOffset is 2000. They won't match, so ChangeView is called unnecessarily, and on a subsequent identical call the same mismatch the task never early-returns as expected.occurs
The iOS fix correctly clamps first:
var minScrollVertical = Math.Clamp(request.VerticalOffset, 0, availableScrollHeight);
bool alreadyAtTarget = uiScrollView.ContentOffset.Y == minScrollVertical && ...Recommended fix for Windows (mirrors iOS approach):
var maxHorizontal = Math.Max(handler.PlatformView.ExtentWidth - handler.PlatformView.ViewportWidth, 0);
var maxVertical = Math.Max(handler.PlatformView.ExtentHeight - handler.PlatformView.ViewportHeight, 0);
var horizontalOffset = Math.Clamp(request.HorizontalOffset, 0, maxHorizontal);
var verticalOffset = Math.Clamp(request.VerticalOffset, 0, maxVertical);
if (horizontalOffset == handler.PlatformView.HorizontalOffset && verticalOffset == handler.PlatformView.VerticalOffset)
{
handler.VirtualView.ScrollFinished();
return;
}
handler.PlatformView.ChangeView(horizontalOffset, verticalOffset, null, request.Instant);Formatting: Mixed indentation in iOS file
File: src/Core/src/Handlers/ScrollView/ScrollViewHandler.iOS.cs
The new if (!alreadyAtTarget) block uses spaces for indentation inside, while the rest of the file uses tabs:
if (!alreadyAtTarget)
{
uiScrollView. spaces mixed with tabsSetContentOffset(...); //
}Run dotnet format Microsoft.Maui.sln --no-restore --exclude Templates/src --exclude-diagnostics CA1822 to fix.
Formatting: Missing space after if keyword (Windows)
File: src/Core/src/Handlers/ScrollView/ScrollViewHandler.Windows.cs
// Current:
if(request.VerticalOffset == ...
// Should be:
if (request.VerticalOffset == ...Looks Good
- iOS logic is correct: Clamped values used for comparison,
SetContentOffsetskipped when unnecessary,ScrollFinished()called for both instant and already-at-target cases. - Test re-enablement is appropriate: Three
[FailsOn*WhenRunningOnXamarinUITest]attributes correctly removed fromthese were tracking this exact bug.ScrollToYTwice - Minimal, targeted change: No unrelated modifications. The fix is well-scoped to the problem.
- MacCatalyst covered:
ScrollViewHandler.iOS.cscompiles for both iOS and MacCatalyst (.ios.csconvention), so the fix covers all three affected platforms from a single file.
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!
Issue Details:
When calling ScrollToAsync thrice with the same Y value on a ScrollView, the scrolling operation does not complete on iOS, Windows, and Catalyst platforms. This behavior is inconsistent with Android, where the scrolling operation completes as expected.
Root Cause:
ScrollToAsync hangs when called with the same position the scroll view is already at. On iOS, UIScrollView.SetContentOffset does not trigger ScrollAnimationEnded when the content offset doesn't actually so ScrollFinished() is never called and the Task never completes. On Windows, ScrollViewer.ChangeView has the same behavior when already at the target offset.change
Description of Change:
iOS (ScrollViewHandler.iOS.cs):
Before calling SetContentOffset, check whether the clamped target position already matches the current ContentOffset. If already at the target:
Skip the SetContentOffset call (no-op scroll)
Call ScrollFinished() directly to complete the pending Task
ScrollFinished) handles all other cases unchanged.
Windows (ScrollViewHandler.Windows.cs):
Before calling ChangeView, check whether the requested position matches the current VerticalOffset/HorizontalOffset. If already at the target, call ScrollFinished() and return early.
Tests (ScrollViewUITests.cs):
Removed three [FailsOn*WhenRunningOnXamarinUITest] attributes from ScrollToYTwice that were tracking this issue on iOS, Mac, and Windows.
Platform Affected
Tested the behavior in the following platforms.
Reference:
N/A
Issues Fixed:
Fixes #27250
Screenshots
Before_27250.mov
After_27250.mov