From a4054803e5b1240f27f0de05be28304c7c5bd9db Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Fri, 5 Apr 2024 07:11:01 -0600 Subject: [PATCH 1/7] fix: enhancing lookup logic for Select Tab --- .../Navigation/PageNavigationService.cs | 53 +++++++++++++------ .../Fixtures/Navigation/NavigationTests.cs | 23 ++++++++ 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/src/Maui/Prism.Maui/Navigation/PageNavigationService.cs b/src/Maui/Prism.Maui/Navigation/PageNavigationService.cs index 852572548..4ef443486 100644 --- a/src/Maui/Prism.Maui/Navigation/PageNavigationService.cs +++ b/src/Maui/Prism.Maui/Navigation/PageNavigationService.cs @@ -973,39 +973,58 @@ private void TabbedPageSelectRootTab(TabbedPage tabbedPage, string selectedTab) { var registry = Registry; var selectRegistration = registry.Registrations.FirstOrDefault(x => x.Name == selectedTab); + Page child = null; if (selectRegistration is null) - throw new KeyNotFoundException($"No Registration found to select tab '{selectedTab}'."); - - var child = tabbedPage.Children - .FirstOrDefault(x => IsPage(x, selectRegistration)); - if (child is not null) { - tabbedPage.CurrentPage = child; + child = tabbedPage.Children.FirstOrDefault(x => x.GetType().Name == selectedTab) ?? + throw new KeyNotFoundException($"No Registration found to select tab '{selectedTab}'."); + } + else + { + child = tabbedPage.Children.FirstOrDefault(x => IsPage(x, selectRegistration, selectedTab)) ?? + throw new KeyNotFoundException($"No Child Page was found with the key '{selectedTab}'."); } - } - private static bool IsPage(Page page, ViewRegistration registration) => - (string)page.GetValue(ViewModelLocator.NavigationNameProperty) == registration.Name || page.GetType() == registration.View; + tabbedPage.CurrentPage = child; + } private void TabbedPageSelectNavigationChildTab(TabbedPage tabbedPage, string rootTab, string selectedTab) { var registry = Registry; var rootRegistration = registry.Registrations.FirstOrDefault(x => x.Name == rootTab); var selectRegistration = registry.Registrations.FirstOrDefault(x => x.Name == selectedTab); - if (rootRegistration is null) - throw new KeyNotFoundException($"No Registration found to select tab '{rootTab}'."); - else if (selectRegistration is null) - throw new KeyNotFoundException($"No Registration found to select tab '{selectedTab}'."); - else if (!rootRegistration.View.IsAssignableTo(typeof(NavigationPage))) - throw new InvalidOperationException($"Could not select Tab with a root type '{rootRegistration.View.FullName}'. This must inherit from NavigationPage."); - var child = tabbedPage.Children - .FirstOrDefault(x => x is NavigationPage navPage && IsPage(x, rootRegistration) && (IsPage(navPage.RootPage, selectRegistration) || IsPage(navPage.CurrentPage, selectRegistration))); + var candidates = tabbedPage.Children + .OfType() + .Where(x => IsPage(x, rootRegistration, rootTab)); + var child = candidates.SingleOrDefault(x => IsPage(x.RootPage, selectRegistration, selectedTab)) ?? + candidates.SingleOrDefault(x => IsPage(x.CurrentPage, selectRegistration, selectedTab)); if (child is not null) tabbedPage.CurrentPage = child; } + // This provides a fallback if we did not find a registration for the Page + private static bool IsPage(Page referencePage, string name) => + ViewModelLocator.GetNavigationName(referencePage) == name || referencePage.GetType().Name == name || referencePage.GetType().FullName == name; + + private static bool IsPage(Page referencePage, ViewRegistration registration, string name) + { + var referenceType = referencePage.GetType(); + if (registration is not null) + { + // We're allowing an empty string here for cases where someone has a manually constructed TabbedPage + var navigationName = ViewModelLocator.GetNavigationName(referencePage); + if (registration.View == referenceType && (string.IsNullOrEmpty(navigationName) || navigationName == name)) + return true; + + // This is an override for cases where someone may have a NavigationPage + return name == nameof(NavigationPage) && referenceType == typeof(NavigationPage); + } + + return IsPage(referencePage, name); + } + protected virtual async Task UseReverseNavigation(Page currentPage, string nextSegment, Queue segments, INavigationParameters parameters, bool? useModalNavigation, bool? animated) { var navigationStack = new Stack(); diff --git a/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs b/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs index 72ec73e17..7b9526584 100644 --- a/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs +++ b/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs @@ -702,6 +702,29 @@ public async Task DeepLinked_ModalNavigationPage_GoesBackToPreviousPage_AsTabbed Assert.IsType(window.CurrentPage); } + [Theory] + [InlineData("NavigationPage|MockViewB", typeof(MockViewB))] + [InlineData("MockViewC", typeof(MockViewC))] + public void Navigate_And_SelectTab(string selectTab, Type viewType) + { + var mauiApp = CreateBuilder(prism => prism + .CreateWindow(n => n.NavigateAsync($"MockExplicitTabbedPage?{KnownNavigationParameters.SelectedTab}={selectTab}"))) + .Build(); + var window = GetWindow(mauiApp); + var page = window.Page; + + Assert.IsType(page); + var tabbed = page as MockExplicitTabbedPage; + + var child = tabbed.CurrentPage; + if (child is NavigationPage navPage) + { + child = navPage.RootPage; + } + + Assert.IsType(viewType, child); + } + private static void TestPage(Page page, bool ignoreNavigationPage = false) { Assert.NotNull(page.BindingContext); From 1afbb9af6c281191d3ffedce99f4a37a8c4821be Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Fri, 5 Apr 2024 08:59:27 -0600 Subject: [PATCH 2/7] fix: when going back to named view should use GoBackTo --- e2e/Maui/MauiModule/ViewModels/ViewModelBase.cs | 2 +- .../Prism.Maui/Navigation/Builder/INavigationBuilder.cs | 6 +++--- .../Prism.Maui/Navigation/Builder/NavigationBuilder.cs | 4 ++-- .../Navigation/Builder/NavigationBuilderExtensions.cs | 9 +++++++++ src/Maui/Prism.Maui/Navigation/INavigationService.cs | 2 +- .../Navigation/INavigationServiceExtensions.cs | 2 +- src/Maui/Prism.Maui/Navigation/PageNavigationService.cs | 2 +- .../Fixtures/Navigation/NavigationTests.cs | 8 ++++---- 8 files changed, 22 insertions(+), 13 deletions(-) diff --git a/e2e/Maui/MauiModule/ViewModels/ViewModelBase.cs b/e2e/Maui/MauiModule/ViewModels/ViewModelBase.cs index 7c3b7b7a4..a429feabc 100644 --- a/e2e/Maui/MauiModule/ViewModels/ViewModelBase.cs +++ b/e2e/Maui/MauiModule/ViewModels/ViewModelBase.cs @@ -80,7 +80,7 @@ private void DialogCallback(IDialogResult result) => private void OnGoBack(string viewName) { Messages.Add($"On Go Back {viewName}"); - _navigationService.GoBackAsync(viewName); + _navigationService.GoBackToAsync(viewName); } public void Initialize(INavigationParameters parameters) diff --git a/src/Maui/Prism.Maui/Navigation/Builder/INavigationBuilder.cs b/src/Maui/Prism.Maui/Navigation/Builder/INavigationBuilder.cs index 6da7da5be..57f261eb0 100644 --- a/src/Maui/Prism.Maui/Navigation/Builder/INavigationBuilder.cs +++ b/src/Maui/Prism.Maui/Navigation/Builder/INavigationBuilder.cs @@ -62,11 +62,11 @@ public interface INavigationBuilder INavigationBuilder UseRelativeNavigation(); /// - /// Navigates back to the previous view model asynchronously. + /// Navigates back to the specified view asynchronously. /// - /// The type of the view model to navigate back to. + /// The name of the View to navigate back to. /// A task representing the asynchronous operation. - Task GoBackAsync(); + Task GoBackToAsync(string name); /// /// Navigates to the specified view model asynchronously. diff --git a/src/Maui/Prism.Maui/Navigation/Builder/NavigationBuilder.cs b/src/Maui/Prism.Maui/Navigation/Builder/NavigationBuilder.cs index 5f90e14a1..6dac61f78 100644 --- a/src/Maui/Prism.Maui/Navigation/Builder/NavigationBuilder.cs +++ b/src/Maui/Prism.Maui/Navigation/Builder/NavigationBuilder.cs @@ -53,10 +53,10 @@ public INavigationBuilder AddParameter(string key, object value) return this; } - public async Task GoBackAsync() + public async Task GoBackToAsync() { var name = NavigationBuilderExtensions.GetNavigationKey(this); - return await _navigationService.GoBackAsync(name, _navigationParameters); + return await _navigationService.GoBackToAsync(name, _navigationParameters); } public Task NavigateAsync() diff --git a/src/Maui/Prism.Maui/Navigation/Builder/NavigationBuilderExtensions.cs b/src/Maui/Prism.Maui/Navigation/Builder/NavigationBuilderExtensions.cs index 355416bf9..247ed9faf 100644 --- a/src/Maui/Prism.Maui/Navigation/Builder/NavigationBuilderExtensions.cs +++ b/src/Maui/Prism.Maui/Navigation/Builder/NavigationBuilderExtensions.cs @@ -114,6 +114,15 @@ public static ICreateTabBuilder AddNavigationPage(this ICreateTabBuilder builder public static INavigationBuilder AddNavigationPage(this INavigationBuilder builder, bool useModalNavigation) => builder.AddNavigationPage(o => o.UseModalNavigation(useModalNavigation)); + /// + /// Navigates back to the specified view model asynchronously. + /// + /// The ViewModel to navigate to. + /// The . + /// A task representing the asynchronous operation. + public static Task GoBackToAsync(this INavigationBuilder builder) => + builder.GoBackToAsync(GetNavigationKey(builder)); + //public static INavigationBuilder AddSegment(this INavigationBuilder builder, string segmentName, params string[] createTabs) //{ // return builder; diff --git a/src/Maui/Prism.Maui/Navigation/INavigationService.cs b/src/Maui/Prism.Maui/Navigation/INavigationService.cs index f973fad6b..00687a7ac 100644 --- a/src/Maui/Prism.Maui/Navigation/INavigationService.cs +++ b/src/Maui/Prism.Maui/Navigation/INavigationService.cs @@ -18,7 +18,7 @@ public interface INavigationService /// The name of the View to navigate back to /// The navigation parameters /// If true a go back operation was successful. If false the go back operation failed. - Task GoBackAsync(string viewName, INavigationParameters parameters); + Task GoBackToAsync(string viewName, INavigationParameters parameters); /// /// When navigating inside a NavigationPage: Pops all but the root Page off the navigation stack diff --git a/src/Maui/Prism.Maui/Navigation/INavigationServiceExtensions.cs b/src/Maui/Prism.Maui/Navigation/INavigationServiceExtensions.cs index ebf1a5c6d..ff92aafdd 100644 --- a/src/Maui/Prism.Maui/Navigation/INavigationServiceExtensions.cs +++ b/src/Maui/Prism.Maui/Navigation/INavigationServiceExtensions.cs @@ -31,7 +31,7 @@ public static Task GoBackAsync(this INavigationService naviga /// Service for handling navigation between views /// The name of the View to navigate back to /// If true a go back operation was successful. If false the go back operation failed. - public static Task GoBackAsync(this INavigationService navigationService, string viewName) => navigationService.GoBackAsync(viewName, new NavigationParameters()); + public static Task GoBackToAsync(this INavigationService navigationService, string viewName) => navigationService.GoBackToAsync(viewName, new NavigationParameters()); /// /// When navigating inside a NavigationPage: Pops all but the root Page off the navigation stack diff --git a/src/Maui/Prism.Maui/Navigation/PageNavigationService.cs b/src/Maui/Prism.Maui/Navigation/PageNavigationService.cs index 4ef443486..c94930703 100644 --- a/src/Maui/Prism.Maui/Navigation/PageNavigationService.cs +++ b/src/Maui/Prism.Maui/Navigation/PageNavigationService.cs @@ -130,7 +130,7 @@ private async Task GoBackInternalAsync(INavigationParameters } /// - public virtual async Task GoBackAsync(string viewName, INavigationParameters parameters) + public virtual async Task GoBackToAsync(string viewName, INavigationParameters parameters) { await _semaphore.WaitAsync(); try diff --git a/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs b/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs index 7b9526584..f6a9b31fc 100644 --- a/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs +++ b/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs @@ -241,7 +241,7 @@ public async Task GoBack_Name_PopsToSpecifiedView() var result = await navigationPage.CurrentPage.GetContainerProvider() .Resolve() - .GoBackAsync("MockViewC"); + .GoBackToAsync("MockViewC"); Assert.True(result.Success); @@ -264,7 +264,7 @@ public async Task GoBack_ViewModel_PopsToSpecifiedView() var result = await navigationPage.CurrentPage.GetContainerProvider() .Resolve() .CreateBuilder() - .GoBackAsync(); + .GoBackToAsync(); Assert.True(result.Success); @@ -315,7 +315,7 @@ public async Task GoBack_Name_PopsToSpecifiedViewWithoutPoppingEachPage() var result = await navigationPage.CurrentPage.GetContainerProvider() .Resolve() - .GoBackAsync("MockViewC"); + .GoBackToAsync("MockViewC"); Assert.True(result.Success); @@ -340,7 +340,7 @@ public async Task GoBack_Name_PopsToSpecifiedViewWithoutPoppingEachPageOfLimitat var result = await navigationPage.CurrentPage.GetContainerProvider() .Resolve() - .GoBackAsync("MockViewA"); + .GoBackToAsync("MockViewA"); Assert.True(result.Success); From 22ad326488ee933c7779bc4b8288f1d66e678e62 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sun, 7 Apr 2024 10:54:57 -0600 Subject: [PATCH 3/7] feat: add ability to select tab and navigate --- src/Maui/Prism.Maui/Mvvm/ViewModelLocator.cs | 4 +- .../Navigation/INavigationService.cs | 5 +- .../INavigationServiceExtensions.cs | 43 +++++- .../Navigation/PageNavigationService.cs | 102 ++++++++++----- .../Navigation/NavigationSelectTabTests.cs | 122 ++++++++++++++++++ .../Fixtures/Navigation/NavigationTests.cs | 84 ------------ 6 files changed, 239 insertions(+), 121 deletions(-) create mode 100644 tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationSelectTabTests.cs diff --git a/src/Maui/Prism.Maui/Mvvm/ViewModelLocator.cs b/src/Maui/Prism.Maui/Mvvm/ViewModelLocator.cs index b30a94ff3..fccd23901 100644 --- a/src/Maui/Prism.Maui/Mvvm/ViewModelLocator.cs +++ b/src/Maui/Prism.Maui/Mvvm/ViewModelLocator.cs @@ -27,7 +27,9 @@ private static void OnViewModelLocatorBehaviorChanged(BindableObject bindable, o propertyChanged: OnViewModelPropertyChanged); public static readonly BindableProperty NavigationNameProperty = - BindableProperty.CreateAttached("NavigationName", typeof(string), typeof(ViewModelLocator), null); + BindableProperty.CreateAttached("NavigationName", typeof(string), typeof(ViewModelLocator), null, defaultValueCreator: CreateDefaultNavigationName); + + private static object CreateDefaultNavigationName(BindableObject bindable) => bindable.GetType().Name; public static string GetNavigationName(BindableObject bindable) => (string)bindable.GetValue(NavigationNameProperty); diff --git a/src/Maui/Prism.Maui/Navigation/INavigationService.cs b/src/Maui/Prism.Maui/Navigation/INavigationService.cs index 00687a7ac..25e1e12f0 100644 --- a/src/Maui/Prism.Maui/Navigation/INavigationService.cs +++ b/src/Maui/Prism.Maui/Navigation/INavigationService.cs @@ -41,10 +41,11 @@ public interface INavigationService Task NavigateAsync(Uri uri, INavigationParameters parameters); /// - /// Selects a Tab of the TabbedPage parent. + /// Selects a Tab of the TabbedPage parent and Navigates to a specified Uri /// /// The name of the tab to select + /// The Uri to navigate to /// The navigation parameters /// indicating whether the request was successful or if there was an encountered . - Task SelectTabAsync(string name, INavigationParameters parameters); + Task SelectTabAsync(string name, Uri uri, INavigationParameters parameters); } diff --git a/src/Maui/Prism.Maui/Navigation/INavigationServiceExtensions.cs b/src/Maui/Prism.Maui/Navigation/INavigationServiceExtensions.cs index ff92aafdd..7e1097aef 100644 --- a/src/Maui/Prism.Maui/Navigation/INavigationServiceExtensions.cs +++ b/src/Maui/Prism.Maui/Navigation/INavigationServiceExtensions.cs @@ -120,10 +120,51 @@ public static void OnNavigationError(this Task navigationTask }); } + /// + /// Selects a Tab of the TabbedPage parent. + /// + /// Service for handling navigation between views + /// The name of the tab to select + /// The navigation parameters + /// indicating whether the request was successful or if there was an encountered . + public static Task SelectTabAsync(this INavigationService navigationService, string name, INavigationParameters parameters) => + navigationService.SelectTabAsync(name, null, parameters); + + /// + /// Selects a Tab of the TabbedPage parent. + /// + /// Service for handling navigation between views + /// The name of the tab to select + /// The Uri to navigate to + /// indicating whether the request was successful or if there was an encountered . + public static Task SelectTabAsync(this INavigationService navigationService, string name, Uri uri) => + navigationService.SelectTabAsync(name, uri, new NavigationParameters()); + + /// + /// Selects a Tab of the TabbedPage parent. + /// + /// Service for handling navigation between views + /// The name of the tab to select + /// The Uri to navigate to + /// indicating whether the request was successful or if there was an encountered . + public static Task SelectTabAsync(this INavigationService navigationService, string name, string uri) => + navigationService.SelectTabAsync(name, UriParsingHelper.Parse(uri), new NavigationParameters()); + + /// + /// Selects a Tab of the TabbedPage parent. + /// + /// Service for handling navigation between views + /// The name of the tab to select + /// The Uri to navigate to + /// The navigation parameters + /// indicating whether the request was successful or if there was an encountered . + public static Task SelectTabAsync(this INavigationService navigationService, string name, string uri, INavigationParameters parameters) => + navigationService.SelectTabAsync(name, UriParsingHelper.Parse(uri), parameters); + /// /// Selects a tab programatically /// - /// + /// Service for handling navigation between views /// The name of the tab to select /// The . public static Task SelectTabAsync(this INavigationService navigationService, string tabName) => diff --git a/src/Maui/Prism.Maui/Navigation/PageNavigationService.cs b/src/Maui/Prism.Maui/Navigation/PageNavigationService.cs index c94930703..8c0c35378 100644 --- a/src/Maui/Prism.Maui/Navigation/PageNavigationService.cs +++ b/src/Maui/Prism.Maui/Navigation/PageNavigationService.cs @@ -46,6 +46,9 @@ protected Window Window } // This should be resolved by the container when accessed as a Module could register views after the NavigationService was resolved + /// + /// Gets the + /// public IViewRegistry Registry => _container.Resolve(); /// @@ -73,13 +76,17 @@ public PageNavigationService(IContainerProvider container, /// If true a go back operation was successful. If false the go back operation failed. public virtual async Task GoBackAsync(INavigationParameters parameters) { - await _semaphore.WaitAsync(); - - INavigationResult result = await GoBackInternalAsync(parameters); - - _semaphore.Release(); - - return result; + try + { + await WaitForPendingNavigationRequests(); + return await GoBackInternalAsync(parameters); + } + finally + { + _lastNavigate = DateTime.Now; + NavigationSource = PageNavigationSource.Device; + _semaphore.Release(); + } } private async Task GoBackInternalAsync(INavigationParameters parameters) @@ -132,11 +139,10 @@ private async Task GoBackInternalAsync(INavigationParameters /// public virtual async Task GoBackToAsync(string viewName, INavigationParameters parameters) { - await _semaphore.WaitAsync(); + await WaitForPendingNavigationRequests(); try { - if (parameters is null) - parameters = new NavigationParameters(); + parameters ??= new NavigationParameters(); parameters.GetNavigationParametersInternal().Add(KnownInternalParameters.NavigationMode, NavigationMode.Back); @@ -149,16 +155,13 @@ public virtual async Task GoBackToAsync(string viewName, INav var pagesToDestroy = page.Navigation.NavigationStack.ToList(); // get all pages to destroy pagesToDestroy.Reverse(); // destroy them in reverse order - var goBackPage = pagesToDestroy.FirstOrDefault(p => ViewModelLocator.GetNavigationName(p) == viewName); // find the go back page - if (goBackPage is null) - { - throw new NavigationException(NavigationException.GoBackRequiresNavigationPage); - } + var goBackPage = pagesToDestroy.FirstOrDefault(p => ViewModelLocator.GetNavigationName(p) == viewName) + ?? throw new NavigationException(NavigationException.GoBackRequiresNavigationPage); // find the go back page var index = pagesToDestroy.IndexOf(goBackPage); pagesToDestroy.RemoveRange(index, pagesToDestroy.Count - index); // don't destroy pages from the go back page to the root page var pagesToRemove = pagesToDestroy.Skip(1).ToList(); // exclude the current page from the destroy pages - bool animated = parameters.ContainsKey(KnownNavigationParameters.Animated) ? parameters.GetValue(KnownNavigationParameters.Animated) : true; + bool animated = !parameters.ContainsKey(KnownNavigationParameters.Animated) || parameters.GetValue(KnownNavigationParameters.Animated); NavigationSource = PageNavigationSource.NavigationService; foreach(var removePage in pagesToRemove) { @@ -183,13 +186,13 @@ public virtual async Task GoBackToAsync(string viewName, INav } finally { + _lastNavigate = DateTime.Now; NavigationSource = PageNavigationSource.Device; _semaphore.Release(); } } - - private static Exception GetGoBackException(Page currentPage, IView mainPage) + private static NavigationException GetGoBackException(Page currentPage, IView mainPage) { if (IsMainPage(currentPage, mainPage)) { @@ -237,11 +240,10 @@ private static bool IsMainPage(IView currentPage, IView mainPage) /// Only works when called from a View within a NavigationPage public virtual async Task GoBackToRootAsync(INavigationParameters parameters) { - await _semaphore.WaitAsync(); try { - if (parameters is null) - parameters = new NavigationParameters(); + await WaitForPendingNavigationRequests(); + parameters ??= new NavigationParameters(); parameters.GetNavigationParametersInternal().Add(KnownInternalParameters.NavigationMode, NavigationMode.Back); @@ -257,7 +259,7 @@ public virtual async Task GoBackToRootAsync(INavigationParame var root = pagesToDestroy.Last(); pagesToDestroy.Remove(root); //don't destroy the root page - bool animated = parameters.ContainsKey(KnownNavigationParameters.Animated) ? parameters.GetValue(KnownNavigationParameters.Animated) : true; + bool animated = !parameters.ContainsKey(KnownNavigationParameters.Animated) || parameters.GetValue(KnownNavigationParameters.Animated); NavigationSource = PageNavigationSource.NavigationService; await page.Navigation.PopToRootAsync(animated); NavigationSource = PageNavigationSource.Device; @@ -278,6 +280,7 @@ public virtual async Task GoBackToRootAsync(INavigationParame } finally { + _lastNavigate = DateTime.Now; NavigationSource = PageNavigationSource.Device; _semaphore.Release(); } @@ -294,12 +297,7 @@ public virtual async Task GoBackToRootAsync(INavigationParame /// public virtual async Task NavigateAsync(Uri uri, INavigationParameters parameters) { - await _semaphore.WaitAsync(); - // Ensure adequate time has passed since last navigation so that UI Refresh can Occur - if (DateTime.Now - _lastNavigate < TimeSpan.FromMilliseconds(150)) - { - await Task.Delay(150); - } + await WaitForPendingNavigationRequests(); try { @@ -336,14 +334,21 @@ public virtual async Task NavigateAsync(Uri uri, INavigationP /// Selects a Tab of the TabbedPage parent. /// /// The name of the tab to select + /// The Uri to navigate to /// The navigation parameters /// indicating whether the request was successful or if there was an encountered . - public virtual async Task SelectTabAsync(string tabName, INavigationParameters parameters) + public virtual async Task SelectTabAsync(string tabName, Uri uri, INavigationParameters parameters) { try { + ArgumentException.ThrowIfNullOrWhiteSpace(tabName); + + await PageNavigationService.WaitForPendingNavigationRequests(); + NavigationSource = PageNavigationSource.NavigationService; + var tabbedPage = GetTabbedPage(_pageAccessor.Page); - TabbedPage GetTabbedPage(Element page) => + + static TabbedPage GetTabbedPage(Element page) => page switch { TabbedPage tabbedPage => tabbedPage, @@ -370,9 +375,24 @@ TabbedPage GetTabbedPage(Element page) => if (!await MvvmHelpers.CanNavigateAsync(navigatedFromPage, parameters)) throw new NavigationException(NavigationException.IConfirmNavigationReturnedFalse, navigatedFromPage); - tabbedPage.CurrentPage = selectedChild; - MvvmHelpers.OnNavigatedFrom(navigatedFromPage, parameters); - MvvmHelpers.OnNavigatedTo(selectedChild, parameters); + var navigatedToTarget = selectedChild is NavigationPage navPage ? navPage.CurrentPage : selectedChild; + if (uri is not null) + { + if (uri.IsAbsoluteUri) + { + throw new NavigationException("Cannot process an absolute Navigation Uri when navigating within a specified Tab"); + } + + var navigationSegments = UriParsingHelper.GetUriSegments(uri); + await ProcessNavigation(navigatedToTarget, navigationSegments, parameters, null, null); + tabbedPage.CurrentPage = selectedChild; + } + else + { + tabbedPage.CurrentPage = selectedChild; + MvvmHelpers.OnNavigatedFrom(navigatedFromPage, parameters); + MvvmHelpers.OnNavigatedTo(navigatedToTarget, parameters); + } return new NavigationResult(); } @@ -380,6 +400,22 @@ TabbedPage GetTabbedPage(Element page) => { return new NavigationResult(ex); } + finally + { + _lastNavigate = DateTime.Now; + NavigationSource = PageNavigationSource.Device; + _semaphore.Release(); + } + } + + private static async Task WaitForPendingNavigationRequests() + { + await _semaphore.WaitAsync(); + // Ensure adequate time has passed since last navigation so that UI Refresh can Occur + if (DateTime.Now - _lastNavigate < TimeSpan.FromMilliseconds(150)) + { + await Task.Delay(150); + } } /// diff --git a/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationSelectTabTests.cs b/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationSelectTabTests.cs new file mode 100644 index 000000000..89f5368fe --- /dev/null +++ b/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationSelectTabTests.cs @@ -0,0 +1,122 @@ +using Prism.Controls; +using Prism.DryIoc.Maui.Tests.Mocks.ViewModels; +using Prism.DryIoc.Maui.Tests.Mocks.Views; + +namespace Prism.DryIoc.Maui.Tests.Fixtures.Navigation; + +public class NavigationSelectTabTests : TestBase +{ + public NavigationSelectTabTests(ITestOutputHelper testOutputHelper) + : base(testOutputHelper) + { + } + + [Fact] + public async Task SelectsTab_NavigatesWithinTab_NavigationPage() + { + var mauiApp = CreateBuilder(prism => prism + .RegisterTypes(c => c.RegisterForNavigation()) + .CreateWindow("TabbedPage?createTab=MockViewA&createTab=NavigationPage%2FMockViewB")) + .Build(); + var window = GetWindow(mauiApp); + + Assert.IsType(window.CurrentPage); + var navigationService = Prism.Navigation.Xaml.Navigation.GetNavigationService(window.CurrentPage); + var result = await navigationService.SelectTabAsync("NavigationPage|MockViewB", "MockViewC/MockViewD"); + + Assert.True(result.Success); + Assert.Null(result.Exception); + + Assert.IsType(window.Page); + var tabbedPage = window.Page as TabbedPage; + Assert.IsType(tabbedPage.CurrentPage); + var navPage = tabbedPage.CurrentPage as PrismNavigationPage; + Assert.IsType(navPage.RootPage); + Assert.IsType(navPage.CurrentPage); + Assert.Equal(3, navPage.Navigation.NavigationStack.Count); + } + + [Fact] + public async Task TabbedPage_SelectTabSets_CurrentTab() + { + var mauiApp = CreateBuilder(prism => prism.CreateWindow("TabbedPage?createTab=MockViewA&createTab=MockViewB&selectedTab=MockViewB")) + .Build(); + var window = GetWindow(mauiApp); + + Assert.IsAssignableFrom(window.Page); + var tabbedPage = (TabbedPage)window.Page; + Assert.NotNull(tabbedPage); + Assert.IsType(tabbedPage.CurrentPage); + } + + [Fact] + public async Task TabbedPage_SelectTab_SetsCurrentTab_WithNavigationPageTab() + { + var mauiApp = CreateBuilder(prism => prism.CreateWindow("TabbedPage?createTab=NavigationPage%2FMockViewA&createTab=NavigationPage%2FMockViewB&selectedTab=NavigationPage|MockViewB")) + .Build(); + var window = GetWindow(mauiApp); + + Assert.IsAssignableFrom(window.Page); + var tabbedPage = (TabbedPage)window.Page; + Assert.NotNull(tabbedPage); + var navPage = tabbedPage.CurrentPage as NavigationPage; + Assert.NotNull(navPage); + Assert.IsType(navPage.CurrentPage); + } + + [Fact] + public async Task TabbedPage_SelectsNewTab() + { + var mauiApp = CreateBuilder(prism => prism + .CreateWindow(nav => nav.CreateBuilder() + .AddTabbedSegment(s => s.CreateTab("MockViewA") + .CreateTab("MockViewB") + .CreateTab("MockViewC")) + .NavigateAsync())) + .Build(); + var window = GetWindow(mauiApp); + Assert.IsAssignableFrom(window.Page); + var tabbed = window.Page as TabbedPage; + + Assert.NotNull(tabbed); + + Assert.IsType(tabbed.CurrentPage); + var mockViewA = tabbed.CurrentPage; + var mockViewANav = Prism.Navigation.Xaml.Navigation.GetNavigationService(mockViewA); + + await mockViewANav.SelectTabAsync("MockViewB"); + + Assert.IsNotType(tabbed.CurrentPage); + Assert.IsType(tabbed.CurrentPage); + } + + [Fact] + public async Task TabbedPage_SelectsNewTab_WithNavigationParameters() + { + var mauiApp = CreateBuilder(prism => prism + .CreateWindow(nav => nav.CreateBuilder() + .AddTabbedSegment(s => s.CreateTab("MockViewA") + .CreateTab("MockViewB") + .CreateTab("MockViewC")) + .NavigateAsync())) + .Build(); + var window = GetWindow(mauiApp); + Assert.IsAssignableFrom(window.Page); + var tabbed = window.Page as TabbedPage; + + Assert.NotNull(tabbed); + + Assert.IsType(tabbed.CurrentPage); + var mockViewA = tabbed.CurrentPage; + var mockViewANav = Prism.Navigation.Xaml.Navigation.GetNavigationService(mockViewA); + + var expectedMessage = nameof(TabbedPage_SelectsNewTab_WithNavigationParameters); + await mockViewANav.SelectTabAsync("MockViewB", new NavigationParameters { { "Message", expectedMessage } }); + + Assert.IsNotType(tabbed.CurrentPage); + Assert.IsType(tabbed.CurrentPage); + + var viewModel = tabbed.CurrentPage.BindingContext as MockViewBViewModel; + Assert.Equal(expectedMessage, viewModel?.Message); + } +} diff --git a/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs b/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs index f6a9b31fc..f2c1ca78e 100644 --- a/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs +++ b/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs @@ -351,90 +351,6 @@ public async Task GoBack_Name_PopsToSpecifiedViewWithoutPoppingEachPageOfLimitat Assert.Equal(2, navigationPage.Navigation.NavigationStack.Count); } - [Fact] - public async Task TabbedPage_SelectTabSets_CurrentTab() - { - var mauiApp = CreateBuilder(prism => prism.CreateWindow("TabbedPage?createTab=MockViewA&createTab=MockViewB&selectedTab=MockViewB")) - .Build(); - var window = GetWindow(mauiApp); - - Assert.IsAssignableFrom(window.Page); - var tabbedPage = (TabbedPage)window.Page; - Assert.NotNull(tabbedPage); - Assert.IsType(tabbedPage.CurrentPage); - } - - [Fact] - public async Task TabbedPage_SelectTab_SetsCurrentTab_WithNavigationPageTab() - { - var mauiApp = CreateBuilder(prism => prism.CreateWindow("TabbedPage?createTab=NavigationPage%2FMockViewA&createTab=NavigationPage%2FMockViewB&selectedTab=NavigationPage|MockViewB")) - .Build(); - var window = GetWindow(mauiApp); - - Assert.IsAssignableFrom(window.Page); - var tabbedPage = (TabbedPage)window.Page; - Assert.NotNull(tabbedPage); - var navPage = tabbedPage.CurrentPage as NavigationPage; - Assert.NotNull(navPage); - Assert.IsType(navPage.CurrentPage); - } - - [Fact] - public async Task TabbedPage_SelectsNewTab() - { - var mauiApp = CreateBuilder(prism => prism - .CreateWindow(nav => nav.CreateBuilder() - .AddTabbedSegment(s => s.CreateTab("MockViewA") - .CreateTab("MockViewB") - .CreateTab("MockViewC")) - .NavigateAsync())) - .Build(); - var window = GetWindow(mauiApp); - Assert.IsAssignableFrom(window.Page); - var tabbed = window.Page as TabbedPage; - - Assert.NotNull(tabbed); - - Assert.IsType(tabbed.CurrentPage); - var mockViewA = tabbed.CurrentPage; - var mockViewANav = Prism.Navigation.Xaml.Navigation.GetNavigationService(mockViewA); - - await mockViewANav.SelectTabAsync("MockViewB"); - - Assert.IsNotType(tabbed.CurrentPage); - Assert.IsType(tabbed.CurrentPage); - } - - [Fact] - public async Task TabbedPage_SelectsNewTab_WithNavigationParameters() - { - var mauiApp = CreateBuilder(prism => prism - .CreateWindow(nav => nav.CreateBuilder() - .AddTabbedSegment(s => s.CreateTab("MockViewA") - .CreateTab("MockViewB") - .CreateTab("MockViewC")) - .NavigateAsync())) - .Build(); - var window = GetWindow(mauiApp); - Assert.IsAssignableFrom(window.Page); - var tabbed = window.Page as TabbedPage; - - Assert.NotNull(tabbed); - - Assert.IsType(tabbed.CurrentPage); - var mockViewA = tabbed.CurrentPage; - var mockViewANav = Prism.Navigation.Xaml.Navigation.GetNavigationService(mockViewA); - - var expectedMessage = nameof(TabbedPage_SelectsNewTab_WithNavigationParameters); - await mockViewANav.SelectTabAsync("MockViewB", new NavigationParameters { { "Message", expectedMessage } }); - - Assert.IsNotType(tabbed.CurrentPage); - Assert.IsType(tabbed.CurrentPage); - - var viewModel = tabbed.CurrentPage.BindingContext as MockViewBViewModel; - Assert.Equal(expectedMessage, viewModel?.Message); - } - [Fact] public async Task NavigationPage_DoesNotHave_MauiPage_AsRootPage() { From 9aa90c5a9ad502d719cd791b2095177b661e869d Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sun, 7 Apr 2024 10:56:55 -0600 Subject: [PATCH 4/7] tests: adding test for MAUI 8157 --- .../Fixtures/Navigation/NavigationTests.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs b/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs index f2c1ca78e..ae06e5adf 100644 --- a/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs +++ b/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs @@ -199,7 +199,26 @@ public async Task FlyoutRelativeNavigation_RemovesPage_AndNavigatesNotModally() TestPage(currentPage); } - [Fact(Skip = "Blocked by dotnet/maui/issues/8157")] + [Fact] + public void MAUI_Issue_8157_InitialNavigation_PushesModals() + { + Exception startupEx = null; + var mauiApp = CreateBuilder(prism => prism.CreateWindow("MockViewA/MockViewB", ex => + { + startupEx = ex; + })) + .Build(); + Assert.Null(startupEx); + var window = GetWindow(mauiApp); + + Assert.IsType(window.Page); + TestPage(window.Page); + var currentPage = window.CurrentPage; + Assert.IsType(currentPage); + TestPage(currentPage); + } + + [Fact(Skip = "No longer blocked by dotnet/maui/issues/8157. Not yet implemented.")] public async Task RelativeNavigation_RemovesPage_AndNavigatesModally() { Exception startupEx = null; From f96c6416d5124f6c066b309d11ec80223f540125 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sun, 7 Apr 2024 11:01:01 -0600 Subject: [PATCH 5/7] chore: fixing GoBackTo name --- e2e/Maui/MauiModule/ViewModels/ViewModelBase.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/Maui/MauiModule/ViewModels/ViewModelBase.cs b/e2e/Maui/MauiModule/ViewModels/ViewModelBase.cs index a429feabc..bd7b03805 100644 --- a/e2e/Maui/MauiModule/ViewModels/ViewModelBase.cs +++ b/e2e/Maui/MauiModule/ViewModels/ViewModelBase.cs @@ -1,4 +1,4 @@ -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; using System.Text.RegularExpressions; namespace MauiModule.ViewModels; @@ -29,7 +29,7 @@ protected ViewModelBase(BaseServices baseServices) SelectedDialog = AvailableDialogs.FirstOrDefault(); ShowDialog = new DelegateCommand(OnShowDialogCommand, () => !string.IsNullOrEmpty(SelectedDialog)) .ObservesProperty(() => SelectedDialog); - GoBack = new DelegateCommand(OnGoBack); + GoBack = new DelegateCommand(OnGoToBack); } public IEnumerable AvailableDialogs { get; } @@ -77,7 +77,7 @@ private void OnShowDialogCommand() private void DialogCallback(IDialogResult result) => Messages.Add("Dialog Closed"); - private void OnGoBack(string viewName) + private void OnGoToBack(string viewName) { Messages.Add($"On Go Back {viewName}"); _navigationService.GoBackToAsync(viewName); From 9f603bddc68502ecfa2a25d24d365205aae8e204 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sun, 7 Apr 2024 11:02:28 -0600 Subject: [PATCH 6/7] chore: fixing missing GoBackTo API in NavigationBuilder --- .../Prism.Maui/Navigation/Builder/NavigationBuilder.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Maui/Prism.Maui/Navigation/Builder/NavigationBuilder.cs b/src/Maui/Prism.Maui/Navigation/Builder/NavigationBuilder.cs index 6dac61f78..f3f02fda8 100644 --- a/src/Maui/Prism.Maui/Navigation/Builder/NavigationBuilder.cs +++ b/src/Maui/Prism.Maui/Navigation/Builder/NavigationBuilder.cs @@ -53,11 +53,11 @@ public INavigationBuilder AddParameter(string key, object value) return this; } - public async Task GoBackToAsync() - { - var name = NavigationBuilderExtensions.GetNavigationKey(this); - return await _navigationService.GoBackToAsync(name, _navigationParameters); - } + public Task GoBackToAsync() => + GoBackToAsync(NavigationBuilderExtensions.GetNavigationKey(this)); + + public Task GoBackToAsync(string name) => + _navigationService.GoBackToAsync(name, _navigationParameters); public Task NavigateAsync() { From 64a41f63c941f91311ea2cf435659ace94e52fb5 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sun, 7 Apr 2024 11:04:13 -0600 Subject: [PATCH 7/7] chore: refactor test --- .../Fixtures/Navigation/NavigationTests.cs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs b/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs index ae06e5adf..904282884 100644 --- a/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs +++ b/tests/Maui/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs @@ -230,19 +230,18 @@ public async Task RelativeNavigation_RemovesPage_AndNavigatesModally() Assert.Null(startupEx); var window = GetWindow(mauiApp); - var rootPage = window.Page as MockViewA; - Assert.NotNull(rootPage); - TestPage(rootPage); - var currentPage = rootPage.Navigation.ModalStack.Last(); + Assert.IsType(window.Page); + TestPage(window.Page); + var currentPage = window.CurrentPage; Assert.IsType(currentPage); TestPage(currentPage); - var container = currentPage.GetContainerProvider(); - var navService = container.Resolve(); - Assert.Equal(2, rootPage.Navigation.ModalStack.Count); - await navService.NavigateAsync("../MockViewC"); - var viewC = window.Page.Navigation.ModalStack.Last(); - Assert.IsType(viewC); - Assert.Equal(2, rootPage.Navigation.ModalStack.Count); + var navService = Prism.Navigation.Xaml.Navigation.GetNavigationService(currentPage); + Assert.Single(window.Page.Navigation.ModalStack); + var result = await navService.NavigateAsync("../MockViewC"); + Assert.True(result.Success); + Assert.Null(result.Exception); + Assert.IsType(window.CurrentPage); + Assert.Single(window.Page.Navigation.ModalStack); } [Fact]