Fix VirtualView null exceptions and popup removal race condition

- Fix RemoveAsync race condition: close popup before disconnecting handler
- Add null-safe VirtualView access with try-catch on all platforms
- Properly unsubscribe SizeChanged event in PopupPageHandler
- Fix Android Window type ambiguity with fully qualified name
- Fix README path in csproj

The root cause was layout events firing during popup removal while
the handler was being disconnected. Now we close the popup first
to stop events, then clean up.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
David H. Friedel Jr. 2026-01-01 23:14:34 +00:00
parent d5b0054cb9
commit bf68a5f09a
7 changed files with 146 additions and 76 deletions

View File

@ -110,6 +110,6 @@
<ItemGroup>
<None Include="icon.png" Pack="true" PackagePath="\" />
<None Include="..\..\..\..\README.md" Pack="true" PackagePath="\" />
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>

View File

@ -44,7 +44,7 @@ public class AndroidMopups : IPopupPlatform
return AddAsync(page, null);
}
public Task AddAsync(PopupPage page, Window? targetWindow)
public Task AddAsync(PopupPage page, Microsoft.Maui.Controls.Window? targetWindow)
{
HandleAccessibility(true, page.DisableAndroidAccessibilityHandling, page);

View File

@ -59,7 +59,11 @@ namespace Mopups.Platforms.MacCatalyst
if (Equals(subview, view))
{
((PopupPage)Handler.VirtualView).SendBackgroundClick();
try
{
(Handler.VirtualView as PopupPage)?.SendBackgroundClick();
}
catch (InvalidOperationException) { }
}
}
@ -74,30 +78,36 @@ namespace Mopups.Platforms.MacCatalyst
void UpdateSize(PopupPageRenderer handler)
{
var currentElement = ((PopupPage)Handler.VirtualView);
if (handler.Handler.PlatformView?.Superview?.Frame == null || currentElement == null)
return;
var superviewFrame = handler.Handler.PlatformView.Superview.Frame;
var applicationFrame = UIScreen.MainScreen.ApplicationFrame;
var systemPadding = new Thickness
try
{
Left = applicationFrame.Left,
Top = applicationFrame.Top,
Right = applicationFrame.Right - applicationFrame.Width - applicationFrame.Left,
Bottom = applicationFrame.Bottom - applicationFrame.Height - applicationFrame.Top + handler.KeyboardBounds.Height
};
var currentElement = Handler.VirtualView as PopupPage;
if ((handler.Handler.VirtualView.Width != superviewFrame.Width && handler.Handler.VirtualView.Height != superviewFrame.Height)
|| currentElement.SystemPadding.Bottom != systemPadding.Bottom)
{
currentElement.BatchBegin();
currentElement.SystemPadding = systemPadding;
currentElement.Layout(new Rect(currentElement.X, currentElement.Y, superviewFrame.Width, superviewFrame.Height));
currentElement.BatchCommit();
if (handler.Handler.PlatformView?.Superview?.Frame == null || currentElement == null)
return;
var superviewFrame = handler.Handler.PlatformView.Superview.Frame;
var applicationFrame = UIScreen.MainScreen.ApplicationFrame;
var systemPadding = new Thickness
{
Left = applicationFrame.Left,
Top = applicationFrame.Top,
Right = applicationFrame.Right - applicationFrame.Width - applicationFrame.Left,
Bottom = applicationFrame.Bottom - applicationFrame.Height - applicationFrame.Top + handler.KeyboardBounds.Height
};
var virtualView = handler.Handler.VirtualView;
if (virtualView != null &&
((virtualView.Width != superviewFrame.Width && virtualView.Height != superviewFrame.Height)
|| currentElement.SystemPadding.Bottom != systemPadding.Bottom))
{
currentElement.BatchBegin();
currentElement.SystemPadding = systemPadding;
currentElement.Layout(new Rect(currentElement.X, currentElement.Y, superviewFrame.Width, superviewFrame.Height));
currentElement.BatchCommit();
}
}
catch (InvalidOperationException) { }
}
}

