Skip to content

Commit 47c5535

Browse files
ShowPopupAsync will now correctly return once the popup is closed under heavy garbage collection (#2756)
* Introduce unit test that reproduces the 'indefinite' ShowPopupAsync call * Make PopupClosed event a stronger reference * `dotnet format` --------- Co-authored-by: Shaun Lawrence <[email protected]> Co-authored-by: Brandon Minnick <[email protected]>
1 parent 98449fc commit 47c5535

File tree

8 files changed

+42
-26
lines changed

8 files changed

+42
-26
lines changed

src/CommunityToolkit.Maui.UnitTests/BaseHandlerTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ static void InitializeServicesAndSetMockApplication(out IServiceProvider service
8181
var mockPopup = new MockSelfClosingPopup(mockPageViewModel, new());
8282

8383
PopupService.AddPopup(mockPopup, mockPageViewModel, appBuilder.Services, ServiceLifetime.Transient);
84+
8485
appBuilder.Services.AddTransientPopup<MockPopup>();
86+
appBuilder.Services.AddTransient<GarbageCollectionHeavySelfClosingPopup>();
8587
#endregion
8688

8789
var mauiApp = appBuilder.Build();

src/CommunityToolkit.Maui.UnitTests/Extensions/PopupExtensionsTests.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,21 @@ public async Task ShowPopupAsyncWithView_Shell_ShouldValidateProperBindingContex
946946
Assert.Equal(shellParameterViewModelTextValue, view.BindingContext.Text);
947947
}
948948

949+
[Fact(Timeout = (int)TestDuration.Medium)]
950+
public async Task ShowPopupAsync_ShouldSuccessfullyCompleteAndReturnResultUnderHeavyGarbageCollection()
951+
{
952+
// Arrange
953+
var mockPopup = ServiceProvider.GetRequiredService<GarbageCollectionHeavySelfClosingPopup>();
954+
var selfClosingPopup = ServiceProvider.GetRequiredService<GarbageCollectionHeavySelfClosingPopup>() ?? throw new InvalidOperationException();
955+
956+
// Act
957+
var result = await navigation.ShowPopupAsync<object?>(selfClosingPopup, PopupOptions.Empty, TestContext.Current.CancellationToken);
958+
959+
// Assert
960+
Assert.Same(mockPopup.Result, result.Result);
961+
Assert.False(result.WasDismissedByTappingOutsideOfPopup);
962+
}
963+
949964
[Fact(Timeout = (int)TestDuration.Medium)]
950965
public async Task ShowPopupAsync_ShouldReturnResultOnceClosed()
951966
{
@@ -1430,7 +1445,7 @@ public async Task ClosePopupAsyncT_ShouldClosePopupUsingPageAndReturnResult()
14301445
Assert.Equal(expectedResult, popupResult.Result);
14311446
Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup);
14321447
}
1433-
1448+
14341449
[Fact(Timeout = (int)TestDuration.Short)]
14351450
public async Task ShowPopupAsync_TaskShouldCompleteWhenCloseAsyncIsCalled()
14361451
{
@@ -1460,7 +1475,7 @@ public async Task ShowPopupAsync_TaskShouldCompleteWhenCloseAsyncIsCalled()
14601475
Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup);
14611476
}
14621477

1463-
static TapGestureRecognizer GetTapOutsideGestureRecognizer(PopupPage popupPage) =>
1478+
static TapGestureRecognizer GetTapOutsideGestureRecognizer(PopupPage popupPage) =>
14641479
(TapGestureRecognizer)popupPage.Content.Children.OfType<BoxView>().Single().GestureRecognizers[0];
14651480
}
14661481

src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,9 @@ public async Task ClosePopupAsyncT_ShouldClosePopupUsingPageAndReturnResult()
522522
}
523523
}
524524

