From c6d286b3f13df4b11cc023c55f5e112deb6050a4 Mon Sep 17 00:00:00 2001 From: Shaun Lawrence <17139988+bijington@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:28:01 +0000 Subject: [PATCH 1/2] Reorder popup/view model instantiation --- .../Services/PopupService.shared.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/CommunityToolkit.Maui/Services/PopupService.shared.cs b/src/CommunityToolkit.Maui/Services/PopupService.shared.cs index 8506fefb5e..104eff3e2f 100644 --- a/src/CommunityToolkit.Maui/Services/PopupService.shared.cs +++ b/src/CommunityToolkit.Maui/Services/PopupService.shared.cs @@ -46,7 +46,7 @@ public void ShowPopup(INavigation navigation, IPopupOptions? options = null) { ArgumentNullException.ThrowIfNull(navigation); - var popupContent = GetPopupContent(serviceProvider.GetRequiredService()); + var popupContent = GetPopupContent(); navigation.ShowPopup(popupContent, options); } @@ -57,7 +57,7 @@ public void ShowPopup(Shell shell, IPopupOptions? options = null, IDictionary { ArgumentNullException.ThrowIfNull(shell); - var popupContent = GetPopupContent(serviceProvider.GetRequiredService()); + var popupContent = GetPopupContent(); shell.ShowPopup(popupContent, options, shellParameters); } @@ -77,7 +77,7 @@ public Task ShowPopupAsync(INavigation navigation, IPopupOption token.ThrowIfCancellationRequested(); - var popupContent = GetPopupContent(serviceProvider.GetRequiredService()); + var popupContent = GetPopupContent(); return navigation.ShowPopupAsync(popupContent, options, token); } @@ -89,7 +89,7 @@ public Task ShowPopupAsync(Shell shell, IPopupOptions? options, token.ThrowIfCancellationRequested(); - var popupContent = GetPopupContent(serviceProvider.GetRequiredService()); + var popupContent = GetPopupContent(); return shell.ShowPopupAsync(popupContent, options, shellParameters, token); } @@ -111,7 +111,7 @@ public Task> ShowPopupAsync(INavigation naviga ArgumentNullException.ThrowIfNull(navigation); token.ThrowIfCancellationRequested(); - var popupContent = GetPopupContent(serviceProvider.GetRequiredService()); + var popupContent = GetPopupContent(); return navigation.ShowPopupAsync(popupContent, options, token); } @@ -122,7 +122,7 @@ public Task> ShowPopupAsync(Shell shell, IPopu ArgumentNullException.ThrowIfNull(shell); token.ThrowIfCancellationRequested(); - var popupContent = GetPopupContent(serviceProvider.GetRequiredService()); + var popupContent = GetPopupContent(); return shell.ShowPopupAsync(popupContent, options, shellParameters, token); } @@ -178,16 +178,21 @@ public Task> ClosePopupAsync(INavigation navigation, T result services.TryAdd(new ServiceDescriptor(typeof(TPopupViewModel), typeof(TPopupViewModel), lifetime)); } - View GetPopupContent(T bindingContext) + View GetPopupContent() { - if (bindingContext is View view) + if (viewModelToViewMappings.TryGetValue(typeof(T), out var viewType)) { - return view; + if (serviceProvider.GetRequiredService(viewType) is View content) + { + return content; + } } - if (serviceProvider.GetRequiredService(viewModelToViewMappings[typeof(T)]) is View content) + var bindingContext = serviceProvider.GetRequiredService(typeof(T)); + + if (bindingContext is View view) { - return content; + return view; } throw new InvalidOperationException($"Could not locate {typeof(T).FullName}"); From 66675398aa8ccdbeab831281eaad91d54a32e0c0 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Sat, 7 Feb 2026 10:31:30 -0800 Subject: [PATCH 2/2] Add Regression Tests --- .../BaseViewTest.cs | 1 + .../Services/PopupServiceTests.cs | 88 ++++++++++++++++--- .../Services/PopupService.shared.cs | 8 +- 3 files changed, 78 insertions(+), 19 deletions(-) diff --git a/src/CommunityToolkit.Maui.UnitTests/BaseViewTest.cs b/src/CommunityToolkit.Maui.UnitTests/BaseViewTest.cs index 9f11b1b9b2..ced7ff0cd5 100644 --- a/src/CommunityToolkit.Maui.UnitTests/BaseViewTest.cs +++ b/src/CommunityToolkit.Maui.UnitTests/BaseViewTest.cs @@ -61,6 +61,7 @@ static void InitializeServicesAndSetMockApplication(out IServiceProvider service appBuilder.Services.AddTransientPopup(); appBuilder.Services.AddTransientPopup(); appBuilder.Services.AddTransientPopup(); + appBuilder.Services.AddTransientPopup(); appBuilder.Services.AddTransientPopup(); #endregion diff --git a/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs b/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs index d8d84d1257..3e27c24619 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs @@ -513,9 +513,51 @@ public async Task ClosePopupAsyncT_ShouldClosePopupUsingPageAndReturnResult() Assert.Equal(expectedResult, popupResult.Result); Assert.False(popupResult.WasDismissedByTappingOutsideOfPopup); } + + [Fact] + public void ShowPopup_WithRegisteredPopup_ShouldOnlyConstructViewModelOnce() + { + // Arrange + SingleConstructionViewModel.ConstructorCallCount = 0; + Assert.Equal(0, SingleConstructionViewModel.ConstructorCallCount); + + if (Application.Current?.Windows[0].Page is not Page page) + { + throw new InvalidOperationException("Page cannot be null"); + } + + var popupService = ServiceProvider.GetRequiredService(); + + // Act + popupService.ShowPopup(page.Navigation); + + // Assert + Assert.Equal(1, SingleConstructionViewModel.ConstructorCallCount); + } + + [Fact] + public async Task ShowPopupAsync_WithRegisteredPopup_ShouldOnlyConstructViewModelOnce() + { + // Arrange + SingleConstructionViewModel.ConstructorCallCount = 0; + Assert.Equal(0, SingleConstructionViewModel.ConstructorCallCount); + + if (Application.Current?.Windows[0].Page is not Page page) + { + throw new InvalidOperationException("Page cannot be null"); + } + + var popupService = ServiceProvider.GetRequiredService(); + + // Act + await popupService.ShowPopupAsync(page.Navigation, cancellationToken: TestContext.Current.CancellationToken); + + // Assert + Assert.Equal(1, SingleConstructionViewModel.ConstructorCallCount); + } } -class GarbageCollectionHeavySelfClosingPopup(MockPageViewModel viewModel, object? result = null) : MockSelfClosingPopup(viewModel, TimeSpan.FromMilliseconds(500), result) +sealed class GarbageCollectionHeavySelfClosingPopup(MockPageViewModel viewModel, object? result = null) : MockSelfClosingPopup(viewModel, TimeSpan.FromMilliseconds(500), result) { protected override void HandlePopupOpened(object? sender, EventArgs e) { @@ -532,6 +574,24 @@ protected override void HandlePopupClosed(object? sender, EventArgs e) } } +file class PopupViewModel : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + public Color? Color + { + get; + set + { + if (!Equals(value, field)) + { + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Color))); + } + } + } = new(); +} + sealed class LongLivedSelfClosingPopup(LongLivedMockPageViewModel viewModel) : MockSelfClosingPopup(viewModel, TimeSpan.FromMilliseconds(1500), "Long Lived"); sealed class ShortLivedSelfClosingPopup(ShortLivedMockPageViewModel viewModel) : MockSelfClosingPopup(viewModel, TimeSpan.FromMilliseconds(500), "Short Lived"); @@ -619,20 +679,20 @@ void IQueryAttributable.ApplyQueryAttributes(IDictionary query) sealed class MockPopup : Popup; -sealed file class PopupViewModel : INotifyPropertyChanged +sealed class SingleConstructionViewModel : MockPageViewModel { - public event PropertyChangedEventHandler? PropertyChanged; + public static int ConstructorCallCount { get; set; } - public Color? Color + public SingleConstructionViewModel() { - get; - set - { - if (!Equals(value, field)) - { - field = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Color))); - } - } - } = new(); + ConstructorCallCount++; + } +} + +sealed class SingleConstructionPopup : MockSelfClosingPopup +{ + public SingleConstructionPopup(SingleConstructionViewModel viewModel) : base(viewModel, TimeSpan.FromSeconds(2)) + { + BindingContext = viewModel; + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Services/PopupService.shared.cs b/src/CommunityToolkit.Maui/Services/PopupService.shared.cs index 104eff3e2f..a0ca614c67 100644 --- a/src/CommunityToolkit.Maui/Services/PopupService.shared.cs +++ b/src/CommunityToolkit.Maui/Services/PopupService.shared.cs @@ -180,12 +180,10 @@ public Task> ClosePopupAsync(INavigation navigation, T result View GetPopupContent() { - if (viewModelToViewMappings.TryGetValue(typeof(T), out var viewType)) + if (viewModelToViewMappings.TryGetValue(typeof(T), out var viewType) + && serviceProvider.GetRequiredService(viewType) is View content) { - if (serviceProvider.GetRequiredService(viewType) is View content) - { - return content; - } + return content; } var bindingContext = serviceProvider.GetRequiredService(typeof(T));