From c1488d0d6a02b4a1c27902c419c3cdaa0f4e6bf9 Mon Sep 17 00:00:00 2001 From: Vladislav Antonyuk Date: Wed, 15 Jan 2025 02:09:12 +0200 Subject: [PATCH] Fix tests --- .../MediaElement/MediaElementPage.xaml.cs | 2 +- .../Views/Popup/MultiplePopupPage.xaml.cs | 18 +-- .../Popup/PopupLayoutAlignmentPage.xaml.cs | 2 +- .../Popup/ShowPopupInOnAppearingPage.xaml.cs | 2 +- .../CustomSizeAndPositionPopupViewModel.cs | 2 +- .../Views/Popup/PopupAnchorViewModel.cs | 2 +- .../Views/Popup/PopupPositionViewModel.cs | 2 +- .../Views/Popup/PopupSizingIssuesViewModel.cs | 2 +- .../Views/Popup/StylePopupViewModel.cs | 14 +-- .../Popups/OpenedEventSimplePopup.xaml.cs | 2 +- .../BaseHandlerTest.cs | 2 +- .../Mocks/MockPageViewModel.cs | 4 +- .../Views/Popup/PopupServiceTests.cs | 68 +++++++--- .../Views/Popup/PopupTests.cs | 54 -------- .../Views/Popup/IPopupService.cs | 15 +-- .../Views/Popup/Popup.shared.cs | 39 +++++- .../Views/Popup/PopupContainer.cs | 76 +++++++---- .../Views/Popup/PopupExtensions.cs | 12 +- .../Views/Popup/PopupOptions.cs | 25 ++-- .../Views/Popup/PopupService.cs | 118 ++++++++++++++---- 20 files changed, 277 insertions(+), 184 deletions(-) delete mode 100644 src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupTests.cs diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml.cs index 6a2bb8c4b2..3523571485 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/MediaElement/MediaElementPage.xaml.cs @@ -266,7 +266,7 @@ async void DisplayPopup(object sender, EventArgs e) } }; - await Navigation.ShowPopup(popup, new PopupOptions()); + await Navigation.ShowPopup(popup, new PopupOptions()); popupMediaElement.Stop(); popupMediaElement.Handler?.DisconnectHandler(); } diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/MultiplePopupPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/MultiplePopupPage.xaml.cs index 6510703b47..fd3ad8d3da 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/MultiplePopupPage.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/MultiplePopupPage.xaml.cs @@ -19,48 +19,48 @@ public MultiplePopupPage(MultiplePopupViewModel multiplePopupViewModel, IPopupSe async void HandleSimplePopupButtonClicked(object sender, EventArgs e) { - await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); + await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); } async void HandleButtonPopupButtonClicked(object sender, EventArgs e) { - await popupService.ShowPopupAsync< ButtonPopup>(new PopupOptions(), CancellationToken.None); + await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); } async void HandleMultipleButtonPopupButtonClicked(object sender, EventArgs e) { - await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); + await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); } async void HandleNoOutsideTapDismissPopupClicked(object sender, EventArgs e) { - await popupService.ShowPopupAsync(new PopupOptions(){CanBeDismissedByTappingOutsideOfPopup = false}, CancellationToken.None); + await popupService.ShowPopupAsync(new PopupOptions(){CanBeDismissedByTappingOutsideOfPopup = false}, CancellationToken.None); } async void HandleToggleSizePopupButtonClicked(object sender, EventArgs e) { - await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); + await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); } async void HandleTransparentPopupButtonClicked(object sender, EventArgs e) { - await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); + await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); } async void HandleOpenedEventSimplePopupButtonClicked(object sender, EventArgs e) { - await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); + await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); } async void HandleReturnResultPopupButtonClicked(object sender, EventArgs e) { - var result = await popupService.ShowPopupAsync< ReturnResultPopup, bool>(new PopupOptions(), CancellationToken.None); + var result = await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); await DisplayAlert("Pop Result Returned", $"Result: {result.Result}", "OK"); } async void HandleXamlBindingPopupPopupButtonClicked(object sender, EventArgs e) { - await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); + await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); } } \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/PopupLayoutAlignmentPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/PopupLayoutAlignmentPage.xaml.cs index af4545da88..d80edc7a21 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/PopupLayoutAlignmentPage.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/PopupLayoutAlignmentPage.xaml.cs @@ -20,6 +20,6 @@ async void ShowPopupButtonClicked(object sender, EventArgs e) HeightRequest = double.Parse(heightEntry.Text) }; - await Navigation.ShowPopup(redBlueBoxPopup, new PopupOptions()); + await Navigation.ShowPopup(redBlueBoxPopup, new PopupOptions()); } } \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/ShowPopupInOnAppearingPage.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/ShowPopupInOnAppearingPage.xaml.cs index eac17a8d85..a481ab1389 100644 --- a/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/ShowPopupInOnAppearingPage.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/Pages/Views/Popup/ShowPopupInOnAppearingPage.xaml.cs @@ -23,6 +23,6 @@ protected override async void OnAppearing() { var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // Proves that we now support showing a popup before the platform is even ready. - await popupService.ShowPopupAsync(new PopupOptions(), cts.Token); + await popupService.ShowPopupAsync(new PopupOptions(), cts.Token); } } \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/CustomSizeAndPositionPopupViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/CustomSizeAndPositionPopupViewModel.cs index a5a2fb1c7a..e287c86de8 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/CustomSizeAndPositionPopupViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/CustomSizeAndPositionPopupViewModel.cs @@ -98,7 +98,7 @@ public Task ExecuteShowButton(CancellationToken token) HeightRequest = Height }; - return Shell.Current.Navigation.ShowPopup(popup, new PopupOptions()); + return Shell.Current.Navigation.ShowPopup(popup, new PopupOptions()); } static bool IsFlowDirectionSelectionValid(int flowDirectionSelection, int flowDirectionOptionsCount) diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/PopupAnchorViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/PopupAnchorViewModel.cs index 000acb6dcd..88c7f75d36 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/PopupAnchorViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/PopupAnchorViewModel.cs @@ -16,6 +16,6 @@ static async Task ShowPopup(View anchor) { }; - await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); + await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); } } \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/PopupPositionViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/PopupPositionViewModel.cs index c4a4a75157..3d40a60d36 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/PopupPositionViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/PopupPositionViewModel.cs @@ -15,7 +15,7 @@ static async Task DisplayPopup(PopupPosition position) { var popup = new TransparentPopup(); - await Page.Navigation.ShowPopup(popup, new PopupOptions()); + await Page.Navigation.ShowPopup(popup, new PopupOptions()); } public enum PopupPosition diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/PopupSizingIssuesViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/PopupSizingIssuesViewModel.cs index a60f37594b..6e1499335f 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/PopupSizingIssuesViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/PopupSizingIssuesViewModel.cs @@ -63,7 +63,7 @@ async Task OnShowPopup(Page page) popup.Content = container; - await page.Navigation.ShowPopup(popup, new PopupOptions()); + await page.Navigation.ShowPopup(popup, new PopupOptions()); } static Label GetContentLabel(in string text) => new() diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/StylePopupViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/StylePopupViewModel.cs index 44fb5197f2..229b45f17e 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/StylePopupViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Views/Popup/StylePopupViewModel.cs @@ -13,48 +13,48 @@ public partial class StylePopupViewModel : BaseViewModel static async Task DisplayImplicitStylePopup() { var popup = new ImplicitStylePopup(); - await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); + await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); } [RelayCommand] static async Task DisplayExplicitStylePopup() { var popup = new ExplicitStylePopup(); - await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); + await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); } [RelayCommand] static async Task DisplayDynamicStylePopup() { var popup = new DynamicStylePopup(); - await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); + await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); } [RelayCommand] static async Task DisplayApplyToDerivedTypesPopup() { var popup = new ApplyToDerivedTypesPopup(); - await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); + await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); } [RelayCommand] static async Task DisplayStyleInheritancePopup() { var popup = new StyleInheritancePopup(); - await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); + await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); } [RelayCommand] static async Task DisplayDynamicStyleInheritancePopup() { var popup = new DynamicStyleInheritancePopup(); - await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); + await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); } [RelayCommand] static async Task DisplayStyleClassPopup() { var popup = new StyleClassPopup(); - await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); + await MainPage.Navigation.ShowPopup(popup, new PopupOptions()); } } \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/Views/Popups/OpenedEventSimplePopup.xaml.cs b/samples/CommunityToolkit.Maui.Sample/Views/Popups/OpenedEventSimplePopup.xaml.cs index 3e53985396..f40c7f20e8 100644 --- a/samples/CommunityToolkit.Maui.Sample/Views/Popups/OpenedEventSimplePopup.xaml.cs +++ b/samples/CommunityToolkit.Maui.Sample/Views/Popups/OpenedEventSimplePopup.xaml.cs @@ -9,7 +9,7 @@ public partial class OpenedEventSimplePopup public OpenedEventSimplePopup() { InitializeComponent(); - OnOpened = async () => + OnOpened += async (s,e) => { await Task.Delay(TimeSpan.FromSeconds(1)); diff --git a/src/CommunityToolkit.Maui.UnitTests/BaseHandlerTest.cs b/src/CommunityToolkit.Maui.UnitTests/BaseHandlerTest.cs index b09f7676bb..c577fd5de3 100644 --- a/src/CommunityToolkit.Maui.UnitTests/BaseHandlerTest.cs +++ b/src/CommunityToolkit.Maui.UnitTests/BaseHandlerTest.cs @@ -59,7 +59,7 @@ static void InitializeServicesAndSetMockApplication(in IReadOnlyList trans var mockPageViewModel = new MockPageViewModel(); var mockPopup = new MockSelfClosingPopup(mockPageViewModel, new()); - PopupService.AddPopup(mockPopup, mockPageViewModel, appBuilder.Services, ServiceLifetime.Transient); + PopupService.AddPopup(mockPopup, mockPageViewModel, appBuilder.Services, ServiceLifetime.Singleton); #endregion foreach (var service in transientServicesToRegister) diff --git a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockPageViewModel.cs b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockPageViewModel.cs index 227359d82d..86c172c51b 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Mocks/MockPageViewModel.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Mocks/MockPageViewModel.cs @@ -1,4 +1,6 @@ -namespace CommunityToolkit.Maui.UnitTests.Mocks; +using CommunityToolkit.Maui.Core; + +namespace CommunityToolkit.Maui.UnitTests.Mocks; public class MockPageViewModel : BindableObject { diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupServiceTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupServiceTests.cs index 407ee5dc75..30f60c6d89 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupServiceTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupServiceTests.cs @@ -4,6 +4,7 @@ using CommunityToolkit.Maui.UnitTests.Views; using CommunityToolkit.Maui.Views; using FluentAssertions; +using Microsoft.Maui.Dispatching; using Xunit; namespace CommunityToolkit.Maui.UnitTests; @@ -13,23 +14,16 @@ public class PopupServiceTests : BaseHandlerTest public PopupServiceTests() { var page = new MockPage(new MockPageViewModel()); - var serviceCollection = new ServiceCollection(); - PopupService.AddPopup(serviceCollection, ServiceLifetime.Transient); - CreateViewHandler(page); - Assert.NotNull(Application.Current); Application.Current.Windows[0].Page = page; } [Fact] - public async Task ShowPopupAsyncWithNullOnPresentingShouldThrowArgumentNullException() + public async Task ShowPopupAsyncWithNotRegisteredServiceShouldThrowInvalidOperationException() { var popupService = ServiceProvider.GetRequiredService(); -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - await Assert.ThrowsAsync(() => - popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None)); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + await Assert.ThrowsAsync(() => popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None)); } [Fact(Timeout = (int)TestDuration.Short)] @@ -42,7 +36,7 @@ public async Task ShowPopupAsync_CancellationTokenExpired() // Ensure CancellationToken has expired await Task.Delay(100, CancellationToken.None); - await Assert.ThrowsAsync(() => popupService.ShowPopupAsync(new PopupOptions(), cts.Token)); + await Assert.ThrowsAsync(() => popupService.ShowPopupAsync(new PopupOptions(), cts.Token)); } [Fact(Timeout = (int)TestDuration.Short)] @@ -55,7 +49,7 @@ public async Task ShowPopupAsync_CancellationTokenCanceled() // Ensure CancellationToken has expired await cts.CancelAsync(); - await Assert.ThrowsAsync(() => popupService.ShowPopupAsync(new PopupOptions(), cts.Token)); + await Assert.ThrowsAsync(() => popupService.ShowPopupAsync(new PopupOptions(), cts.Token)); } [Fact(Timeout = (int)TestDuration.Medium)] @@ -65,7 +59,7 @@ public async Task ShowPopupAsyncShouldValidateProperBindingContext() var popupInstance = ServiceProvider.GetRequiredService(); var popupViewModel = ServiceProvider.GetRequiredService(); - await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); + await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); Assert.Same(popupInstance.BindingContext, popupViewModel); } @@ -86,7 +80,7 @@ public async Task ShowPopupAsyncWithOnPresenting_CancellationTokenExpired() // Ensure CancellationToken has expired await Task.Delay(100, CancellationToken.None); - await Assert.ThrowsAsync(() => popupService.ShowPopupAsync(new PopupOptions(){OnOpened = viewModel => viewModel.HasLoaded = true}, cts.Token)); + await Assert.ThrowsAsync(() => popupService.ShowPopupAsync(new PopupOptions(){OnOpened = viewModel => viewModel.HasLoaded = true}, cts.Token)); } [Fact(Timeout = (int)TestDuration.Short)] @@ -99,7 +93,7 @@ public async Task ShowPopupAsyncWithOnPresenting_CancellationTokenCanceled() // Ensure CancellationToken has expired await cts.CancelAsync(); - await Assert.ThrowsAsync(() => popupService.ShowPopupAsync(new PopupOptions() { OnOpened = viewModel => viewModel.HasLoaded = true }, cts.Token)); + await Assert.ThrowsAsync(() => popupService.ShowPopupAsync(new PopupOptions() { OnOpened = viewModel => viewModel.HasLoaded = true }, cts.Token)); } [Fact(Timeout = (int)TestDuration.Medium)] @@ -108,7 +102,10 @@ public async Task ShowPopupAsyncWithOnPresentingShouldBeInvoked() var popupService = ServiceProvider.GetRequiredService(); var popupViewModel = ServiceProvider.GetRequiredService(); - await popupService.ShowPopupAsync(new PopupOptions() { OnOpened = viewModel => viewModel.HasLoaded = true }, CancellationToken.None); + await popupService.ShowPopupAsync(new PopupOptions() + { + OnOpened = viewModel => viewModel.HasLoaded = true + }, CancellationToken.None); Assert.True(popupViewModel.HasLoaded); } @@ -119,9 +116,9 @@ public async Task ShowPopupAsyncShouldReturnResultOnceClosed() var mockPopup = ServiceProvider.GetRequiredService(); var popupService = ServiceProvider.GetRequiredService(); - var result = await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); + var result = await popupService.ShowPopupAsync(new PopupOptions(), CancellationToken.None); - Assert.Same(mockPopup.Result, result); + Assert.Same(mockPopup.Result, result.Result); } [Fact] @@ -130,19 +127,50 @@ public void ShowPopupWithOnPresentingShouldBeInvoked() var popupService = ServiceProvider.GetRequiredService(); var popupViewModel = ServiceProvider.GetRequiredService(); - popupService.ShowPopupAsync(new PopupOptions() { OnOpened = viewModel => viewModel.HasLoaded = true }, CancellationToken.None); + popupService.ShowPopupAsync(new PopupOptions() { OnOpened = viewModel => viewModel.HasLoaded = true }, CancellationToken.None); Assert.True(popupViewModel.HasLoaded); } } -sealed class MockSelfClosingPopup : Popup +sealed class MockSelfClosingPopup : Popup { public MockSelfClosingPopup(MockPageViewModel viewModel, object? result = null) { BindingContext = viewModel; Result = result; + OnOpened += MockSelfClosingPopup_OnOpened; } - public new object? Result { get; } + void MockSelfClosingPopup_OnOpened(object? sender, EventArgs e) + { + var timer = Dispatcher.CreateTimer(); + timer.Interval = TimeSpan.FromMilliseconds(500); + timer.Tick += (s, e) => Close(Result); + timer.Start(); + } + + public object? Result { get; } +} + +public class MockPopup : Popup +{ +} + +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(); } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupTests.cs b/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupTests.cs deleted file mode 100644 index cbf0e6e185..0000000000 --- a/src/CommunityToolkit.Maui.UnitTests/Views/Popup/PopupTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.ComponentModel; -using CommunityToolkit.Maui.Core; -using CommunityToolkit.Maui.UnitTests.Mocks; -using CommunityToolkit.Maui.Views; -using Xunit; - -namespace CommunityToolkit.Maui.UnitTests.Views; - -public class PopupTests : BaseHandlerTest -{ - public PopupTests() - { - Assert.IsType(new MockPopup(), exactMatch: false); - } - - [Fact] - public void NullColorThrowsArgumentNullException() - { - var popupViewModel = new PopupViewModel(); - var popupWithBinding = new PopupContainer - { - BindingContext = popupViewModel - }; - popupWithBinding.SetBinding(PopupContainer.BackgroundColorProperty, nameof(PopupViewModel.Color)); - -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - Assert.Throws(() => new PopupContainer { BackgroundColor = null }); - Assert.Throws(() => new PopupContainer().BackgroundColor = null); - Assert.Throws(() => popupViewModel.Color = null); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - } -} - -public class MockPopup : Popup -{ -} - -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(); -} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Views/Popup/IPopupService.cs b/src/CommunityToolkit.Maui/Views/Popup/IPopupService.cs index 8b6bf72c9d..18e4805038 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/IPopupService.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/IPopupService.cs @@ -1,7 +1,4 @@ -using System.ComponentModel; -using Microsoft.Maui.Primitives; - -namespace CommunityToolkit.Maui.Core; +namespace CommunityToolkit.Maui.Core; /// /// Provides a mechanism for displaying Popups based on the underlying view model. @@ -13,7 +10,7 @@ public interface IPopupService /// /// /// - Task ShowPopupAsync(PopupOptions options, CancellationToken cancellationToken) + Task ShowPopupAsync(PopupOptions options, CancellationToken cancellationToken) where TBindingContext : notnull; /// @@ -22,11 +19,15 @@ Task ShowPopupAsync(PopupOptions options, Cancella /// /// /// - Task> ShowPopupAsync(PopupOptions options, CancellationToken cancellationToken) + Task> ShowPopupAsync(PopupOptions options, CancellationToken cancellationToken) where TBindingContext : notnull; /// /// /// - void ClosePopup(); + Task ClosePopup(); + /// + /// + /// + Task ClosePopup(T result); } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs b/src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs index 4694c15add..df22a087f3 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/Popup.shared.cs @@ -13,12 +13,12 @@ public partial class Popup : ContentView /// /// /// - public Action? OnOpened { get; set; } + public event EventHandler? OnOpened; /// /// /// - public Action? OnClosed { get; set; } + public event EventHandler? OnClosed; /// /// @@ -37,11 +37,40 @@ public void Close(T result) popupContainer?.Close(new PopupResult(result, false)); } - internal void SetPopup(PopupContainer container, PopupOptions options) + internal void SetPopup(PopupContainer container) { popupContainer = container; + } + + internal void NotifyPopupIsOpened() + { + OnOpened?.Invoke(this, EventArgs.Empty); + } + + internal void NotifyPopupIsClosed() + { + OnClosed?.Invoke(this, EventArgs.Empty); + } +} + +/// +/// Represents a small View that pops up at front the Page. Implements . +/// +public partial class Popup : Popup +{ + PopupContainer? popupContainer; - OnOpened = options.OnOpened; - OnClosed = options.OnClosed; + /// + /// + /// + /// + public void Close(T result) + { + popupContainer?.Close(new PopupResult(result, false)); + } + + internal void SetPopup(PopupContainer container) + { + popupContainer = container; } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupContainer.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupContainer.cs index e285d26c5c..42ef882f41 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/PopupContainer.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/PopupContainer.cs @@ -4,13 +4,56 @@ namespace CommunityToolkit.Maui.Views; +partial class PopupContainer : PopupContainer +{ + readonly Popup content; + readonly TaskCompletionSource> taskCompletionSource; + + /// + /// + /// + /// + /// + public PopupContainer(Popup content, TaskCompletionSource> taskCompletionSource) :base (content, null) + { + this.content = content; + this.taskCompletionSource = taskCompletionSource; + content.SetPopup(this); + Shell.SetPresentationMode(this, PresentationMode.ModalNotAnimated); + On().SetModalPresentationStyle(UIModalPresentationStyle.OverFullScreen); + } + + /// + /// + /// + /// + protected override bool OnBackButtonPressed() + { + return !CanBeDismissedByTappingOutsideOfPopup; + } + + public async Task Close(PopupResult result) + { + taskCompletionSource.SetResult(result); + await Navigation.PopModalAsync(); + } +} + partial class PopupContainer : ContentPage { + readonly Popup content; + readonly TaskCompletionSource? taskCompletionSource; + /// /// /// - public PopupContainer() + /// + /// + public PopupContainer(Popup content, TaskCompletionSource? taskCompletionSource) { + this.content = content; + this.taskCompletionSource = taskCompletionSource; + content.SetPopup(this); Shell.SetPresentationMode(this, PresentationMode.ModalNotAnimated); On().SetModalPresentationStyle(UIModalPresentationStyle.OverFullScreen); } @@ -32,43 +75,28 @@ protected override bool OnBackButtonPressed() /// On Android - when false the hardware back button is disabled. /// public bool CanBeDismissedByTappingOutsideOfPopup { get; internal set; } - + /// /// /// - protected sealed override void OnAppearing() + protected override void OnAppearing() { base.OnAppearing(); - if (Content is Popup popup) - { - popup.OnOpened?.Invoke(); - } + content.NotifyPopupIsOpened(); } /// /// /// - protected sealed override void OnDisappearing() + protected override void OnDisappearing() { - if (Content is Popup popup) - { - popup.OnClosed?.Invoke(); - } - + content.NotifyPopupIsClosed(); base.OnDisappearing(); } - public void Close(PopupResult result) - { - - } - - /// - /// - /// - /// - public void Close(PopupResult result) + public async Task Close(PopupResult result) { - + taskCompletionSource?.SetResult(result); + await Navigation.PopModalAsync(); } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupExtensions.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupExtensions.cs index 451db04aec..f3c2fcc11f 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/PopupExtensions.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/PopupExtensions.cs @@ -14,9 +14,9 @@ public static class PopupExtensions /// /// /// - /// /// - public static async Task ShowPopup(this INavigation navigation, Popup popup, PopupOptions options, Action? onTappingOutsideOfPopup = null) + public static async Task ShowPopup(this INavigation navigation, Popup popup, PopupOptions options) + where T:Popup { TaskCompletionSource taskCompletionSource = new(); @@ -27,14 +27,13 @@ public static async Task ShowPopup(this INavigation navigation, Popup popup, Pop Content = popup } }; - var popupContainer = new PopupContainer + var popupContainer = new PopupContainer(popup, taskCompletionSource) { BackgroundColor = options.BackgroundColor ?? Color.FromRgba(0, 0, 0, 0.4), // https://rgbacolorpicker.com/rgba-to-hex, CanBeDismissedByTappingOutsideOfPopup = options.CanBeDismissedByTappingOutsideOfPopup, Content = view, BindingContext = popup.BindingContext }; - popup.SetPopup(popupContainer, options); view.BindingContext = popup.BindingContext; @@ -44,9 +43,8 @@ public static async Task ShowPopup(this INavigation navigation, Popup popup, Pop { Command = new Command(async () => { - onTappingOutsideOfPopup?.Invoke(); - taskCompletionSource.SetResult(new PopupResult(true)); - await navigation.PopModalAsync(); + options.OnTappingOutsideOfPopup?.Invoke(); + await popupContainer.Close(new PopupResult(true)); }) }); } diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupOptions.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupOptions.cs index db0c21ee1e..16b25c7d39 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/PopupOptions.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/PopupOptions.cs @@ -3,41 +3,30 @@ /// /// /// -public class PopupOptions : PopupOptions +public class PopupOptions { /// /// /// - public new Action? OnOpened { get; init; } + public bool CanBeDismissedByTappingOutsideOfPopup { get; init; } /// /// /// - public new Action? OnClosed { get; init; } -} - -/// -/// -/// -public class PopupOptions -{ - /// - /// - /// - public bool CanBeDismissedByTappingOutsideOfPopup { get; init; } - + public Color? BackgroundColor { get; init; } + /// /// /// - public Color? BackgroundColor { get; init; } + public Action? OnOpened { get; set; } /// /// /// - public Action? OnOpened { get; init; } + public Action? OnClosed { get; set; } /// /// /// - public Action? OnClosed { get; init; } + public Action? OnTappingOutsideOfPopup { get; set; } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui/Views/Popup/PopupService.cs b/src/CommunityToolkit.Maui/Views/Popup/PopupService.cs index 6c4e1f95c4..2ad53c6451 100644 --- a/src/CommunityToolkit.Maui/Views/Popup/PopupService.cs +++ b/src/CommunityToolkit.Maui/Views/Popup/PopupService.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using CommunityToolkit.Maui.Core; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace CommunityToolkit.Maui.Views; @@ -49,22 +50,22 @@ public PopupService() where TPopupView : IView where TPopupViewModel : notnull { - viewModelToViewMappings.Add(typeof(TPopupViewModel), typeof(TPopupView)); + viewModelToViewMappings.TryAdd(typeof(TPopupViewModel), typeof(TPopupView)); Routing.RegisterRoute(typeof(TPopupViewModel).FullName, typeof(TPopupView)); - services.Add(new ServiceDescriptor(typeof(TPopupView), typeof(TPopupView), lifetime)); - services.Add(new ServiceDescriptor(typeof(TPopupViewModel), typeof(TPopupViewModel), lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(TPopupView), typeof(TPopupView), lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(TPopupViewModel), typeof(TPopupViewModel), lifetime)); } internal static void AddPopup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TPopupView, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TPopupViewModel>(TPopupView popup, TPopupViewModel viewModel, IServiceCollection services, ServiceLifetime lifetime) where TPopupView : IView where TPopupViewModel : notnull { - viewModelToViewMappings.Add(typeof(TPopupViewModel), typeof(TPopupView)); + viewModelToViewMappings.TryAdd(typeof(TPopupViewModel), typeof(TPopupView)); Routing.RegisterRoute(typeof(TPopupViewModel).FullName, typeof(TPopupView)); - services.Add(new ServiceDescriptor(typeof(TPopupView), () => popup, lifetime)); - services.Add(new ServiceDescriptor(typeof(TPopupViewModel), () => viewModel, lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(TPopupView), (_) => popup, lifetime)); + services.TryAdd(new ServiceDescriptor(typeof(TPopupViewModel), (_) => viewModel, lifetime)); } void EnsureMainThreadIsUsed([CallerMemberName] string? callerName = null) @@ -82,14 +83,14 @@ void EnsureMainThreadIsUsed([CallerMemberName] string? callerName = null) /// /// /// - public async Task ShowPopupAsync(PopupOptions options, CancellationToken cancellationToken) + public async Task ShowPopupAsync(PopupOptions options, CancellationToken cancellationToken) where TBindingContext : notnull { TaskCompletionSource taskCompletionSource = new(); var bindingContext = serviceProvider.GetRequiredService(); - var popup = GetPopup(options, bindingContext, () => taskCompletionSource.SetResult(new PopupResult(true))); + var popup = GetPopup(options, bindingContext, taskCompletionSource); await ShowPopup(popup); - return await taskCompletionSource.Task; + return await taskCompletionSource.Task.WaitAsync(cancellationToken); } /// @@ -100,20 +101,36 @@ public async Task ShowPopupAsync(PopupOptions opti /// /// /// - public async Task> ShowPopupAsync(PopupOptions options, CancellationToken cancellationToken) + public async Task> ShowPopupAsync(PopupOptions options, CancellationToken cancellationToken) where TBindingContext : notnull { TaskCompletionSource> taskCompletionSource = new(); var bindingContext = serviceProvider.GetRequiredService(); - var popup = GetPopup(options, bindingContext, () => taskCompletionSource.SetResult(new PopupResult(default, true))); + var popup = GetPopup(options, bindingContext, taskCompletionSource); await ShowPopup(popup); - return await taskCompletionSource.Task; + return await taskCompletionSource.Task.WaitAsync(cancellationToken); + } + + /// + /// + /// + public async Task ClosePopup() + { + var popupLifecycleController = serviceProvider.GetRequiredService(); + var popup = popupLifecycleController.GetCurrentPopup(); + if (popup is null) + { + return; + } + + await popup.Close(new PopupResult(false)); + popupLifecycleController.UnregisterPopup(popup); } /// /// /// - public void ClosePopup() + public async Task ClosePopup(T result) { var popupLifecycleController = serviceProvider.GetRequiredService(); var popup = popupLifecycleController.GetCurrentPopup(); @@ -122,7 +139,7 @@ public void ClosePopup() return; } - popup.Close(new PopupResult(false)); + await popup.Close(new PopupResult(result, false)); popupLifecycleController.UnregisterPopup(popup); } @@ -135,11 +152,10 @@ async Task ShowPopup(PopupContainer popupContainer) await navigation.PushModalAsync(popupContainer); } - PopupContainer GetPopup(PopupOptions options, object bindingContext, Action onTappingOutsideOfPopup) + PopupContainer GetPopup(PopupOptions options, TBindingContext bindingContext, TaskCompletionSource taskCompletionSource) { - var content = GetPopupContent(bindingContext); - var navigation = Application.Current?.Windows[^1].Page?.Navigation ?? throw new InvalidOperationException("Unable to get navigation"); - + var content = GetPopupContent(bindingContext); + var view = new Grid() { new Border() @@ -147,15 +163,17 @@ PopupContainer GetPopup(PopupOptions options, object bindingCon Content = content } }; - var popup = new PopupContainer + var popup = new PopupContainer(content, taskCompletionSource) { BackgroundColor = options.BackgroundColor ?? Color.FromRgba(0, 0, 0, 0.4), // https://rgbacolorpicker.com/rgba-to-hex, CanBeDismissedByTappingOutsideOfPopup = options.CanBeDismissedByTappingOutsideOfPopup, Content = view, BindingContext = bindingContext }; - content.SetPopup(popup, options); + popup.Appearing += (s, e) => options.OnOpened?.Invoke(bindingContext); + popup.Disappearing += (s, e) => options.OnClosed?.Invoke(bindingContext); + view.BindingContext = bindingContext; if (options.CanBeDismissedByTappingOutsideOfPopup) @@ -164,8 +182,8 @@ PopupContainer GetPopup(PopupOptions options, object bindingCon { Command = new Command(async () => { - onTappingOutsideOfPopup(); - await navigation.PopModalAsync(); + options.OnTappingOutsideOfPopup?.Invoke(); + await popup.Close(new PopupResult(true)); }) }); } @@ -173,7 +191,7 @@ PopupContainer GetPopup(PopupOptions options, object bindingCon return popup; } - Popup GetPopupContent(object bindingContext) + Popup GetPopupContent(TBindingContext bindingContext) { if (bindingContext is Popup view) { @@ -187,4 +205,58 @@ Popup GetPopupContent(object bindingContext) throw new InvalidOperationException($"Could not locate a view for {typeof(TBindingContext).FullName}"); } + + PopupContainer GetPopup(PopupOptions options, TBindingContext bindingContext, TaskCompletionSource> taskCompletionSource) + { + var content = GetPopupContent(bindingContext); + + var view = new Grid() + { + new Border() + { + Content = content + } + }; + var popup = new PopupContainer(content, taskCompletionSource) + { + BackgroundColor = options.BackgroundColor ?? Color.FromRgba(0, 0, 0, 0.4), // https://rgbacolorpicker.com/rgba-to-hex, + CanBeDismissedByTappingOutsideOfPopup = options.CanBeDismissedByTappingOutsideOfPopup, + Content = view, + BindingContext = bindingContext + }; + + popup.Appearing += (s, e) => options.OnOpened?.Invoke(bindingContext); + popup.Disappearing += (s, e) => options.OnClosed?.Invoke(bindingContext); + + view.BindingContext = bindingContext; + + if (options.CanBeDismissedByTappingOutsideOfPopup) + { + view.GestureRecognizers.Add(new TapGestureRecognizer() + { + Command = new Command(async () => + { + options.OnTappingOutsideOfPopup?.Invoke(); + await popup.Close(new PopupResult(true)); + }) + }); + } + + return popup; + } + + Popup GetPopupContent(TBindingContext bindingContext) + { + if (bindingContext is Popup view) + { + return view; + } + + if (serviceProvider.GetRequiredService(viewModelToViewMappings[typeof(TBindingContext)]) is Popup content) + { + return content; + } + + throw new InvalidOperationException($"Could not locate a view for {typeof(TBindingContext).FullName}"); + } } \ No newline at end of file