525-
sealed class MockSelfClosingPopup : Popup<object?>, IQueryAttributable
525+
class GarbageCollectionHeavySelfClosingPopup(MockPageViewModel viewModel, object? result = null) : MockSelfClosingPopup(viewModel, result);
526+
527+
class MockSelfClosingPopup : Popup<object?>, IQueryAttributable
526528
{
527529
public const int ExpectedResult = 2;
528530

@@ -548,7 +550,9 @@ async void HandleTick(object? sender, EventArgs e)
548550
timer.Tick -= HandleTick;
549551
try
550552
{
553+
GC.Collect();
551554
await CloseAsync(Result);
555+
GC.Collect();
552556
}
553557
catch (InvalidOperationException)
554558
{

src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupPageTests.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -230,32 +230,32 @@ public void TapGestureRecognizer_VerifyCanBeDismissedByTappingOutsideOfPopup_Sho
230230

231231
// Assert
232232
Assert.True(tapGestureRecognizer.Command?.CanExecute(null));
233-
233+
234234
// Act
235235
view.CanBeDismissedByTappingOutsideOfPopup = false;
236236
popupOptions.CanBeDismissedByTappingOutsideOfPopup = false;
237-
237+
238238
// Assert
239239
Assert.False(tapGestureRecognizer.Command?.CanExecute(null));
240-
240+
241241
// Act
242242
view.CanBeDismissedByTappingOutsideOfPopup = true;
243243
popupOptions.CanBeDismissedByTappingOutsideOfPopup = false;
244-
244+
245245
// Assert
246246
Assert.False(tapGestureRecognizer.Command?.CanExecute(null));
247-
247+
248248
// Act
249249
view.CanBeDismissedByTappingOutsideOfPopup = false;
250250
popupOptions.CanBeDismissedByTappingOutsideOfPopup = true;
251-
251+
252252
// Assert
253253
Assert.False(tapGestureRecognizer.Command?.CanExecute(null));
254-
254+
255255
// Act
256256
view.CanBeDismissedByTappingOutsideOfPopup = true;
257257
popupOptions.CanBeDismissedByTappingOutsideOfPopup = true;
258-
258+
259259
// Assert
260260
Assert.True(tapGestureRecognizer.Command?.CanExecute(null));
261261

@@ -529,8 +529,8 @@ public void PopupPage_ShouldRespectLayoutOptions()
529529
Assert.Equal(LayoutOptions.Start, border.VerticalOptions);
530530
Assert.Equal(LayoutOptions.End, border.HorizontalOptions);
531531
}
532-
533-
static TapGestureRecognizer GetTapOutsideGestureRecognizer(PopupPage popupPage) =>
532+
533+
static TapGestureRecognizer GetTapOutsideGestureRecognizer(PopupPage popupPage) =>
534534
(TapGestureRecognizer)popupPage.Content.Children.OfType<BoxView>().Single().GestureRecognizers[0];
535535

536536
// Helper class for testing protected methods

src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public void PopupBackgroundColor_DefaultValue_ShouldBeWhite()
1414
{
1515
Assert.Equal(PopupDefaults.BackgroundColor, Colors.White);
1616
}
17-
17+
1818
[Fact]
1919
public void CanBeDismissedByTappingOutsideOfPopup_DefaultValue_ShouldBeTrue()
2020
{
@@ -158,7 +158,7 @@ public async Task PopupT_Close_ShouldNotThrowExceptionWhenCloseIsOverridden()
158158
await popup.CloseAsync(TestContext.Current.CancellationToken);
159159
await popup.CloseAsync("Hello", TestContext.Current.CancellationToken);
160160
}
161-
161+
162162
[Fact(Timeout = (int)TestDuration.Short)]
163163
public async Task ShowPopupAsync_TaskShouldCompleteWhenPopupCloseAsyncIsCalled()
164164
{

src/CommunityToolkit.Maui/Primitives/Defaults/PopupDefaults.shared.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ static class PopupDefaults
3131
/// Default value for <see cref="VisualElement.BackgroundColor"/> BackgroundColor
3232
/// </summary>
3333
public static Color BackgroundColor { get; } = Colors.White;
34-
34+
3535
/// <summary>
3636
/// Default value for <see cref="Popup.CanBeDismissedByTappingOutsideOfPopup"/>
3737
/// </summary>

src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public partial class Popup : ContentView
2626
/// Bindable property to set the vertical position of the <see cref="Popup"/> when displayed on screen
2727
/// </summary>
2828
public static new readonly BindableProperty VerticalOptionsProperty = View.VerticalOptionsProperty;
29-
29+
3030
/// <summary>
3131
/// Backing BindableProperty for the <see cref="CanBeDismissedByTappingOutsideOfPopup"/> property.
3232
/// </summary>
@@ -88,7 +88,7 @@ public Popup()
8888
get => base.VerticalOptions;
8989
set => base.VerticalOptions = value;
9090
}
91-
91+
9292
/// <inheritdoc cref="IPopupOptions.CanBeDismissedByTappingOutsideOfPopup"/> />
9393
/// <remarks>
9494
/// When true and the user taps outside the popup, it will dismiss.

src/CommunityToolkit.Maui/Views/Popup/PopupPage.shared.cs

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ partial class PopupPage : ContentPage, IQueryAttributable
2626
readonly Popup popup;
2727
readonly IPopupOptions popupOptions;
2828
readonly Command tapOutsideOfPopupCommand;
29-
readonly WeakEventManager popupClosedEventManager = new();
3029

3130
public PopupPage(View view, IPopupOptions popupOptions)
3231
: this(view as Popup ?? CreatePopupFromView<Popup>(view), popupOptions)
@@ -64,11 +63,7 @@ public PopupPage(Popup popup, IPopupOptions popupOptions)
6463
On<iOS>().SetModalPresentationStyle(UIModalPresentationStyle.OverFullScreen);
6564
}
6665

67-
public event EventHandler<IPopupResult> PopupClosed
68-
{
69-
add => popupClosedEventManager.AddEventHandler(value);
70-
remove => popupClosedEventManager.RemoveEventHandler(value);
71-
}
66+
public event EventHandler<IPopupResult>? PopupClosed;
7267

7368
// Prevent Content from being set by external class
7469
// Casts `PopupPage.Content` to return typeof(PopupPageLayout)
@@ -102,7 +97,7 @@ public async Task CloseAsync(PopupResult result, CancellationToken token = defau
10297
token.ThrowIfCancellationRequested();
10398
await Navigation.PopModalAsync(false).WaitAsync(token);
10499

105-
popupClosedEventManager.HandleEvent(this, result, nameof(PopupClosed));
100+
PopupClosed?.Invoke(this, result);
106101
}
107102

108103
protected override bool OnBackButtonPressed()
@@ -164,7 +159,7 @@ void HandlePopupOptionsPropertyChanged(object? sender, PropertyChangedEventArgs
164159
tapOutsideOfPopupCommand.ChangeCanExecute();
165160
}
166161
}
167-
162+
168163
void HandlePopupPropertyChanged(object? sender, PropertyChangedEventArgs e)
169164
{
170165
if (e.PropertyName == Popup.CanBeDismissedByTappingOutsideOfPopupProperty.PropertyName)

0 commit comments

Comments
 (0)