diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue36298.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue36298.cs new file mode 100644 index 000000000000..136970189669 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue36298.cs @@ -0,0 +1,69 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 36298, "[Windows] ContentPresenter throws ArgumentException when dynamically switching RefreshView or ScrollView content.", PlatformAffected.UWP)] +public class Issue36298 : ContentPage +{ + ContentView _contentHolder; + View _view1; + View _view2; + + public Issue36298() + { + _view1 = new ContentView + { + Content = new RefreshView + { + Content = new Label { Text = "View 1 - RefreshView" } + } + }; + + _view2 = new ContentView + { + Content = new ScrollView + { + Content = new Label { Text = "View 2 - ScrollView" } + } + }; + + _contentHolder = new ContentView + { + Content = _view1 + }; + + var switchToView2Button = new Button + { + Text = "Switch to View 2", + AutomationId = "SwitchToView2" + }; + switchToView2Button.Clicked += (_, _) => _contentHolder.Content = _view2; + + var switchToView1Button = new Button + { + Text = "Switch to View 1", + AutomationId = "SwitchToView1" + }; + switchToView1Button.Clicked += (_, _) => _contentHolder.Content = _view1; + + var successLabel = new Label + { + Text = "Waiting", + AutomationId = "SuccessLabel" + }; + + switchToView2Button.Clicked += (_, _) => successLabel.Text = "View2"; + switchToView1Button.Clicked += (_, _) => successLabel.Text = "Success"; + + Content = new VerticalStackLayout + { + Spacing = 10, + Padding = new Thickness(20), + Children = + { + switchToView2Button, + switchToView1Button, + _contentHolder, + successLabel + } + }; + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36298.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36298.cs new file mode 100644 index 000000000000..c19e9a2a7bca --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue36298.cs @@ -0,0 +1,34 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue36298 : _IssuesUITest +{ + public Issue36298(TestDevice device) : base(device) { } + + public override string Issue => "[Windows] ContentPresenter throws ArgumentException when dynamically switching RefreshView or ScrollView content."; + + [Test] + [Category(UITestCategories.Layout)] + public void SwitchingContentPresenterContentShouldNotCrash() + { + // Wait for the page to load showing View 1 + App.WaitForElement("SwitchToView2"); + + // Switch to View 2 + App.Tap("SwitchToView2"); + App.WaitForElement("SwitchToView1"); + + // Switch back to View 1 — this triggered the ArgumentException before the fix + App.Tap("SwitchToView1"); + App.Tap("SwitchToView2"); + App.Tap("SwitchToView1"); + + // Label is updated to "Success" only if the switch completed without crashing. + // WaitForElement("SuccessLabel") alone is insufficient because the label + // is present from page load; we must verify the text was actually updated. + Assert.That(App.WaitForTextToBePresentInElement("SuccessLabel", "Success"), Is.True); + } +} diff --git a/src/Core/src/Handlers/Border/BorderHandler.Windows.cs b/src/Core/src/Handlers/Border/BorderHandler.Windows.cs index d9a32ce0caa0..0feb80de3e8f 100644 --- a/src/Core/src/Handlers/Border/BorderHandler.Windows.cs +++ b/src/Core/src/Handlers/Border/BorderHandler.Windows.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.UI.Xaml; namespace Microsoft.Maui.Handlers { @@ -20,14 +21,38 @@ static partial void UpdateContent(IBorderHandler handler) _ = handler.VirtualView ?? throw new InvalidOperationException($"{nameof(VirtualView)} should have been set by base class."); _ = handler.MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} should have been set by base class."); - handler.PlatformView.CachedChildren.Clear(); handler.PlatformView.EnsureBorderPath(); if (handler.VirtualView.PresentedContent is IView view) { - // Detach the old handler if it exists (prevents WinUI COM exception on reuse) - view.Handler?.DisconnectHandler(); - handler.PlatformView.Content = view.ToPlatform(handler.MauiContext); + var platformView = view.ToPlatform(handler.MauiContext); + + // Detach from existing parent — mirrors Android RemoveFromParent / iOS RemoveFromSuperview. + // Always remove via CachedChildren directly: Content = null is a no-op when _content + // is null (e.g. ScrollViewHandler adds via paddingShim.CachedChildren.Add, not the + // Content setter), leaving the element with a live parent and causing a COM exception + // when we try to reparent it. Only clear _content when it actually tracks fwElement. + if (platformView is FrameworkElement fwElement && fwElement.Parent is not null) + { + if (fwElement.Parent is ContentPanel existingContentPanel) + { + existingContentPanel.CachedChildren.Remove(fwElement); + if (existingContentPanel.Content == fwElement) + { + existingContentPanel.Content = null; + } + } + else if (fwElement.Parent is MauiPanel existingPanel) + { + existingPanel.CachedChildren.Remove(fwElement); + } + } + + handler.PlatformView.Content = platformView; + } + else + { + handler.PlatformView.Content = null; } } diff --git a/src/Core/src/Handlers/ContentView/ContentViewHandler.Windows.cs b/src/Core/src/Handlers/ContentView/ContentViewHandler.Windows.cs index e62ecf46af3a..73e05ae50823 100644 --- a/src/Core/src/Handlers/ContentView/ContentViewHandler.Windows.cs +++ b/src/Core/src/Handlers/ContentView/ContentViewHandler.Windows.cs @@ -1,5 +1,6 @@ using System; using Microsoft.Maui.Graphics; +using Microsoft.UI.Xaml; namespace Microsoft.Maui.Handlers { @@ -21,13 +22,36 @@ static void UpdateContent(IContentViewHandler handler) _ = handler.VirtualView ?? throw new InvalidOperationException($"{nameof(VirtualView)} should have been set by base class."); _ = handler.MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} should have been set by base class."); - handler.PlatformView.CachedChildren.Clear(); - if (handler.VirtualView.PresentedContent is IView view) { - // Detach the old handler if it exists (prevents WinUI COM exception on reuse) - view.Handler?.DisconnectHandler(); - handler.PlatformView.CachedChildren.Add(view.ToPlatform(handler.MauiContext)); + var platformView = view.ToPlatform(handler.MauiContext); + + // Detach from existing parent — mirrors Android RemoveFromParent / iOS RemoveFromSuperview. + // Always remove via CachedChildren directly: Content = null is a no-op when _content + // is null (e.g. ScrollViewHandler adds via paddingShim.CachedChildren.Add, not the + // Content setter), leaving the element with a live parent and causing a COM exception + // when we try to reparent it. Only clear _content when it actually tracks fwElement. + if (platformView is FrameworkElement fwElement && fwElement.Parent is not null) + { + if (fwElement.Parent is ContentPanel existingContentPanel) + { + existingContentPanel.CachedChildren.Remove(fwElement); + if (existingContentPanel.Content == fwElement) + { + existingContentPanel.Content = null; + } + } + else if (fwElement.Parent is MauiPanel existingPanel) + { + existingPanel.CachedChildren.Remove(fwElement); + } + } + + handler.PlatformView.Content = platformView; + } + else + { + handler.PlatformView.Content = null; } }