Skip to content

Commit 1ca1644

Browse files
committed
Fix crash when displaying alerts on unloaded pages
Added null checks for the Window property in DisplayAlertAsync, DisplayActionSheetAsync, and DisplayPromptAsync methods in Page.cs to prevent NullReferenceException when the page is no longer attached to a window. New UI tests verify that async alert requests do not crash the app after navigating away from the page.
1 parent e7f8f08 commit 1ca1644

File tree

3 files changed

+230
-3
lines changed

3 files changed

+230
-3
lines changed

src/Controls/src/Core/Page/Page.cs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -323,10 +323,26 @@ public Task<string> DisplayActionSheetAsync(string title, string cancel, string
323323
var args = new ActionSheetArguments(title, cancel, destruction, buttons);
324324

325325
args.FlowDirection = flowDirection;
326+
327+
// If page is no longer attached to a window (e.g., navigated away), ignore the action sheet request
328+
if (Window is null)
329+
{
330+
// Complete the task with cancel result
331+
args.SetResult(cancel);
332+
return args.Result.Task;
333+
}
334+
326335
if (IsPlatformEnabled)
327336
Window.AlertManager.RequestActionSheet(this, args);
328337
else
329-
_pendingActions.Add(() => Window.AlertManager.RequestActionSheet(this, args));
338+
_pendingActions.Add(() =>
339+
{
340+
// Check again in case window was detached while waiting
341+
if (Window is not null)
342+
Window.AlertManager.RequestActionSheet(this, args);
343+
else
344+
args.SetResult(cancel);
345+
});
330346

331347
return args.Result.Task;
332348
}
@@ -384,10 +400,25 @@ public Task<bool> DisplayAlertAsync(string title, string message, string accept,
384400
var args = new AlertArguments(title, message, accept, cancel);
385401
args.FlowDirection = flowDirection;
386402

403+
// If page is no longer attached to a window (e.g., navigated away), ignore the alert request
404+
if (Window is null)
405+
{
406+
// Complete the task with default result (cancel)
407+
args.SetResult(false);
408+
return args.Result.Task;
409+
}
410+
387411
if (IsPlatformEnabled)
388412
Window.AlertManager.RequestAlert(this, args);
389413
else
390-
_pendingActions.Add(() => Window.AlertManager.RequestAlert(this, args));
414+
_pendingActions.Add(() =>
415+
{
416+
// Check again in case window was detached while waiting
417+
if (Window is not null)
418+
Window.AlertManager.RequestAlert(this, args);
419+
else
420+
args.SetResult(false);
421+
});
391422

392423
return args.Result.Task;
393424
}
@@ -408,10 +439,25 @@ public Task<bool> DisplayAlertAsync(string title, string message, string accept,
408439
{
409440
var args = new PromptArguments(title, message, accept, cancel, placeholder, maxLength, keyboard, initialValue);
410441

442+
// If page is no longer attached to a window (e.g., navigated away), ignore the prompt request
443+
if (Window is null)
444+
{
445+
// Complete the task with null result
446+
args.SetResult(null);
447+
return args.Result.Task;
448+
}
449+
411450
if (IsPlatformEnabled)
412451
Window.AlertManager.RequestPrompt(this, args);
413452
else
414-
_pendingActions.Add(() => Window.AlertManager.RequestPrompt(this, args));
453+
_pendingActions.Add(() =>
454+
{
455+
// Check again in case window was detached while waiting
456+
if (Window is not null)
457+
Window.AlertManager.RequestPrompt(this, args);
458+
else
459+
args.SetResult(null);
460+
});
415461

416462
return args.Result.Task;
417463
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
namespace Maui.Controls.Sample.Issues;
5+
6+
[Issue(IssueTracker.Github, 33287, "DisplayAlertAsync throws NullReferenceException when page is no longer displayed", PlatformAffected.All)]
7+
public partial class Issue33287 : NavigationPage
8+
{
9+
public Issue33287() : base(new Issue33287MainPage())
10+
{
11+
}
12+
}
13+
14+
public partial class Issue33287MainPage : ContentPage
15+
{
16+
public Issue33287MainPage()
17+
{
18+
Title = "Issue 33287";
19+
20+
var layout = new VerticalStackLayout
21+
{
22+
Padding = 20,
23+
Spacing = 10
24+
};
25+
26+
layout.Children.Add(new Label
27+
{
28+
Text = "DisplayAlertAsync NullReferenceException Test",
29+
FontSize = 18,
30+
FontAttributes = FontAttributes.Bold
31+
});
32+
33+
layout.Children.Add(new Label
34+
{
35+
Text = "1. Tap 'Navigate to Second Page'\n2. Immediately tap 'Go Back'\n3. Wait 5 seconds - should NOT crash",
36+
TextColor = Colors.Gray
37+
});
38+
39+
layout.Children.Add(new Button
40+
{
41+
Text = "Navigate to Second Page",
42+
AutomationId = "NavigateButton",
43+
Command = new Command(async () =>
44+
{
45+
Console.WriteLine("[Issue33287] Navigating to SecondPage");
46+
StatusLabel.Text = "Status: Navigating...";
47+
await Navigation.PushAsync(new Issue33287SecondPage(this));
48+
})
49+
});
50+
51+
StatusLabel = new Label
52+
{
53+
Text = "Status: Ready",
54+
AutomationId = "StatusLabel",
55+
FontAttributes = FontAttributes.Bold
56+
};
57+
layout.Children.Add(StatusLabel);
58+
59+
Content = layout;
60+
}
61+
62+
public Label StatusLabel { get; private set; }
63+
64+
public void UpdateStatus(string status)
65+
{
66+
StatusLabel.Text = $"Status: {status}";
67+
Console.WriteLine($"[Issue33287] {status}");
68+
}
69+
}
70+
71+
public class Issue33287SecondPage : ContentPage
72+
{
73+
private readonly Issue33287MainPage _mainPage;
74+
75+
public Issue33287SecondPage(Issue33287MainPage mainPage)
76+
{
77+
_mainPage = mainPage;
78+
Title = "Second Page";
79+
80+
var layout = new VerticalStackLayout
81+
{
82+
Padding = 20,
83+
Spacing = 10
84+
};
85+
86+
layout.Children.Add(new Label
87+
{
88+
Text = "Second Page - Tap 'Go Back' quickly!",
89+
FontSize = 18,
90+
FontAttributes = FontAttributes.Bold
91+
});
92+
93+
layout.Children.Add(new Button
94+
{
95+
Text = "Go Back",
96+
AutomationId = "GoBackButton",
97+
Command = new Command(async () =>
98+
{
99+
Console.WriteLine("[Issue33287] Going back...");
100+
await Navigation.PopAsync();
101+
})
102+
});
103+
104+
Content = layout;
105+
}
106+
107+
protected override async void OnAppearing()
108+
{
109+
base.OnAppearing();
110+
111+
Console.WriteLine("[Issue33287] SecondPage.OnAppearing - Starting 5 second delay");
112+
_mainPage?.UpdateStatus("On second page, waiting 5 seconds...");
113+
114+
// Delay before showing alert
115+
await Task.Delay(5000);
116+
117+
Console.WriteLine("[Issue33287] Attempting to show DisplayAlertAsync");
118+
_mainPage?.UpdateStatus("Showing alert from unloaded page...");
119+
120+
try
121+
{
122+
// This should cause NullReferenceException if page is no longer displayed
123+
await DisplayAlertAsync("Test Alert", "This alert was delayed", "OK");
124+
125+
Console.WriteLine("[Issue33287] ✅ DisplayAlertAsync succeeded (page still loaded or fix applied)");
126+
_mainPage?.UpdateStatus("✅ Alert shown successfully");
127+
}
128+
catch (NullReferenceException ex)
129+
{
130+
Console.WriteLine($"[Issue33287] ❌ NullReferenceException caught: {ex.Message}");
131+
_mainPage?.UpdateStatus("❌ NullReferenceException occurred!");
132+
}
133+
catch (Exception ex)
134+
{
135+
Console.WriteLine($"[Issue33287] ❌ Exception: {ex.GetType().Name}: {ex.Message}");
136+
_mainPage?.UpdateStatus($"❌ Exception: {ex.GetType().Name}");
137+
}
138+
}
139+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using NUnit.Framework;
2+
using UITest.Appium;
3+
using UITest.Core;
4+
5+
namespace Microsoft.Maui.TestCases.Tests.Issues;
6+
7+
public class Issue33287 : _IssuesUITest
8+
{
9+
public override string Issue => "DisplayAlertAsync throws NullReferenceException when page is no longer displayed";
10+
11+
public Issue33287(TestDevice device) : base(device) { }
12+
13+
[Test]
14+
[Category(UITestCategories.Page)]
15+
public void DisplayAlertAsyncShouldNotCrashWhenPageUnloaded()
16+
{
17+
App.WaitForElement("NavigateButton");
18+
19+
// Navigate to second page
20+
App.Tap("NavigateButton");
21+
22+
// Wait for second page to appear
23+
App.WaitForElement("GoBackButton");
24+
25+
// Immediately go back before the 5 second delay completes
26+
App.Tap("GoBackButton");
27+
28+
// Wait for navigation to complete
29+
App.WaitForElement("StatusLabel");
30+
31+
// Wait for the delayed DisplayAlertAsync to be triggered (5 seconds + buffer)
32+
System.Threading.Thread.Sleep(6000);
33+
34+
// Get the status - should not show NullReferenceException
35+
var status = App.FindElement("StatusLabel").GetText();
36+
Console.WriteLine($"[TEST] Final status: {status}");
37+
38+
// Assert that no NullReferenceException occurred
39+
Assert.That(status, Does.Not.Contain("NullReferenceException"),
40+
"DisplayAlertAsync should not throw NullReferenceException when page is unloaded");
41+
}
42+
}

0 commit comments

Comments
 (0)