View File

@ -6,6 +6,8 @@ namespace Mopups.Platforms.Windows
{
public class PopupPageHandler : PageHandler
{
private Microsoft.UI.Xaml.SizeChangedEventHandler? _sizeChangedHandler;
public PopupPageHandler()
{
}
@ -14,7 +16,25 @@ namespace Mopups.Platforms.Windows
{
base.ConnectHandler(platformView);
PlatformView.SizeChanged += (_, e) => VirtualView.ComputeDesiredSize(e.NewSize.Width, e.NewSize.Height);
_sizeChangedHandler = (_, e) =>
{
try
{
VirtualView?.ComputeDesiredSize(e.NewSize.Width, e.NewSize.Height);
}
catch (InvalidOperationException) { }
};
PlatformView.SizeChanged += _sizeChangedHandler;
}
protected override void DisconnectHandler(ContentPanel platformView)
{
if (_sizeChangedHandler != null)
{
platformView.SizeChanged -= _sizeChangedHandler;
_sizeChangedHandler = null;
}
base.DisconnectHandler(platformView);
}
protected override ContentPanel CreatePlatformView()

View File

@ -15,7 +15,20 @@ namespace Mopups.Platforms.Windows
internal WinPopup? Container { get; private set; }
private PopupPage CurrentElement => (PopupPage)handler.VirtualView;
private PopupPage? CurrentElement
{
get
{
try
{
return handler.VirtualView as PopupPage;
}
catch (InvalidOperationException)
{
return null;
}
}
}
public PopupPageRenderer(PopupPageHandler handler)
{
@ -92,7 +105,7 @@ namespace Mopups.Platforms.Windows
{
if ((e.OriginalSource as PopupPageRenderer) == this)
{
CurrentElement.SendBackgroundClick();
CurrentElement?.SendBackgroundClick();
}
}

View File

@ -56,34 +56,33 @@ namespace Mopups.Windows.Implementation
public async Task AddAsync(PopupPage page, Window? window)
{
var mainPage = Application.Current.MainPage;
mainPage.AddLogicalChild(page);
var popup = new global::Microsoft.UI.Xaml.Controls.Primitives.Popup();
// Use TOPLATFORM to create your handlers
// I'd recommend wiring up all your services through ConfigureMopups
// builder.Services.AddScoped<IPopupPlatform, PopupPlatform>();
// builder.Services.AddScoped<IPopupNavigation, PopupNavigation>();
// Then you can use contructor resolution instead of singletons
// But I figured we could do that in a later PR and just work on windows here
var renderer = (PopupPageRenderer)page.ToPlatform(mainPage.Handler.MauiContext);
renderer.Prepare(popup);
popup.Child = renderer;
// https://github.com/microsoft/microsoft-ui-xaml/issues/3389
// Use the specified window's XamlRoot if provided, otherwise fall back to main window
// Use target window's page if provided, otherwise fall back to MainPage
Page parentPage;
IMauiContext mauiContext;
Microsoft.UI.Xaml.Window nativeWindow;
if (window?.Handler?.PlatformView is Microsoft.UI.Xaml.Window targetWindow)
if (window?.Handler?.PlatformView is Microsoft.UI.Xaml.Window targetWindow && window.Page != null)
{
parentPage = window.Page;
mauiContext = window.Handler.MauiContext!;
nativeWindow = targetWindow;
}
else
{
nativeWindow = mainPage.Handler.MauiContext.Services.GetService<Microsoft.UI.Xaml.Window>();
parentPage = Application.Current.MainPage;
mauiContext = parentPage.Handler.MauiContext;
nativeWindow = mauiContext.Services.GetService<Microsoft.UI.Xaml.Window>();
}
parentPage.AddLogicalChild(page);
var popup = new global::Microsoft.UI.Xaml.Controls.Primitives.Popup();
var renderer = (PopupPageRenderer)page.ToPlatform(mauiContext);
renderer.Prepare(popup);
popup.Child = renderer;
// https://github.com/microsoft/microsoft-ui-xaml/issues/3389
popup.XamlRoot = nativeWindow.Content.XamlRoot;
popup.IsOpen = true;
page.ForceLayout();
@ -96,17 +95,35 @@ namespace Mopups.Windows.Implementation
if (page == null)
throw new Exception("Popup page is null");
var renderer = (PopupPageRenderer)page.ToPlatform(Application.Current.MainPage.Handler.MauiContext);
var popup = renderer.Container;
// Get the handler before we start cleanup
var handler = page.Handler;
if (handler == null)
{
// Already cleaned up
page.Parent?.RemoveLogicalChild(page);
return;
}
var renderer = handler.PlatformView as PopupPageRenderer;
var popup = renderer?.Container;
if (popup != null)
{
renderer.Destroy();
Cleanup(page);
page.Parent?.RemoveLogicalChild(page);
popup.Child = null;
// First close the popup to stop layout events
popup.IsOpen = false;
popup.Child = null;
// Now safe to destroy and cleanup
renderer?.Destroy();
page.Parent?.RemoveLogicalChild(page);
// Disconnect handler last
Cleanup(page);
}
else
{
page.Parent?.RemoveLogicalChild(page);
Cleanup(page);
}
await Task.Delay(5);

View File

@ -59,7 +59,11 @@ namespace Mopups.Platforms.iOS
if (Equals(subview, view))
{
((PopupPage)Handler.VirtualView).SendBackgroundClick();
try
{
(Handler.VirtualView as PopupPage)?.SendBackgroundClick();
}
catch (InvalidOperationException) { }
}
}
@ -74,30 +78,36 @@ namespace Mopups.Platforms.iOS
void UpdateSize(PopupPageRenderer handler)
{
var currentElement = ((PopupPage)Handler.VirtualView);
if (handler.Handler.PlatformView?.Superview?.Frame == null || currentElement == null)
return;
var superviewFrame = handler.Handler.PlatformView.Superview.Frame;
var applicationFrame = UIScreen.MainScreen.ApplicationFrame;
var systemPadding = new Thickness
try
{
Left = applicationFrame.Left,
Top = applicationFrame.Top,
Right = applicationFrame.Right - applicationFrame.Width - applicationFrame.Left,
Bottom = applicationFrame.Bottom - applicationFrame.Height - applicationFrame.Top + handler.KeyboardBounds.Height
};
var currentElement = Handler.VirtualView as PopupPage;
if ((handler.Handler.VirtualView.Width != superviewFrame.Width && handler.Handler.VirtualView.Height != superviewFrame.Height)
|| currentElement.SystemPadding.Bottom != systemPadding.Bottom)
{
currentElement.BatchBegin();
currentElement.SystemPadding = systemPadding;
currentElement.Layout(new Rect(currentElement.X, currentElement.Y, superviewFrame.Width, superviewFrame.Height));
currentElement.BatchCommit();
if (handler.Handler.PlatformView?.Superview?.Frame == null || currentElement == null)
return;
var superviewFrame = handler.Handler.PlatformView.Superview.Frame;
var applicationFrame = UIScreen.MainScreen.ApplicationFrame;
var systemPadding = new Thickness
{
Left = applicationFrame.Left,
Top = applicationFrame.Top,
Right = applicationFrame.Right - applicationFrame.Width - applicationFrame.Left,
Bottom = applicationFrame.Bottom - applicationFrame.Height - applicationFrame.Top + handler.KeyboardBounds.Height
};
var virtualView = handler.Handler.VirtualView;
if (virtualView != null &&
((virtualView.Width != superviewFrame.Width && virtualView.Height != superviewFrame.Height)
|| currentElement.SystemPadding.Bottom != systemPadding.Bottom))
{
currentElement.BatchBegin();
currentElement.SystemPadding = systemPadding;
currentElement.Layout(new Rect(currentElement.X, currentElement.Y, superviewFrame.Width, superviewFrame.Height));
currentElement.BatchCommit();
}
}
catch (InvalidOperationException) { }
}
}