diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs index 834b8b3eb963..b9ce63fe59d2 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs @@ -346,7 +346,7 @@ protected virtual void OnPageSelected(int position) // This mainly happens if all of the items that are part of this shell section // vanish. Android calls `OnPageSelected` with position zero even though the view pager is // empty - if (visibleItems.Count >= position) + if (position >= visibleItems.Count) return; var shellContent = visibleItems[position]; diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Android.cs index 70f9a87f1d57..c3904ac73c06 100644 --- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Android.cs @@ -6,6 +6,7 @@ using Android.Views; using AndroidX.AppCompat.Widget; using AndroidX.DrawerLayout.Widget; +using AndroidX.ViewPager2.Widget; using Google.Android.Material.AppBar; using Microsoft.Maui.Controls; using Microsoft.Maui.Controls.Handlers.Compatibility; @@ -361,5 +362,26 @@ RecyclerViewContainer GetFlyoutMenuReyclerView(ShellRenderer shellRenderer) return flyoutContainer ?? throw new Exception("RecyclerView not found"); } + + async Task TapToSelect(ContentPage page) + { + var shellContent = page.Parent as ShellContent; + var shellSection = shellContent.Parent as ShellSection; + var shellItem = shellSection.Parent as ShellItem; + var shell = shellItem.Parent as Shell; + await OnNavigatedToAsync(shell.CurrentPage); + + if (shellItem != shell.CurrentItem) + throw new NotImplementedException(); + + if (shellSection != shell.CurrentItem.CurrentItem) + throw new NotImplementedException(); + + var pagerParent = (shell.CurrentPage.Handler as IPlatformViewHandler) + .PlatformView.GetParentOfType(); + + pagerParent.CurrentItem = shellSection.Items.IndexOf(shellContent); + await OnNavigatedToAsync(page); + } } } diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Windows.cs index 9d31c2e51c14..bc7081c60994 100644 --- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Windows.cs @@ -580,5 +580,55 @@ await CreateHandlerAndAddToWindow(shell, async (handler) => Assert.Equal((rootView.SelectedItem as NavigationViewItemViewModel).Data, flyoutItems[0][1]); }); } + + async Task TapToSelect(ContentPage page) + { + var shellContent = page.Parent as ShellContent; + var shellSection = shellContent.Parent as ShellSection; + var shellItem = shellSection.Parent as ShellItem; + var shell = shellItem.Parent as Shell; + + await OnNavigatedToAsync(shell.CurrentPage); + + if (shellItem != shell.CurrentItem) + throw new NotImplementedException(); + + if (shellSection != shell.CurrentItem.CurrentItem) + throw new NotImplementedException(); + + var mauiNavigationView = shellItem.Handler.PlatformView as MauiNavigationView; + var navSource = mauiNavigationView.MenuItemsSource as IEnumerable; + + bool found = false; + foreach (NavigationViewItemViewModel item in navSource) + { + if (item.Data == shellContent) + { + mauiNavigationView.SelectedItem = item; + found = true; + break; + } + else if (item.MenuItemsSource is IEnumerable children) + { + foreach (NavigationViewItemViewModel childContent in children) + { + if (childContent.Data == shellContent) + { + mauiNavigationView.SelectedItem = childContent; + found = true; + break; + } + } + } + + if (found) + break; + } + + if (!found) + throw new InvalidOperationException("Unable to locate page inside platform shell components"); + + await OnNavigatedToAsync(page); + } } } \ No newline at end of file diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs index b8ffeddb44c7..6240f5bb1475 100644 --- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs @@ -635,6 +635,45 @@ await CreateHandlerAndAddToWindow(shell, async (handler) => }); } + [Fact(DisplayName = "LifeCycleEvents Fire When Navigating Top Tabs")] + public async Task LifeCycleEventsFireWhenNavigatingTopTabs() + { + SetupBuilder(); + + var page1 = new LifeCycleTrackingPage() { Content = new Label() { Padding = 40, Text = "Page 1", Background = SolidColorBrush.Purple } }; + var page2 = new LifeCycleTrackingPage() { Content = new Label() { Padding = 40, Text = "Page 2", Background = SolidColorBrush.Green } }; + + var shell = await CreateShellAsync((shell) => + { + shell.Items.Add(new Tab() + { + Items = + { + new ShellContent() + { + Title = "Tab 1", + Content = page1 + }, + new ShellContent() + { + Title = "Tab 2", + Content = page2 + }, + } + }); + }); + + await CreateHandlerAndAddToWindow(shell, async (handler) => + { + await OnNavigatedToAsync(page1); + + Assert.Equal(0, page2.OnNavigatedToCount); + await TapToSelect(page2); + Assert.Equal(page2.Parent, shell.CurrentItem.CurrentItem.CurrentItem); + page2.AssertLifeCycleCounts(); + }); + } + protected Task CreateShellAsync(Action action) => InvokeOnMainThreadAsync(() => { diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.iOS.cs index caa04c5c7055..75e8a9a48ab4 100644 --- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.iOS.cs @@ -1,8 +1,10 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Foundation; using Microsoft.Maui.Controls; using Microsoft.Maui.Controls.Handlers.Compatibility; using Microsoft.Maui.Controls.Platform; @@ -10,6 +12,7 @@ using Microsoft.Maui.Controls.PlatformConfiguration; using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; using Microsoft.Maui.Platform; +using UIKit; using Xunit; namespace Microsoft.Maui.DeviceTests @@ -32,7 +35,7 @@ public async Task SwipingAwayModalPropagatesToShell() await CreateHandlerAndAddToWindow(shell, async (handler) => { var modalPage = new ContentPage(); - modalPage.On().SetModalPresentationStyle(UIModalPresentationStyle.FormSheet); + modalPage.On().SetModalPresentationStyle(Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle.FormSheet); var platformWindow = MauiContext.GetPlatformWindow().RootViewController; await shell.Navigation.PushModalAsync(modalPage); @@ -73,7 +76,7 @@ public async Task SwipingAwayModalRemovesEntireNavigationPage() await CreateHandlerAndAddToWindow(shell, async (handler) => { var modalPage = new Controls.NavigationPage(new ContentPage()); - modalPage.On().SetModalPresentationStyle(UIModalPresentationStyle.FormSheet); + modalPage.On().SetModalPresentationStyle(Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle.FormSheet); var platformWindow = MauiContext.GetPlatformWindow().RootViewController; await shell.Navigation.PushModalAsync(modalPage); @@ -197,6 +200,43 @@ void ShellNavigating(object sender, ShellNavigatingEventArgs e) }); } + async Task TapToSelect(ContentPage page) + { + var shellContent = page.Parent as ShellContent; + var shellSection = shellContent.Parent as ShellSection; + var shellItem = shellSection.Parent as ShellItem; + var shell = shellItem.Parent as Shell; + await OnNavigatedToAsync(shell.CurrentPage); + + if (shellItem != shell.CurrentItem) + throw new NotImplementedException(); + + if (shellSection != shell.CurrentItem.CurrentItem) + throw new NotImplementedException(); + + var pagerParent = (shell.CurrentPage.Handler as IPlatformViewHandler) + .PlatformView.FindParent(x => x.NextResponder is UITabBarController); + + var tabController = pagerParent.NextResponder as ShellItemRenderer; + + var section = tabController.SelectedViewController as ShellSectionRenderer; + + var rootCV = section.ViewControllers[0] as + ShellSectionRootRenderer; + + var rootHeader = rootCV.ChildViewControllers + .OfType() + .First(); + + var newIndex = shellSection.Items.IndexOf(shellContent); + + await Task.Delay(100); + + rootHeader.ItemSelected(rootHeader.CollectionView, NSIndexPath.FromItemSection((int)newIndex, 0)); + + await OnNavigatedToAsync(page); + } + class ModalShellPage : ContentPage { public ModalShellPage() diff --git a/src/Controls/tests/DeviceTests/TestClasses/LifeCycleTrackingPage.cs b/src/Controls/tests/DeviceTests/TestClasses/LifeCycleTrackingPage.cs new file mode 100644 index 000000000000..a08886283243 --- /dev/null +++ b/src/Controls/tests/DeviceTests/TestClasses/LifeCycleTrackingPage.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Xunit; + +namespace Microsoft.Maui.DeviceTests +{ + public class LifeCycleTrackingPage : ContentPage + { + public NavigatedFromEventArgs NavigatedFromArgs { get; private set; } + public NavigatingFromEventArgs NavigatingFromArgs { get; private set; } + public NavigatedToEventArgs NavigatedToArgs { get; private set; } + public int AppearingCount { get; private set; } + public int DisappearingCount { get; private set; } + public int OnNavigatedToCount { get; private set; } + public int OnNavigatingFromCount { get; private set; } + public int OnNavigatedFromCount { get; private set; } + + public void ClearNavigationArgs() + { + NavigatedFromArgs = null; + NavigatingFromArgs = null; + NavigatedToArgs = null; + } + + protected override void OnAppearing() + { + base.OnAppearing(); + AppearingCount++; + } + + protected override void OnDisappearing() + { + base.OnDisappearing(); + DisappearingCount++; + } + + protected override void OnNavigatedFrom(NavigatedFromEventArgs args) + { + base.OnNavigatedFrom(args); + NavigatedFromArgs = args; + OnNavigatedFromCount++; + } + + protected override void OnNavigatingFrom(NavigatingFromEventArgs args) + { + base.OnNavigatingFrom(args); + NavigatingFromArgs = args; + OnNavigatingFromCount++; + } + + protected override void OnNavigatedTo(NavigatedToEventArgs args) + { + base.OnNavigatedTo(args); + NavigatedToArgs = args; + OnNavigatedToCount++; + } + + public void AssertLifeCycleCounts() + { + if (!this.HasAppeared) + { + Assert.Equal(AppearingCount, DisappearingCount); + Assert.Equal(DisappearingCount, OnNavigatedToCount); + Assert.Equal(OnNavigatingFromCount, OnNavigatedToCount); + } + else + { + Assert.Equal(AppearingCount, OnNavigatedToCount); + Assert.Equal(DisappearingCount, AppearingCount - 1); + Assert.Equal(OnNavigatingFromCount, AppearingCount - 1); + } + + Assert.Equal(DisappearingCount, OnNavigatingFromCount); + } + } +}