Skip to content

Fix LayoutCycleException from nested Borders on Windows#34337

Merged
kubaflo merged 4 commits intodotnet:inflight/currentfrom
Oxymoron290:fix/32406-border-layout-cycle
Mar 18, 2026
Merged

Fix LayoutCycleException from nested Borders on Windows#34337
kubaflo merged 4 commits intodotnet:inflight/currentfrom
Oxymoron290:fix/32406-border-layout-cycle

Conversation

@Oxymoron290
Copy link
Copy Markdown

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!

Description

Fixes #32406

On Windows, deeply nested Border elements (300+ at 3+ nesting depth) in ControlTemplates cause an unrecoverable LayoutCycleException. This is a regression from PR #24844 which moved Border shape updates from OnPropertyChanged (Width/Height) into BorderHandler.PlatformArrange.

Root Cause

The layout cycle is caused by double shape-geometry updates during the WinUI arrange pass:

  1. ContentPanel.ContentPanelSizeChanged (pre-existing) fires during WinUI's arrange pass and sets Path.Data
  2. BorderHandler.PlatformArrange (added by Speed-up Border rendering by avoiding useless pass during size allocation #24844) also calls UpdateValue(Shape) during arrange, which sets Path.Data again

Setting Path.Data on a WinUI Path element invalidates layout (triggers re-measure/arrange). With two invalidations per Border per arrange pass, and hundreds of deeply nested Borders, the cascading invalidations exceed WinUI's layout cycle detection threshold.

Fix

Skip the PlatformArrange shape update on Windows (#if !WINDOWS), since ContentPanel.ContentPanelSizeChanged already handles size-based shape updates during the layout pass. Non-Windows platforms retain PlatformArrange behavior since they have no equivalent SizeChanged handler.

Changes

  • src/Core/src/Handlers/Border/BorderHandler.cs — Wrap PlatformArrange override and _lastSize field with #if !WINDOWS
  • src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt — Mark removed API
  • src/Controls/tests/TestCases.HostApp/Issues/Issue32406.cs — HostApp page with 350 nested Borders at depth 3
  • src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32406.cs — UI test verifying no crash

Oxymoron290 and others added 2 commits March 4, 2026 13:10
Skip PlatformArrange shape update on Windows where ContentPanel.SizeChanged
already handles size-based path geometry updates. Calling UpdateValue(Shape)
during PlatformArrange sets Path.Data during the WinUI arrange pass, which
invalidates layout. With many nested Borders (300+ at 3+ depth), cascading
invalidations exceed WinUI's layout cycle detection threshold.

Regression from PR dotnet#24844 which moved shape updates from OnPropertyChanged
(Width/Height) to PlatformArrange.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Creates 350 Border elements at 3 nesting levels to verify no
LayoutCycleException occurs. Tests the fix on Windows where deep
Border trees previously caused crashes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 4, 2026 19:25
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 4, 2026

🚀 Dogfood this PR with:

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

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

Or

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

@dotnet-policy-service dotnet-policy-service bot added the community ✨ Community Contribution label Mar 4, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Hey there @@Oxymoron290! Thank you so much for your PR! Someone from the team will get assigned to your PR shortly and we'll get it reviewed.

@Oxymoron290
Copy link
Copy Markdown
Author

@Oxymoron290 please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@dotnet-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@dotnet-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@dotnet-policy-service agree company="Microsoft"

Contributor License Agreement

@dotnet-policy-service agree company="Microsoft"

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses a Windows-specific LayoutCycleException caused by deeply nested Border elements by removing the extra shape-geometry update performed during the WinUI arrange pass, relying instead on the existing ContentPanel size-changed path update logic.

Changes:

  • Skip BorderHandler.PlatformArrange shape updates on Windows to avoid layout invalidation loops during arrange.
  • Record the Windows TFM API surface change in PublicAPI.Unshipped.txt.
  • Add a HostApp repro page and an Appium UI test for issue #32406.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
src/Core/src/Handlers/Border/BorderHandler.cs Excludes the PlatformArrange override (and _lastSize) on Windows to prevent re-triggering layout during arrange.
src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt Marks the removed Windows-specific override as a removed API entry.
src/Controls/tests/TestCases.HostApp/Issues/Issue32406.cs Adds a repro page that constructs a large nested Border tree and signals success on load.
src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32406.cs Adds a UI test that asserts the page loads successfully without crashing.

@PureWeen PureWeen added this to the .NET 10 SR6 milestone Mar 4, 2026
@PureWeen PureWeen added the p/0 Current heighest priority issues that we are targeting for a release. label Mar 4, 2026
- Move rationale comment above #if !WINDOWS so it's visible in Windows builds
- Fix test namespace to Microsoft.Maui.TestCases.Tests.Issues and add required usings
- Clarify element count comment (350 elements * 3 depth = 1050 total Borders)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Author

@Oxymoron290 Oxymoron290 left a comment

Choose a reason for hiding this comment

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

Addressed all 3 review comments in commit aac7663:

  1. Comment placement: Moved the rationale comment above the #if directive so it is visible in Windows builds.
  2. Test namespace/usings: Fixed namespace to Microsoft.Maui.TestCases.Tests.Issues and added required using directives.
  3. Element count clarity: Updated comment to clarify that 350 elements x 3 nesting depth = 1050 total Border instances, intentional to exceed the layout cycle threshold.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

- Keep PlatformArrange public API surface consistent across all platforms;
  conditionalize only the UpdateValue(Shape) body with #if !WINDOWS
- Revert PublicAPI.Unshipped.txt removal entry (no longer needed)
- Use WaitForTextToBePresentInElement instead of WaitForElement + GetText
  to avoid race between element existence and Loaded handler completion

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@sheiksyedm
Copy link
Copy Markdown
Contributor

/azp run maui-pr-uitests , maui-pr-devicetests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 2 pipeline(s).

@kubaflo
Copy link
Copy Markdown
Contributor

kubaflo commented Mar 18, 2026

🤖 AI Summary

📊 Expand Full Reviewd76ab7b · Keep PlatformArrange override on all TFMs and fix test race condition
🔍 Pre-Flight — Context & Validation

Issue: #32406 - LayoutCycleException caused by nested Borders in ControlTemplates
PR: #34337 - Fix LayoutCycleException from nested Borders on Windows
Platforms Affected: Windows
Files Changed: 1 implementation, 2 test

Key Findings

  • The issue is Windows-only and is tracked as a regression from PR Speed-up Border rendering by avoiding useless pass during size allocation #24844, where Border shape updates moved into BorderHandler.PlatformArrange.
  • The PR keeps PlatformArrange public surface intact on all TFMs, but skips the size-triggered UpdateValue(nameof(IBorderStroke.Shape)) body on Windows to avoid WinUI arrange-pass invalidation loops.
  • Test coverage consists of a HostApp repro page (Issue32406) plus a matching Appium UI test that waits for the page's loaded-state success label.
  • Prior automated review comments were addressed in follow-up commits: comment placement, UITest namespace/usings, race-proof waiting, and preservation of Windows public API surface.
  • Issue discussion mentions only a batching/delay workaround; no unresolved edge-case comments were found on the PR.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #34337 Keep PlatformArrange override, but on Windows avoid the arrange-time UpdateValue(Shape) because ContentPanel.ContentPanelSizeChanged already handles size-based shape updates PENDING (Gate) src/Core/src/Handlers/Border/BorderHandler.cs, src/Controls/tests/TestCases.HostApp/Issues/Issue32406.cs, src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32406.cs Original PR

🚦 Gate — Test Verification

Gate Result: PASSED

Platform: windows
Mode: Full Verification

  • Tests FAIL without fix:
  • Tests PASS with fix:

Evidence: NestedBordersShouldNotCauseLayoutCycle failed without the fix because the app crashed during layout, and passed with the fix when the page loaded successfully.


🔧 Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 try-fix Skip redundant Windows Path.Data update from ContentPanel.UpdateBorder when invoked during arrange, letting ContentPanelSizeChanged handle the post-arrange path update PASS src/Core/src/Platform/Windows/ContentPanel.cs Works, but introduces one-shot flagging and deferred cleanup logic in Windows platform code
2 try-fix Cache the last Windows shape/size update in ContentPanel and deduplicate Path.Data writes, pre-populating the cache in ArrangeOverride so arrange-triggered UpdateBorder becomes a no-op PASS src/Core/src/Platform/Windows/ContentPanel.cs Works, but adds stateful cache coordination between arrange and size-changed paths
3 try-fix Defer Windows Path.Data / RenderTransform writes from BorderExtensions.UpdatePath via DispatcherQueue.TryEnqueue so mutation happens after layout FAIL src/Core/src/Platform/Windows/BorderExtensions.cs Never reached runtime validation due nullability compile errors (CS8602)
4 try-fix In BorderHandler.PlatformArrange, on Windows skip UpdateValue(Shape) only when the ContentPanel is already at the arranged size, treating size-match as proof the platform layer already updated shape PASS src/Core/src/Handlers/Border/BorderHandler.cs Works, but adds Windows-specific runtime heuristics to shared handler code
5 try-fix Move all Windows geometry writes to ContentPanelSizeChanged and cache normalized geometry plus ScaleTransform, so arrange-time UpdateBorder never writes Path.Data PASS src/Core/src/Platform/Windows/ContentPanel.cs Works, but is the most complex platform-layer redesign of the passing candidates
6 try-fix Replace SizeChanged-driven geometry writes with a LayoutUpdated hook so Path.Data is applied only after layout stabilizes PASS src/Core/src/Platform/Windows/ContentPanel.cs Works, but shifts border rendering to a post-layout event-driven model
PR PR #34337 Keep PlatformArrange override but skip Windows arrange-time UpdateValue(Shape) because ContentPanel.ContentPanelSizeChanged already handles size-based shape updates PASSED (Gate) src/Core/src/Handlers/Border/BorderHandler.cs, src/Controls/tests/TestCases.HostApp/Issues/Issue32406.cs, src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32406.cs Simplest passing fix; matches existing architecture and is directly validated by Gate

Cross-Pollination

Model Round New Ideas? Details
claude-opus-4.6 1 N/A Attempted Windows platform-layer guard in ContentPanel and passed
claude-sonnet-4.6 1 N/A Attempted cache-based Path.Data deduplication in ContentPanel and passed
gpt-5.3-codex 1 N/A Attempted deferred BorderExtensions.UpdatePath; failed at compile time
gemini-3-pro-preview 1 N/A Attempted runtime size-match guard in BorderHandler.PlatformArrange and passed
claude-opus-4.6 2 Yes NEW IDEA: cache normalized geometry per shape and scale the Path instead of regenerating geometry each size change
claude-sonnet-4.6 2 Yes NEW IDEA: use LayoutUpdated instead of SizeChanged/arrange timing to update the shape after the tree stabilizes
gpt-5.3-codex 2 No NO NEW IDEAS
gemini-3-pro-preview 2 No NO NEW IDEAS
claude-opus-4.6 3 Yes NEW IDEA: mutate a single PathGeometry in place rather than reassigning Path.Data
claude-sonnet-4.6 3 Yes NEW IDEA: isolate the border Path inside a Canvas so child invalidations do not bubble the same way
gpt-5.3-codex 3 No NO NEW IDEAS
gemini-3-pro-preview 3 Yes NEW IDEA: assign Path.Data once and update geometry figures/segments in place thereafter

Exhausted: No reached the 3-round cross-pollination cap with a few more invasive, untested ideas remaining.

Best Fix Comparison

Candidate Passes test Scope Complexity Servicing fit Notes
PR #34337 Yes 1 handler file (+tests in PR) Low High Directly addresses the redundant Windows arrange update and leaves Windows platform code unchanged
Attempt 1 Yes 1 Windows platform file Medium Medium Works, but adds timing flag logic
Attempt 2 Yes 1 Windows platform file Medium Medium Works, but adds cache/state coupling
Attempt 4 Yes 1 handler file Medium Medium Works, but relies on runtime size heuristics
Attempt 5 Yes 1 Windows platform file High Low Considerably more complex than needed
Attempt 6 Yes 1 Windows platform file High Low Event-driven post-layout model is heavier than necessary

Selected Fix: PR #34337 It is the simplest empirically validated solution, directly matches the diagnosed root cause on Windows, avoids new platform-layer state/event machinery, and is the best servicing-fit among all passing candidates.


📋 Report — Final Recommendation

Final Recommendation: APPROVE

Phase Status

Phase Status Notes
Pre-Flight COMPLETE Windows-only regression in Border layout; PR changes 1 implementation file and a matching HostApp/Appium UI-test pair
Gate PASSED Issue32406 failed without the fix and passed with the fix on windows
Try-Fix COMPLETE 6 attempts run, 5 passing; PR fix remained the best servicing-fit after comparison
Report COMPLETE

Summary

PR #34337 correctly fixes a Windows-only LayoutCycleException caused by nested Border elements. The added UI test reproduces the crash without the fix and passes with it. I also ran the mandatory multi-model try-fix phase: several alternative fixes passed, but all of them were more complex, more stateful, or more invasive than the PR's direct handler-level suppression of the redundant Windows arrange-time shape update.

Root Cause

On Windows, BorderHandler.PlatformArrange could call UpdateValue(nameof(IBorderStroke.Shape)) during the WinUI arrange pass. That flows into the Windows ContentPanel / BorderExtensions.UpdatePath path, which assigns Path.Data. Writing Path.Data during arrange invalidates layout and, with hundreds of nested borders, triggers a LayoutCycleException.

Fix Quality

The PR addresses the root cause directly by skipping the redundant Windows arrange-time shape update while preserving existing Windows size-based updates through ContentPanel.ContentPanelSizeChanged. Compared with the passing alternatives I tested, this is the simplest and clearest servicing fix: one small behavior change in the handler, no added platform-layer flags/caches/events, and no runtime heuristics beyond the platform conditional already justified by the Windows-specific rendering path.


📋 Expand PR Finalization Review

PR #34337 Finalization Review

Title: Good, optional improvement

Current: Fix LayoutCycleException from nested Borders on Windows

Assessment: Accurate and searchable.

Recommended (optional): [Windows] Border: Fix LayoutCycleException from nested Borders

Adding the platform/component prefix would better match the repository's commit-title style, but the current title is still acceptable.

Description: Strong, but needs one accuracy fix

Quality assessment:

  • Structure: Clear root-cause and fix explanation
  • Technical depth: Explains why the WinUI layout cycle happens
  • Accuracy: Mostly accurate, but one stale bullet remains
  • Completeness: Includes the required NOTE block and links the issue

Mismatch with final diff:

  • The ### Changes section still says the PR updates src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt.
  • That is no longer true in the final PR. The PublicAPI change was reverted when the author kept PlatformArrange on all TFMs and only conditionalized the Windows-specific body.

Recommended description edits:

  • Remove the stale PublicAPI.Unshipped.txt bullet.
  • Update the BorderHandler.cs bullet to reflect the final implementation:
    • Keep PlatformArrange on all TFMs.
    • Wrap only _lastSize and the UpdateValue(nameof(IBorderStroke.Shape)) path in #if !WINDOWS.

Optional cleanup:

  • Rename ## Description to ### Description of Change.
  • Move Fixes #32406 under a ### Issues Fixed heading to align with the PR template more closely.

Code Review Findings

Critical Issues

  • None.

Suggestions / Follow-up Before Merge

Potential Windows regression: stroke-thickness-only updates can leave stale path geometry

  • File: src/Core/src/Handlers/Border/BorderHandler.cs
  • Problem: This PR intentionally stops recalculating border geometry during PlatformArrange on Windows and relies on ContentPanel.ContentPanelSizeChanged instead. That fixes the layout cycle, but StrokeThickness also affects the computed path geometry on Windows (BorderExtensions.UpdatePath subtracts StrokeThickness from width/height and updates the render transform). MapStrokeThickness currently updates only Path.StrokeThickness; it does not recalculate Path.Data, and changing stroke thickness alone does not trigger ContentPanelSizeChanged.
  • Impact: A border whose StrokeThickness changes after initial layout can render with stale geometry on Windows.
  • Recommendation: Before merge, handle the Windows StrokeThickness path by forcing a geometry refresh when thickness changes without a size change, and consider adding coverage for that scenario.

Looks Good

  • The core fix is well-targeted and matches the existing Windows ContentPanel.ContentPanelSizeChanged behavior for size-driven updates.
  • The final implementation avoids unnecessary public API churn by keeping PlatformArrange present on all TFMs.
  • The UI test was updated to use WaitForTextToBePresentInElement, which avoids the original race between page load and text assertion.
  • PR checks are green (gh pr checks 34337 reported all checks successful, including UI/device test lanes), which increases confidence that the added repro test is stable.

Overall

Recommendation: Do not finalize as-is. First, correct the stale PR-description bullet and resolve the Windows StrokeThickness geometry regression risk described above.

@kubaflo kubaflo added s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) labels Mar 18, 2026
@kubaflo kubaflo changed the base branch from main to inflight/current March 18, 2026 13:07
@kubaflo kubaflo merged commit 844d6c5 into dotnet:inflight/current Mar 18, 2026
141 of 142 checks passed
@github-project-automation github-project-automation bot moved this from Approved to Done in MAUI SDK Ongoing Mar 18, 2026
@Oxymoron290 Oxymoron290 deleted the fix/32406-border-layout-cycle branch March 18, 2026 16:57
PureWeen pushed a commit that referenced this pull request Mar 19, 2026
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could <a
href="https://github.com/dotnet/maui/wiki/Testing-PR-Builds">test the
resulting artifacts</a> from this PR and let us know in a comment if
this change resolves your issue. Thank you!

## Description

Fixes #32406

On Windows, deeply nested `Border` elements (300+ at 3+ nesting depth)
in ControlTemplates cause an unrecoverable `LayoutCycleException`. This
is a regression from PR #24844 which moved Border shape updates from
`OnPropertyChanged` (Width/Height) into `BorderHandler.PlatformArrange`.

### Root Cause

The layout cycle is caused by **double shape-geometry updates during the
WinUI arrange pass**:

1. `ContentPanel.ContentPanelSizeChanged` (pre-existing) fires during
WinUI's arrange pass and sets `Path.Data`
2. `BorderHandler.PlatformArrange` (added by #24844) also calls
`UpdateValue(Shape)` during arrange, which sets `Path.Data` again

Setting `Path.Data` on a WinUI `Path` element invalidates layout
(triggers re-measure/arrange). With two invalidations per Border per
arrange pass, and hundreds of deeply nested Borders, the cascading
invalidations exceed WinUI's layout cycle detection threshold.

### Fix

Skip the `PlatformArrange` shape update on Windows (`#if !WINDOWS`),
since `ContentPanel.ContentPanelSizeChanged` already handles size-based
shape updates during the layout pass. Non-Windows platforms retain
`PlatformArrange` behavior since they have no equivalent `SizeChanged`
handler.

### Changes

- `src/Core/src/Handlers/Border/BorderHandler.cs` — Wrap
`PlatformArrange` override and `_lastSize` field with `#if !WINDOWS`
- `src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt` — Mark
removed API
- `src/Controls/tests/TestCases.HostApp/Issues/Issue32406.cs` — HostApp
page with 350 nested Borders at depth 3
- `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32406.cs`
— UI test verifying no crash

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PureWeen pushed a commit that referenced this pull request Mar 23, 2026
#34575)

<!-- 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!

## Description

Adds Windows platform support to the `maui-copilot` CI pipeline (AzDO
definition 27723), enabling Copilot PR reviews on Windows-targeted PRs.

### Changes

**`eng/pipelines/ci-copilot.yml`**
- Add `catalyst` and `windows` to Platform parameter values
- Add per-platform pool selection (`androidPool`, `iosPool`, `macPool`,
`windowsPool`)
- Skip Xcode, Android SDK, simulator setup for Windows/Catalyst
- Add Windows-specific "Set screen resolution" step (1920x1080)
- Add MacCatalyst-specific "Disable Notification Center" step
- Fix `sed` command for `Directory.Build.Override.props` on Windows (Git
Bash uses GNU sed)
- Handle Copilot CLI PATH detection on Windows vs Unix
- Change `script:` steps to `bash:` for cross-platform consistency

**`.github/scripts/Review-PR.ps1`**
- Add `catalyst` to ValidateSet for Platform parameter

**`.github/scripts/BuildAndRunHostApp.ps1`**
- Add Windows test assembly directory for artifact collection

**`.github/scripts/post-ai-summary-comment.ps1` /
`post-pr-finalize-comment.ps1`**
- Various improvements for cross-platform comment posting

### Validation

Successfully ran the pipeline with `Platform=windows` on multiple
Windows-specific PRs:
- PR #27713 — ✅ Succeeded
- PR #34337 — ✅ Succeeded
- PR #26217, #27609, #27880, #28617, #29927, #30068 — Triggered and
running

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PureWeen pushed a commit that referenced this pull request Mar 24, 2026
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could <a
href="https://github.com/dotnet/maui/wiki/Testing-PR-Builds">test the
resulting artifacts</a> from this PR and let us know in a comment if
this change resolves your issue. Thank you!

## Description

Fixes #32406

On Windows, deeply nested `Border` elements (300+ at 3+ nesting depth)
in ControlTemplates cause an unrecoverable `LayoutCycleException`. This
is a regression from PR #24844 which moved Border shape updates from
`OnPropertyChanged` (Width/Height) into `BorderHandler.PlatformArrange`.

### Root Cause

The layout cycle is caused by **double shape-geometry updates during the
WinUI arrange pass**:

1. `ContentPanel.ContentPanelSizeChanged` (pre-existing) fires during
WinUI's arrange pass and sets `Path.Data`
2. `BorderHandler.PlatformArrange` (added by #24844) also calls
`UpdateValue(Shape)` during arrange, which sets `Path.Data` again

Setting `Path.Data` on a WinUI `Path` element invalidates layout
(triggers re-measure/arrange). With two invalidations per Border per
arrange pass, and hundreds of deeply nested Borders, the cascading
invalidations exceed WinUI's layout cycle detection threshold.

### Fix

Skip the `PlatformArrange` shape update on Windows (`#if !WINDOWS`),
since `ContentPanel.ContentPanelSizeChanged` already handles size-based
shape updates during the layout pass. Non-Windows platforms retain
`PlatformArrange` behavior since they have no equivalent `SizeChanged`
handler.

### Changes

- `src/Core/src/Handlers/Border/BorderHandler.cs` — Wrap
`PlatformArrange` override and `_lastSize` field with `#if !WINDOWS`
- `src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt` — Mark
removed API
- `src/Controls/tests/TestCases.HostApp/Issues/Issue32406.cs` — HostApp
page with 350 nested Borders at depth 3
- `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32406.cs`
— UI test verifying no crash

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
KarthikRajaKalaimani pushed a commit to KarthikRajaKalaimani/maui that referenced this pull request Mar 30, 2026
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could <a
href="https://github.com/dotnet/maui/wiki/Testing-PR-Builds">test the
resulting artifacts</a> from this PR and let us know in a comment if
this change resolves your issue. Thank you!

## Description

Fixes dotnet#32406

On Windows, deeply nested `Border` elements (300+ at 3+ nesting depth)
in ControlTemplates cause an unrecoverable `LayoutCycleException`. This
is a regression from PR dotnet#24844 which moved Border shape updates from
`OnPropertyChanged` (Width/Height) into `BorderHandler.PlatformArrange`.

### Root Cause

The layout cycle is caused by **double shape-geometry updates during the
WinUI arrange pass**:

1. `ContentPanel.ContentPanelSizeChanged` (pre-existing) fires during
WinUI's arrange pass and sets `Path.Data`
2. `BorderHandler.PlatformArrange` (added by dotnet#24844) also calls
`UpdateValue(Shape)` during arrange, which sets `Path.Data` again

Setting `Path.Data` on a WinUI `Path` element invalidates layout
(triggers re-measure/arrange). With two invalidations per Border per
arrange pass, and hundreds of deeply nested Borders, the cascading
invalidations exceed WinUI's layout cycle detection threshold.

### Fix

Skip the `PlatformArrange` shape update on Windows (`#if !WINDOWS`),
since `ContentPanel.ContentPanelSizeChanged` already handles size-based
shape updates during the layout pass. Non-Windows platforms retain
`PlatformArrange` behavior since they have no equivalent `SizeChanged`
handler.

### Changes

- `src/Core/src/Handlers/Border/BorderHandler.cs` — Wrap
`PlatformArrange` override and `_lastSize` field with `#if !WINDOWS`
- `src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt` — Mark
removed API
- `src/Controls/tests/TestCases.HostApp/Issues/Issue32406.cs` — HostApp
page with 350 nested Borders at depth 3
- `src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue32406.cs`
— UI test verifying no crash

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community ✨ Community Contribution p/0 Current heighest priority issues that we are targeting for a release. s/agent-approved AI agent recommends approval - PR fix is correct and optimal s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

LayoutCycleException caused by nested Borders in ControlTemplates

5 participants