From 41c7df80e0d0c677e1387b8f8fda30169b357482 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Tue, 14 Jun 2022 21:10:49 -0700 Subject: [PATCH 01/12] update navigation builder api --- sample/PrismMauiDemo/MauiProgram.cs | 2 +- .../ViewModels/SplashPageViewModel.cs | 2 +- .../Navigation/Builder/CreateTabBuilder.cs | 2 +- .../Builder/IConfigurableSegmentName.cs | 3 + .../Navigation/Builder/ICreateTabBuilder.cs | 8 +- .../Navigation/Builder/INavigationBuilder.cs | 2 +- .../Navigation/Builder/ISegmentBuilder.cs | 17 ++- .../Navigation/Builder/NavigationBuilder.cs | 2 +- .../Builder/NavigationBuilderExtensions.cs | 137 +++++++++++++++--- .../Navigation/Builder/SegmentBuilder.cs | 4 +- .../Navigation/NavigationBuilderFixture.cs | 16 +- 11 files changed, 155 insertions(+), 40 deletions(-) diff --git a/sample/PrismMauiDemo/MauiProgram.cs b/sample/PrismMauiDemo/MauiProgram.cs index 08c6926..8d8c591 100644 --- a/sample/PrismMauiDemo/MauiProgram.cs +++ b/sample/PrismMauiDemo/MauiProgram.cs @@ -39,7 +39,7 @@ public static MauiApp CreateMauiApp() Console.Error.WriteLine(x.Result.Exception.Message); })) .OnAppStart(navigationService => navigationService.CreateBuilder() - .AddNavigationSegment() + .AddSegment() .Navigate(HandleNavigationError)) ) .ConfigureFonts(fonts => diff --git a/sample/PrismMauiDemo/ViewModels/SplashPageViewModel.cs b/sample/PrismMauiDemo/ViewModels/SplashPageViewModel.cs index ffbb5a3..8163696 100644 --- a/sample/PrismMauiDemo/ViewModels/SplashPageViewModel.cs +++ b/sample/PrismMauiDemo/ViewModels/SplashPageViewModel.cs @@ -12,7 +12,7 @@ public SplashPageViewModel(INavigationService navigationService) public void OnAppearing() { _navigationService.CreateBuilder() - .AddNavigationSegment() + .AddSegment() .Navigate(); } diff --git a/src/Prism.Maui/Navigation/Builder/CreateTabBuilder.cs b/src/Prism.Maui/Navigation/Builder/CreateTabBuilder.cs index 35d656a..52a8892 100644 --- a/src/Prism.Maui/Navigation/Builder/CreateTabBuilder.cs +++ b/src/Prism.Maui/Navigation/Builder/CreateTabBuilder.cs @@ -13,7 +13,7 @@ public CreateTabBuilder() public string Segment => BuildSegment(); - public ICreateTabBuilder AddNavigationSegment(string segmentName, Action configureSegment) + public ICreateTabBuilder AddSegment(string segmentName, Action configureSegment) { var builder = new SegmentBuilder(segmentName); configureSegment?.Invoke(builder); diff --git a/src/Prism.Maui/Navigation/Builder/IConfigurableSegmentName.cs b/src/Prism.Maui/Navigation/Builder/IConfigurableSegmentName.cs index 26d53c1..825c809 100644 --- a/src/Prism.Maui/Navigation/Builder/IConfigurableSegmentName.cs +++ b/src/Prism.Maui/Navigation/Builder/IConfigurableSegmentName.cs @@ -2,5 +2,8 @@ internal interface IConfigurableSegmentName { + /// + /// Gets the Segment Name + /// string SegmentName { get; } } diff --git a/src/Prism.Maui/Navigation/Builder/ICreateTabBuilder.cs b/src/Prism.Maui/Navigation/Builder/ICreateTabBuilder.cs index 90e79f8..080293c 100644 --- a/src/Prism.Maui/Navigation/Builder/ICreateTabBuilder.cs +++ b/src/Prism.Maui/Navigation/Builder/ICreateTabBuilder.cs @@ -2,5 +2,11 @@ public interface ICreateTabBuilder { - ICreateTabBuilder AddNavigationSegment(string segmentName, Action configureSegment); + /// + /// Adds a Segment for the + /// + /// + /// + /// + ICreateTabBuilder AddSegment(string segmentName, Action configureSegment); } diff --git a/src/Prism.Maui/Navigation/Builder/INavigationBuilder.cs b/src/Prism.Maui/Navigation/Builder/INavigationBuilder.cs index 688bc05..dd61150 100644 --- a/src/Prism.Maui/Navigation/Builder/INavigationBuilder.cs +++ b/src/Prism.Maui/Navigation/Builder/INavigationBuilder.cs @@ -3,7 +3,7 @@ public interface INavigationBuilder { Uri Uri { get; } - INavigationBuilder AddNavigationSegment(string segmentName, Action configureSegment); + INavigationBuilder AddSegment(string segmentName, Action configureSegment); INavigationBuilder AddTabbedSegment(Action configuration); INavigationBuilder WithParameters(INavigationParameters parameters); INavigationBuilder AddParameter(string key, object value); diff --git a/src/Prism.Maui/Navigation/Builder/ISegmentBuilder.cs b/src/Prism.Maui/Navigation/Builder/ISegmentBuilder.cs index 9fdf5d6..57b1f24 100644 --- a/src/Prism.Maui/Navigation/Builder/ISegmentBuilder.cs +++ b/src/Prism.Maui/Navigation/Builder/ISegmentBuilder.cs @@ -2,9 +2,24 @@ public interface ISegmentBuilder { + /// + /// Gets the Segment Name like `ViewA` + /// string SegmentName { get; } - ISegmentBuilder AddSegmentParameter(string key, object value); + /// + /// Adds a Segment Parameter. This will append the generated URI query parameters NOT the + /// that are passed to every page. + /// + /// The Query Parameter key. + /// The Query Parameter value. + /// The . + ISegmentBuilder AddParameter(string key, object value); + /// + /// Specifies whether to force Modal Navigation for the current Navigation Segment + /// + /// If the NavigationService will force Modal Navigation for the Navigation Segment. Modal Navigation may be the default following this segment if it is not a NavigationPage. + /// The . ISegmentBuilder UseModalNavigation(bool useModalNavigation); } diff --git a/src/Prism.Maui/Navigation/Builder/NavigationBuilder.cs b/src/Prism.Maui/Navigation/Builder/NavigationBuilder.cs index 34f689f..dd41acf 100644 --- a/src/Prism.Maui/Navigation/Builder/NavigationBuilder.cs +++ b/src/Prism.Maui/Navigation/Builder/NavigationBuilder.cs @@ -23,7 +23,7 @@ public NavigationBuilder(INavigationService navigationService) public Uri Uri => BuildUri(); - public INavigationBuilder AddNavigationSegment(string segmentName, Action configureSegment) + public INavigationBuilder AddSegment(string segmentName, Action configureSegment) { var builder = new SegmentBuilder(segmentName); configureSegment?.Invoke(builder); diff --git a/src/Prism.Maui/Navigation/Builder/NavigationBuilderExtensions.cs b/src/Prism.Maui/Navigation/Builder/NavigationBuilderExtensions.cs index e836728..03cea01 100644 --- a/src/Prism.Maui/Navigation/Builder/NavigationBuilderExtensions.cs +++ b/src/Prism.Maui/Navigation/Builder/NavigationBuilderExtensions.cs @@ -5,6 +5,11 @@ namespace Prism.Navigation; public static class NavigationBuilderExtensions { + /// + /// Creates a using the current instance of the . + /// + /// The . + /// public static INavigationBuilder CreateBuilder(this INavigationService navigationService) => new NavigationBuilder(navigationService); @@ -20,30 +25,48 @@ internal static string GetNavigationKey(object builder) return registryAware.Registry.GetViewModelNavigationKey(vmType); } + /// + /// This will force the generated Navigation URI to return an Absolute URI resetting the current 's property. + /// + /// The . + /// The . public static INavigationBuilder UseAbsoluteNavigation(this INavigationBuilder builder) => builder.UseAbsoluteNavigation(true); - public static INavigationBuilder AddNavigationSegment(this INavigationBuilder builder, string segmentName, bool? useModalNavigation = null) => - builder.AddNavigationSegment(segmentName, o => + /// + /// Adds the specified segment `ViewA` to the Navigation URI + /// + /// The . + /// The navigation segment name `ViewA`. + /// An optional parameter whether to force Modal Navigation or use the default behavior based on the Navigation Stack. + /// The . + public static INavigationBuilder AddSegment(this INavigationBuilder builder, string segmentName, bool? useModalNavigation = null) => + builder.AddSegment(segmentName, o => { if (useModalNavigation.HasValue) o.UseModalNavigation(useModalNavigation.Value); }); - public static ICreateTabBuilder AddNavigationSegment(this ICreateTabBuilder builder) => - builder.AddNavigationSegment(b => { }); + /// + /// Adds the registered Navigation Segment name for the specified ViewModel. + /// + /// The ViewModel to navigate to. + /// The . + /// The . + public static ICreateTabBuilder AddSegment(this ICreateTabBuilder builder) => + builder.AddSegment(b => { }); - public static ICreateTabBuilder AddNavigationSegment(this ICreateTabBuilder builder, Action configureSegment) => - builder.AddNavigationSegment(GetNavigationKey(builder), configureSegment); + public static ICreateTabBuilder AddSegment(this ICreateTabBuilder builder, Action configureSegment) => + builder.AddSegment(GetNavigationKey(builder), configureSegment); - public static INavigationBuilder AddNavigationSegment(this INavigationBuilder builder) => - builder.AddNavigationSegment(b => { }); + public static INavigationBuilder AddSegment(this INavigationBuilder builder) => + builder.AddSegment(b => { }); - public static INavigationBuilder AddNavigationSegment(this INavigationBuilder builder, Action configureSegment) => - builder.AddNavigationSegment(GetNavigationKey(builder), configureSegment); + public static INavigationBuilder AddSegment(this INavigationBuilder builder, Action configureSegment) => + builder.AddSegment(GetNavigationKey(builder), configureSegment); - public static INavigationBuilder AddNavigationSegment(this INavigationBuilder builder, bool useModalNavigation) => - builder.AddNavigationSegment(b => b.UseModalNavigation(useModalNavigation)); + public static INavigationBuilder AddSegment(this INavigationBuilder builder, bool useModalNavigation) => + builder.AddSegment(b => b.UseModalNavigation(useModalNavigation)); // Will check for the Navigation key of a registered NavigationPage public static INavigationBuilder AddNavigationPage(this INavigationBuilder builder) => @@ -59,7 +82,7 @@ public static INavigationBuilder AddNavigationPage(this INavigationBuilder build throw new NavigationException(NavigationException.NoPageIsRegistered, nameof(NavigationPage)); var registration = registrations.Last(); - return builder.AddNavigationSegment(registration.Name, configureSegment); + return builder.AddSegment(registration.Name, configureSegment); } public static ICreateTabBuilder AddNavigationPage(this ICreateTabBuilder builder) => @@ -75,58 +98,110 @@ public static ICreateTabBuilder AddNavigationPage(this ICreateTabBuilder builder throw new NavigationException(NavigationException.NoPageIsRegistered, nameof(NavigationPage)); var registration = registrations.Last(); - return builder.AddNavigationSegment(registration.Name, configureSegment); + return builder.AddSegment(registration.Name, configureSegment); } + /// + /// Adds a NavigationPage to the Navigation Uri + /// + /// The . + /// When this will force Modal Navigation. + /// The . + /// This should only be used when you have a single registered for Navigation. Typically this is automatically registered for you by Prism. public static INavigationBuilder AddNavigationPage(this INavigationBuilder builder, bool useModalNavigation) => builder.AddNavigationPage(o => o.UseModalNavigation(useModalNavigation)); - //public static INavigationBuilder AddNavigationSegment(this INavigationBuilder builder, string segmentName, params string[] createTabs) + //public static INavigationBuilder AddSegment(this INavigationBuilder builder, string segmentName, params string[] createTabs) //{ // return builder; //} - //public static INavigationBuilder AddNavigationSegment(this INavigationBuilder builder, string segmentName, bool useModalNavigation, params string[] createTabs) + //public static INavigationBuilder AddSegment(this INavigationBuilder builder, string segmentName, bool useModalNavigation, params string[] createTabs) //{ // return builder; //} - //public static INavigationBuilder AddNavigationSegment(this INavigationBuilder builder, string segmentName, string selectTab, bool? useModalNavigation, params string[] createTabs) + //public static INavigationBuilder AddSegment(this INavigationBuilder builder, string segmentName, string selectTab, bool? useModalNavigation, params string[] createTabs) //{ // return builder; //} + /// + /// Navigates to the URI generated by the . + /// + /// The . public static async void Navigate(this INavigationBuilder builder) { await builder.NavigateAsync(); } + /// + /// Navigates to the URI generated by the with a specified callback + /// when the Navigation is unsuccessful with an Exception encountered while navigating. This is typically a . You should check that the navigation was not cancelled. + /// + /// The . + /// Delegate to handle when we encounter a Navigation Exception public static async void Navigate(this INavigationBuilder builder, Action onError) { await builder.NavigateAsync(onError); } + /// + /// Navigates to the URI generated by the with a specified callback when the Navigation is successful. + /// + /// The . + /// Delegate to handle when the navigation is successful public static async void Navigate(this INavigationBuilder builder, Action onSuccess) { await builder.NavigateAsync(onSuccess, _ => { }); } + /// + /// Navigates to the URI generated by the with callbacks for navigation success and errors. + /// + /// The . + /// Delegate to handle when the navigation is successful + /// Delegate to handle when we encounter a Navigation Exception public static async void Navigate(this INavigationBuilder builder, Action onSuccess, Action onError) { await builder.NavigateAsync(onSuccess, onError); } + /// + /// Forces Modal Navigation + /// + /// The . + /// The . public static ISegmentBuilder UseModalNavigation(this ISegmentBuilder builder) => builder.UseModalNavigation(true); - public static ICreateTabBuilder AddNavigationSegment(this ICreateTabBuilder builder, string segmentNameOrUri) => - builder.AddNavigationSegment(segmentNameOrUri, null); - + /// + /// Adds a Segment to the for deep linking within a created tab + /// + /// The . + /// The Navigation Segment name `ViewA` or uri `ViewA?id=5` + /// The . + public static ICreateTabBuilder AddSegment(this ICreateTabBuilder builder, string segmentNameOrUri) => + builder.AddSegment(segmentNameOrUri, null); + + /// + /// Will dynamically create a Tab within the . + /// + /// The . + /// The name of the . + /// Delegate to configure the generated . + /// The . public static ITabbedSegmentBuilder CreateTab(this ITabbedSegmentBuilder builder, string segmentName, Action configureSegment) => - builder.CreateTab(o => o.AddNavigationSegment(segmentName, configureSegment)); - + builder.CreateTab(o => o.AddSegment(segmentName, configureSegment)); + + /// + /// Dynamically creates a tab within a based on a Segment Name `ViewA` or Uri `ViewA?id=5`. + /// + /// The . + /// The View name or Uri + /// The . public static ITabbedSegmentBuilder CreateTab(this ITabbedSegmentBuilder builder, string segmentNameOrUri) => - builder.CreateTab(o => o.AddNavigationSegment(segmentNameOrUri)); + builder.CreateTab(o => o.AddSegment(segmentNameOrUri)); public static ITabbedSegmentBuilder CreateTab(this ITabbedSegmentBuilder builder) { @@ -134,12 +209,28 @@ public static ITabbedSegmentBuilder CreateTab(this ITabbedSegmentBui return builder.CreateTab(navigationKey); } + /// + /// Selects the active tab for the specified ViewModel + /// + /// The ViewModel + /// The . + /// The . public static ITabbedSegmentBuilder SelectTab(this ITabbedSegmentBuilder builder) { var navigationKey = GetNavigationKey(builder); return builder.SelectedTab(navigationKey); } + /// + /// Will Select a specific Tab within the . + /// + /// The . + /// The Navigation Segment Name or Names. + /// + /// Typically only a single Navigation Segment should be needed. In the event multiple tabs use a you should specify the name of the NavigationPage & the name of the Current or Top most Page within the tab you want to navigate to + /// + /// builder.SelectTab("NavigationPage", "ViewA"); + /// public static ITabbedNavigationBuilder SelectTab(this ITabbedNavigationBuilder builder, params string[] navigationSegments) => builder.SelectTab(string.Join("|", navigationSegments)); } diff --git a/src/Prism.Maui/Navigation/Builder/SegmentBuilder.cs b/src/Prism.Maui/Navigation/Builder/SegmentBuilder.cs index c6a799c..bdd673c 100644 --- a/src/Prism.Maui/Navigation/Builder/SegmentBuilder.cs +++ b/src/Prism.Maui/Navigation/Builder/SegmentBuilder.cs @@ -14,7 +14,7 @@ public SegmentBuilder(string segmentName) public string Segment => BuildSegment(); - public ISegmentBuilder AddSegmentParameter(string key, object value) + public ISegmentBuilder AddParameter(string key, object value) { _parameters.Add(key, value); return this; @@ -22,7 +22,7 @@ public ISegmentBuilder AddSegmentParameter(string key, object value) public ISegmentBuilder UseModalNavigation(bool useModalNavigation) { - return AddSegmentParameter(KnownNavigationParameters.UseModalNavigation, useModalNavigation); + return AddParameter(KnownNavigationParameters.UseModalNavigation, useModalNavigation); } private string BuildSegment() diff --git a/tests/Prism.Maui.Tests/Fixtures/Navigation/NavigationBuilderFixture.cs b/tests/Prism.Maui.Tests/Fixtures/Navigation/NavigationBuilderFixture.cs index 240ab55..d9f317e 100644 --- a/tests/Prism.Maui.Tests/Fixtures/Navigation/NavigationBuilderFixture.cs +++ b/tests/Prism.Maui.Tests/Fixtures/Navigation/NavigationBuilderFixture.cs @@ -9,7 +9,7 @@ public void GeneratesRelativeUriWithSingleSegment() { var uri = Mock.Of() .CreateBuilder() - .AddNavigationSegment("ViewA") + .AddSegment("ViewA") .Uri; Assert.Equal("ViewA", uri.ToString()); @@ -20,9 +20,9 @@ public void GeneratesRelativeUriWithMultipleSegments() { var uri = Mock.Of() .CreateBuilder() - .AddNavigationSegment("ViewA") - .AddNavigationSegment("ViewB") - .AddNavigationSegment("ViewC") + .AddSegment("ViewA") + .AddSegment("ViewB") + .AddSegment("ViewC") .Uri; Assert.Equal("ViewA/ViewB/ViewC", uri.ToString()); @@ -33,7 +33,7 @@ public void GeneratesAbsoluteUriWithSingleSegment() { var uri = Mock.Of() .CreateBuilder() - .AddNavigationSegment("ViewA") + .AddSegment("ViewA") .UseAbsoluteNavigation() .Uri; @@ -45,9 +45,9 @@ public void GeneratesAbsoluteUriWithMultipleSegments() { var uri = Mock.Of() .CreateBuilder() - .AddNavigationSegment("ViewA") - .AddNavigationSegment("ViewB") - .AddNavigationSegment("ViewC") + .AddSegment("ViewA") + .AddSegment("ViewB") + .AddSegment("ViewC") .UseAbsoluteNavigation() .Uri; From bf719e10a7638e8de74a9eb6a88379fd55d916b5 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Tue, 14 Jun 2022 21:11:02 -0700 Subject: [PATCH 02/12] add cancelled flag on INavigationResult --- src/Prism.Maui/Navigation/INavigationResult.cs | 6 +++++- src/Prism.Maui/Navigation/NavigationResult.cs | 2 -- src/Prism.Maui/Navigation/PageNavigationService.cs | 6 ++---- .../Fixtures/Navigation/Xaml/GoBackExtensionFixture.cs | 8 ++++---- .../Navigation/Xaml/NavigateToExtensionFixture.cs | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Prism.Maui/Navigation/INavigationResult.cs b/src/Prism.Maui/Navigation/INavigationResult.cs index 9110148..f40131b 100644 --- a/src/Prism.Maui/Navigation/INavigationResult.cs +++ b/src/Prism.Maui/Navigation/INavigationResult.cs @@ -2,7 +2,11 @@ public interface INavigationResult { - bool Success { get; } + bool Success => Exception is null; + + bool Cancelled => + Exception is NavigationException navigationException + && navigationException.Message == NavigationException.IConfirmNavigationReturnedFalse; Exception Exception { get; } } diff --git a/src/Prism.Maui/Navigation/NavigationResult.cs b/src/Prism.Maui/Navigation/NavigationResult.cs index c941954..3960a32 100644 --- a/src/Prism.Maui/Navigation/NavigationResult.cs +++ b/src/Prism.Maui/Navigation/NavigationResult.cs @@ -2,7 +2,5 @@ public record NavigationResult : INavigationResult { - public bool Success { get; init; } - public Exception Exception { get; init; } } diff --git a/src/Prism.Maui/Navigation/PageNavigationService.cs b/src/Prism.Maui/Navigation/PageNavigationService.cs index 45b9284..93bdb35 100644 --- a/src/Prism.Maui/Navigation/PageNavigationService.cs +++ b/src/Prism.Maui/Navigation/PageNavigationService.cs @@ -1013,8 +1013,7 @@ private INavigationResult Notify(NavigationRequestType type, INavigationParamete { var result = new NavigationResult { - Exception = exception, - Success = exception is null + Exception = exception }; _eventAggregator.GetEvent().Publish(new NavigationRequestContext { @@ -1030,8 +1029,7 @@ private INavigationResult Notify(Uri uri, INavigationParameters parameters, Exce { var result = new NavigationResult { - Exception = exception, - Success = exception is null, + Exception = exception }; _eventAggregator.GetEvent().Publish(new NavigationRequestContext diff --git a/tests/Prism.Maui.Tests/Fixtures/Navigation/Xaml/GoBackExtensionFixture.cs b/tests/Prism.Maui.Tests/Fixtures/Navigation/Xaml/GoBackExtensionFixture.cs index 66d472c..b775acf 100644 --- a/tests/Prism.Maui.Tests/Fixtures/Navigation/Xaml/GoBackExtensionFixture.cs +++ b/tests/Prism.Maui.Tests/Fixtures/Navigation/Xaml/GoBackExtensionFixture.cs @@ -54,7 +54,7 @@ public void Execute_GoBackTypeDefault_NavigationParameters_HasKnownNavigationPar { parameters = navParameters; }) - .ReturnsAsync(new NavigationResult { Success = false, Exception = null }); + .ReturnsAsync(new NavigationResult { Exception = new Exception() }); var registry = container.Resolve(); var page = registry.CreateView(container, "PageMock") as Page; @@ -109,7 +109,7 @@ public void Execute_GoBackTypeToRoot_NavigationParameters_DoNotHaveKnownNavigati { parameters = navParameters; }) - .ReturnsAsync(new NavigationResult { Success = false, Exception = null }); + .ReturnsAsync(new NavigationResult { Exception = new Exception() }); var registry = container.Resolve(); var page = registry.CreateView(container, "PageMock") as Page; @@ -139,7 +139,7 @@ public void Execute_GoBackTypeDefault_CommandParameter_IncludedInNavigationParam { parameters = navParameters; }) - .ReturnsAsync(new NavigationResult { Success = true, Exception = null }); + .ReturnsAsync(new NavigationResult { Exception = new Exception() }); var registry = container.Resolve(); var page = registry.CreateView(container, "PageMock") as Page; @@ -168,7 +168,7 @@ public void Execute_GoBackTypeToRoot_CommandParameter_IncludedInNavigationParame { parameters = navParameters; }) - .ReturnsAsync(new NavigationResult { Success = true, Exception = null }); + .ReturnsAsync(new NavigationResult { Exception = new Exception() }); var registry = container.Resolve(); var page = registry.CreateView(container, "PageMock") as Page; diff --git a/tests/Prism.Maui.Tests/Fixtures/Navigation/Xaml/NavigateToExtensionFixture.cs b/tests/Prism.Maui.Tests/Fixtures/Navigation/Xaml/NavigateToExtensionFixture.cs index f058409..270a89c 100644 --- a/tests/Prism.Maui.Tests/Fixtures/Navigation/Xaml/NavigateToExtensionFixture.cs +++ b/tests/Prism.Maui.Tests/Fixtures/Navigation/Xaml/NavigateToExtensionFixture.cs @@ -42,7 +42,7 @@ public void Execute_NameIsSet_NavigatesToPage() Mock.Get(mockNavigation) .Setup(x => x.NavigateAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new NavigationResult { Success = true, Exception = null }); + .ReturnsAsync(new NavigationResult()); var registry = container.Resolve(); var page = registry.CreateView(container, "PageMock") as Page; @@ -78,7 +78,7 @@ public void Execute_NavigationParameters_HasKnownNavigationParameters(bool anima { parameters = navParameters; }) - .ReturnsAsync(new NavigationResult { Success = false, Exception = null }); + .ReturnsAsync(new NavigationResult { Exception = new Exception() }); var registry = container.Resolve(); var page = registry.CreateView(container, "PageMock") as Page; @@ -113,7 +113,7 @@ public void Execute_CommandParameter_IncludedInNavigationParameters() { parameters = navParameters; }) - .ReturnsAsync(new NavigationResult { Success = true, Exception = null }); + .ReturnsAsync(new NavigationResult()); var registry = container.Resolve(); var page = registry.CreateView(container, "PageMock") as Page; From 75f1a7432c5b9eb0187f982674659d954978ff46 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Fri, 17 Jun 2022 20:59:18 -0700 Subject: [PATCH 03/12] resolve build warnings --- global.json | 2 +- src/Prism.Maui/Navigation/NavigationRequestContext.cs | 2 +- src/Prism.Maui/Prism.Maui.csproj | 1 + src/Prism.Maui/PrismAppBuilderExtensions.cs | 3 ++- src/Prism.Maui/Regions/Xaml/RegionManager.cs | 1 + .../Mocks/ViewModels/MockRegionViewAViewModel.cs | 6 +++--- .../Mocks/ViewModels/MockRegionViewBViewModel.cs | 2 +- .../Mocks/ViewModels/MockViewModelBase.cs | 2 +- .../Prism.DryIoc.Maui.Tests/Prism.DryIoc.Maui.Tests.csproj | 1 - 9 files changed, 11 insertions(+), 9 deletions(-) diff --git a/global.json b/global.json index 4269bcf..c2c0b89 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.300-preview", + "version": "6.0.300", "rollForward": "latestPatch", "allowPrerelease": true } diff --git a/src/Prism.Maui/Navigation/NavigationRequestContext.cs b/src/Prism.Maui/Navigation/NavigationRequestContext.cs index 7f6eb1c..d03bb96 100644 --- a/src/Prism.Maui/Navigation/NavigationRequestContext.cs +++ b/src/Prism.Maui/Navigation/NavigationRequestContext.cs @@ -4,7 +4,7 @@ public record NavigationRequestContext { public bool Cancelled => Result?.Exception is not null && Result.Exception is NavigationException ne && ne.Message == NavigationException.IConfirmNavigationReturnedFalse; public NavigationRequestType Type { get; init; } - public Uri? Uri { get; init; } + public Uri Uri { get; init; } public INavigationParameters Parameters { get; init; } public INavigationResult Result { get; init; } } \ No newline at end of file diff --git a/src/Prism.Maui/Prism.Maui.csproj b/src/Prism.Maui/Prism.Maui.csproj index 84dbe05..1bbefef 100644 --- a/src/Prism.Maui/Prism.Maui.csproj +++ b/src/Prism.Maui/Prism.Maui.csproj @@ -7,6 +7,7 @@ true true true + 21.0 diff --git a/src/Prism.Maui/PrismAppBuilderExtensions.cs b/src/Prism.Maui/PrismAppBuilderExtensions.cs index cc3ad0c..e66549e 100644 --- a/src/Prism.Maui/PrismAppBuilderExtensions.cs +++ b/src/Prism.Maui/PrismAppBuilderExtensions.cs @@ -25,7 +25,8 @@ public static PrismAppBuilder OnInitialized(this PrismAppBuilder builder, Action /// /// Configures the used by Prism. /// - /// The ModuleCatalog to configure + /// The . + /// Delegate to configure the . public static PrismAppBuilder ConfigureModuleCatalog(this PrismAppBuilder builder, Action configureCatalog) { if (!s_didRegisterModules) diff --git a/src/Prism.Maui/Regions/Xaml/RegionManager.cs b/src/Prism.Maui/Regions/Xaml/RegionManager.cs index d481dc1..7ef770e 100644 --- a/src/Prism.Maui/Regions/Xaml/RegionManager.cs +++ b/src/Prism.Maui/Regions/Xaml/RegionManager.cs @@ -6,6 +6,7 @@ using Prism.Extensions; using Prism.Ioc; using Prism.Properties; +using Prism.Regions.Adapters; using Prism.Regions.Behaviors; namespace Prism.Regions.Xaml; diff --git a/tests/Prism.DryIoc.Maui.Tests/Mocks/ViewModels/MockRegionViewAViewModel.cs b/tests/Prism.DryIoc.Maui.Tests/Mocks/ViewModels/MockRegionViewAViewModel.cs index 2f1bdab..740ac9b 100644 --- a/tests/Prism.DryIoc.Maui.Tests/Mocks/ViewModels/MockRegionViewAViewModel.cs +++ b/tests/Prism.DryIoc.Maui.Tests/Mocks/ViewModels/MockRegionViewAViewModel.cs @@ -13,10 +13,10 @@ public MockRegionViewAViewModel(IPageAccessor accessor) public bool Initialized { get; private set; } - public Page? Page => _accessor.Page; + public Page Page => _accessor.Page; - private string? _message; - public string? Message + private string _message; + public string Message { get => _message; set => SetProperty(ref _message, value); diff --git a/tests/Prism.DryIoc.Maui.Tests/Mocks/ViewModels/MockRegionViewBViewModel.cs b/tests/Prism.DryIoc.Maui.Tests/Mocks/ViewModels/MockRegionViewBViewModel.cs index 57903aa..e27ae6d 100644 --- a/tests/Prism.DryIoc.Maui.Tests/Mocks/ViewModels/MockRegionViewBViewModel.cs +++ b/tests/Prism.DryIoc.Maui.Tests/Mocks/ViewModels/MockRegionViewBViewModel.cs @@ -11,5 +11,5 @@ public MockRegionViewBViewModel(IPageAccessor accessor) _accessor = accessor; } - public Page? Page => _accessor.Page; + public Page Page => _accessor.Page; } diff --git a/tests/Prism.DryIoc.Maui.Tests/Mocks/ViewModels/MockViewModelBase.cs b/tests/Prism.DryIoc.Maui.Tests/Mocks/ViewModels/MockViewModelBase.cs index b165ce2..8332aac 100644 --- a/tests/Prism.DryIoc.Maui.Tests/Mocks/ViewModels/MockViewModelBase.cs +++ b/tests/Prism.DryIoc.Maui.Tests/Mocks/ViewModels/MockViewModelBase.cs @@ -14,7 +14,7 @@ protected MockViewModelBase(IPageAccessor pageAccessor, INavigationService navig public INavigationService NavigationService { get; } - public Page? Page => _pageAccessor.Page; + public Page Page => _pageAccessor.Page; public bool StopNavigation { get; set; } diff --git a/tests/Prism.DryIoc.Maui.Tests/Prism.DryIoc.Maui.Tests.csproj b/tests/Prism.DryIoc.Maui.Tests/Prism.DryIoc.Maui.Tests.csproj index f5692a0..73f74ab 100644 --- a/tests/Prism.DryIoc.Maui.Tests/Prism.DryIoc.Maui.Tests.csproj +++ b/tests/Prism.DryIoc.Maui.Tests/Prism.DryIoc.Maui.Tests.csproj @@ -3,7 +3,6 @@ net6.0 enable - enable false true From 42b6b3f1ad1f51a50d2905b4fae0b9318e79109c Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Fri, 17 Jun 2022 21:06:10 -0700 Subject: [PATCH 04/12] keep Page & Visual Element from showing up in XAML --- src/Prism.Maui/Xaml/TargetAwareExtensionBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Prism.Maui/Xaml/TargetAwareExtensionBase.cs b/src/Prism.Maui/Xaml/TargetAwareExtensionBase.cs index f109c6e..7ac2caa 100644 --- a/src/Prism.Maui/Xaml/TargetAwareExtensionBase.cs +++ b/src/Prism.Maui/Xaml/TargetAwareExtensionBase.cs @@ -9,7 +9,7 @@ namespace Prism.Xaml; public abstract class TargetAwareExtensionBase : BindableObject, IMarkupExtension { private Page _page; - public Page Page + protected internal Page Page { get => _page; set @@ -21,7 +21,7 @@ public Page Page } private VisualElement _targetElement; - public VisualElement TargetElement + protected internal VisualElement TargetElement { get => _targetElement; set From a6378e32f456767dc7be3fd06a3d86df2bd3deb0 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Fri, 17 Jun 2022 21:06:33 -0700 Subject: [PATCH 05/12] add TabbedPage & NavigationPage tests --- .../Navigation/ViewRegistryFixture.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/Prism.Maui.Tests/Fixtures/Navigation/ViewRegistryFixture.cs b/tests/Prism.Maui.Tests/Fixtures/Navigation/ViewRegistryFixture.cs index 4b2bd8b..fc3f394 100644 --- a/tests/Prism.Maui.Tests/Fixtures/Navigation/ViewRegistryFixture.cs +++ b/tests/Prism.Maui.Tests/Fixtures/Navigation/ViewRegistryFixture.cs @@ -3,6 +3,7 @@ using Microsoft.Maui.Controls; using Prism.Behaviors; using Prism.Common; +using Prism.Controls; using Prism.Maui.Tests.Mocks.Ioc; using Prism.Maui.Tests.Mocks.ViewModels; using Prism.Maui.Tests.Mocks.Views; @@ -122,4 +123,43 @@ public void CreateView_WithViewModelLocator_Disabled() Assert.NotNull(page); Assert.IsNotType(page.BindingContext); } + + [Fact] + public void TabbedPageRegistrationIsReturnedForViewOfType() + { + var container = new TestContainer(); + container.RegisterForNavigation(); + + var registry = container.Resolve(); + var registrations = registry.ViewsOfType(typeof(TabbedPage)); + + Assert.Single(registrations); + Assert.Equal(typeof(TabbedPage), registrations.First().View); + } + + [Fact] + public void NavigationPageRegistrationIsReturnedForViewOfType() + { + var container = new TestContainer(); + container.RegisterForNavigation(); + + var registry = container.Resolve(); + var registrations = registry.ViewsOfType(typeof(NavigationPage)); + + Assert.Single(registrations); + Assert.Equal(typeof(NavigationPage), registrations.First().View); + } + + [Fact] + public void PrismNavigationPageRegistrationIsReturnedForViewOfType() + { + var container = new TestContainer(); + container.RegisterForNavigation(); + + var registry = container.Resolve(); + var registrations = registry.ViewsOfType(typeof(NavigationPage)); + + Assert.Single(registrations); + Assert.Equal(typeof(PrismNavigationPage), registrations.First().View); + } } From 7d9d062d70d547d1c3e6847737f2d3e988d9e843 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Fri, 17 Jun 2022 21:08:16 -0700 Subject: [PATCH 06/12] add URI Parsing Tests --- src/Prism.Maui/Common/UriParsingHelper.cs | 9 +- .../Common/UriParsingHelperFixture.cs | 229 ++++++++++++++++++ 2 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 tests/Prism.Maui.Tests/Fixtures/Common/UriParsingHelperFixture.cs diff --git a/src/Prism.Maui/Common/UriParsingHelper.cs b/src/Prism.Maui/Common/UriParsingHelper.cs index 4dc6639..842f678 100644 --- a/src/Prism.Maui/Common/UriParsingHelper.cs +++ b/src/Prism.Maui/Common/UriParsingHelper.cs @@ -104,18 +104,19 @@ public static Uri EnsureAbsolute(Uri uri) if (!uri.OriginalString.StartsWith("/", StringComparison.Ordinal)) { - return new Uri("http://localhost/" + uri, UriKind.Absolute); + return new Uri("app://prismapp.maui/" + uri, UriKind.Absolute); } - return new Uri("http://localhost" + uri, UriKind.Absolute); + return new Uri("app://prismapp.maui" + uri, UriKind.Absolute); } public static Uri Parse(string uri) { - if (uri == null) throw new ArgumentNullException(nameof(uri)); + if (string.IsNullOrEmpty(uri)) + throw new ArgumentNullException(nameof(uri)); if (uri.StartsWith("/", StringComparison.Ordinal)) { - return new Uri("http://localhost" + uri, UriKind.Absolute); + return new Uri("app://prismapp.maui" + uri, UriKind.Absolute); } else { diff --git a/tests/Prism.Maui.Tests/Fixtures/Common/UriParsingHelperFixture.cs b/tests/Prism.Maui.Tests/Fixtures/Common/UriParsingHelperFixture.cs new file mode 100644 index 0000000..d6494c0 --- /dev/null +++ b/tests/Prism.Maui.Tests/Fixtures/Common/UriParsingHelperFixture.cs @@ -0,0 +1,229 @@ +using System; +using Prism.Common; + +namespace Prism.Maui.Tests.Fixtures.Common; + +public class UriParsingHelperFixture +{ + const string _relativeUri = "MainPage?id=3&name=dan"; + const string _absoluteUriWithOutProtocol = "/MainPage?id=3&name=dan"; + const string _absoluteUri = "htp://www.dansiegel.net/MainPage?id=3&name=dan"; + const string _deepLinkAbsoluteUri = "android-app://HellowWorld/MainPage?id=1/ViewA?id=2/ViewB?id=3/ViewC?id=4"; + const string _deepLinkRelativeUri = "MainPage?id=1/ViewA?id=2/ViewB?id=3/ViewC?id=4"; + + [Fact] + public void ParametersParsedFromNullSegment() + { + var parameters = UriParsingHelper.GetSegmentParameters(null); + Assert.NotNull(parameters); + } + + [Fact] + public void ParametersParsedFromEmptySegment() + { + var parameters = UriParsingHelper.GetSegmentParameters(string.Empty); + Assert.NotNull(parameters); + } + + [Fact] + public void ParametersParsedFromRelativeUri() + { + var parameters = UriParsingHelper.GetSegmentParameters(_relativeUri); + + Assert.NotEmpty(parameters); + + Assert.Contains("id", parameters.Keys); + Assert.Contains("name", parameters.Keys); + + Assert.Equal("3", parameters["id"]); + Assert.Equal("dan", parameters["name"]); + } + + [Fact] + public void ParametersParsedFromAbsoluteUri() + { + var parameters = UriParsingHelper.GetSegmentParameters(_absoluteUri); + + Assert.NotEmpty(parameters); + + Assert.Contains("id", parameters.Keys); + Assert.Contains("name", parameters.Keys); + + Assert.Equal("3", parameters["id"]); + Assert.Equal("dan", parameters["name"]); + } + + [Fact] + public void ParametersParsedFromNavigationParametersInRelativeUri() + { + var navParameters = new NavigationParameters + { + { "id", 3 }, + { "name", "dan" } + }; + + var parameters = UriParsingHelper.GetSegmentParameters("MainPage" + navParameters.ToString()); + + Assert.NotEmpty(parameters); + + Assert.Contains("id", parameters.Keys); + Assert.Contains("name", parameters.Keys); + + Assert.Equal("3", parameters["id"]); + Assert.Equal("dan", parameters["name"]); + } + + [Fact] + public void ParametersParsedFromNavigationParametersInAbsoluteUri() + { + var navParameters = new NavigationParameters + { + { "id", 3 }, + { "name", "dan" } + }; + + var parameters = UriParsingHelper.GetSegmentParameters("http://www.dansiegel.net/MainPage" + navParameters.ToString()); + + Assert.NotEmpty(parameters); + + Assert.Contains("id", parameters.Keys); + Assert.Contains("name", parameters.Keys); + + Assert.Equal("3", parameters["id"]); + Assert.Equal("dan", parameters["name"]); + } + + [Fact] + public void TargetNameParsedFromSingleSegment() + { + var target = UriParsingHelper.GetSegmentName(_relativeUri); + Assert.Equal("MainPage", target); + } + + [Fact] + public void SegmentsParsedFromDeepLinkUri() + { + var target = UriParsingHelper.GetUriSegments(new Uri(_deepLinkAbsoluteUri)); + Assert.Equal(4, target.Count); + } + + [Fact] + public void ParametersParsedFromDeepLinkAbsoluteUri() + { + var target = UriParsingHelper.GetUriSegments(new Uri(_deepLinkAbsoluteUri)); + Assert.Equal(4, target.Count); + + var p1 = UriParsingHelper.GetSegmentParameters(target.Dequeue()); + Assert.Equal("1", p1["id"]); + + var p2 = UriParsingHelper.GetSegmentParameters(target.Dequeue()); + Assert.Equal("2", p2["id"]); + + var p3 = UriParsingHelper.GetSegmentParameters(target.Dequeue()); + Assert.Equal("3", p3["id"]); + + var p4 = UriParsingHelper.GetSegmentParameters(target.Dequeue()); + Assert.Equal("4", p4["id"]); + } + + [Fact] + public void ParametersParsedFromDeepLinkRelativeUri() + { + var target = UriParsingHelper.GetUriSegments(new Uri(_deepLinkRelativeUri, UriKind.Relative)); + Assert.Equal(4, target.Count); + + var p1 = UriParsingHelper.GetSegmentParameters(target.Dequeue()); + Assert.Equal("1", p1["id"]); + + var p2 = UriParsingHelper.GetSegmentParameters(target.Dequeue()); + Assert.Equal("2", p2["id"]); + + var p3 = UriParsingHelper.GetSegmentParameters(target.Dequeue()); + Assert.Equal("3", p3["id"]); + + var p4 = UriParsingHelper.GetSegmentParameters(target.Dequeue()); + Assert.Equal("4", p4["id"]); + } + + [Fact] + public void ParametersParsedFromUriWithEmptyPathSegments() + { + var uri = new Uri("app://forms/MainPage//DetailPage"); + var target = UriParsingHelper.GetUriSegments(uri); + Assert.Equal(2, target.Count); + } + + [Fact] + public void EnsureAbsoluteUriForRelativeUri() + { + var uri = UriParsingHelper.EnsureAbsolute(new Uri(_relativeUri, UriKind.Relative)); + Assert.True(uri.IsAbsoluteUri); + } + + [Fact] + public void EnsureAbsoluteUriForRelativeUriThatStartsWithSlash() + { + var uri = UriParsingHelper.EnsureAbsolute(new Uri("/" + _relativeUri, UriKind.Relative)); + Assert.True(uri.IsAbsoluteUri); + } + + [Fact] + public void EnsureAbsoluteUriForAbsoluteUri() + { + var uri = UriParsingHelper.EnsureAbsolute(new Uri(_absoluteUri, UriKind.Absolute)); + Assert.True(uri.IsAbsoluteUri); + } + + [Fact] + public void ParseForNull() + { + var actual = Assert.Throws(() => UriParsingHelper.Parse(null)); + Assert.NotNull(actual); + Assert.Equal("uri", actual.ParamName); + } + + [Fact] + public void ParseForRelativeUri() + { + var uri = UriParsingHelper.Parse(_relativeUri); + Assert.NotNull(uri); + Assert.Equal(_relativeUri, uri.OriginalString); + Assert.False(uri.IsAbsoluteUri); + } + + [Fact] + public void ParseForAbsoluteUri() + { + var uri = UriParsingHelper.Parse(_absoluteUri); + Assert.NotNull(uri); + Assert.Equal(_absoluteUri, uri.OriginalString); + Assert.True(uri.IsAbsoluteUri); + } + + [Fact] + public void ParseForAbsoluteUriWithOutProtocol() + { + var uri = UriParsingHelper.Parse(_absoluteUriWithOutProtocol); + Assert.NotNull(uri); + Assert.Equal("app://prismapp.maui" + _absoluteUriWithOutProtocol, uri.OriginalString); + Assert.True(uri.IsAbsoluteUri); + } + + [Fact] + public void ParseForDeepLinkAbsoluteUri() + { + var uri = UriParsingHelper.Parse(_deepLinkAbsoluteUri); + Assert.NotNull(uri); + Assert.Equal(_deepLinkAbsoluteUri, uri.OriginalString); + Assert.True(uri.IsAbsoluteUri); + } + + [Fact] + public void ParseForDeepLinkRelativeUri() + { + var uri = UriParsingHelper.Parse(_deepLinkRelativeUri); + Assert.NotNull(uri); + Assert.Equal(_deepLinkRelativeUri, uri.OriginalString); + Assert.False(uri.IsAbsoluteUri); + } +} From 072fd48548a8952163bd055f4528cd831994614e Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Fri, 17 Jun 2022 21:14:32 -0700 Subject: [PATCH 07/12] add Create Tab Builder Tests --- .../Navigation/Builder/CreateTabBuilder.cs | 11 ++- .../Navigation/Builder/NavigationBuilder.cs | 5 +- .../Builder/TabbedSegmentBuilder.cs | 4 +- .../Navigation/NavigationBuilderFixture.cs | 91 ++++++++++++++++++- 4 files changed, 103 insertions(+), 8 deletions(-) diff --git a/src/Prism.Maui/Navigation/Builder/CreateTabBuilder.cs b/src/Prism.Maui/Navigation/Builder/CreateTabBuilder.cs index 52a8892..750b3d9 100644 --- a/src/Prism.Maui/Navigation/Builder/CreateTabBuilder.cs +++ b/src/Prism.Maui/Navigation/Builder/CreateTabBuilder.cs @@ -1,18 +1,23 @@ using System.Web; +using Prism.Common; +using Prism.Mvvm; namespace Prism.Navigation.Builder; -internal class CreateTabBuilder : ICreateTabBuilder, IUriSegment +internal class CreateTabBuilder : ICreateTabBuilder, IUriSegment, IRegistryAware { private List _segments { get; } - public CreateTabBuilder() + public CreateTabBuilder(IViewRegistry registry) { _segments = new List(); + Registry = registry; } public string Segment => BuildSegment(); + public IViewRegistry Registry { get; } + public ICreateTabBuilder AddSegment(string segmentName, Action configureSegment) { var builder = new SegmentBuilder(segmentName); @@ -24,6 +29,6 @@ public ICreateTabBuilder AddSegment(string segmentName, Action private string BuildSegment() { var uri = string.Join("/", _segments.Select(x => x.Segment)); - return HttpUtility.UrlEncode(uri); + return HttpUtility.HtmlEncode(uri); } } diff --git a/src/Prism.Maui/Navigation/Builder/NavigationBuilder.cs b/src/Prism.Maui/Navigation/Builder/NavigationBuilder.cs index dd41acf..1fd2d56 100644 --- a/src/Prism.Maui/Navigation/Builder/NavigationBuilder.cs +++ b/src/Prism.Maui/Navigation/Builder/NavigationBuilder.cs @@ -86,8 +86,9 @@ public INavigationBuilder WithParameters(INavigationParameters parameters) internal Uri BuildUri() { - var uri = string.Join("/", _uriSegments.Select(x => x.Segment)); + var uri = (_absoluteNavigation ? "/" : string.Empty) + + string.Join("/", _uriSegments.Select(x => x.Segment)); - return _absoluteNavigation ? new Uri(RootUri, uri) : new Uri(uri, UriKind.Relative); + return UriParsingHelper.Parse(uri); } } diff --git a/src/Prism.Maui/Navigation/Builder/TabbedSegmentBuilder.cs b/src/Prism.Maui/Navigation/Builder/TabbedSegmentBuilder.cs index c684690..ccd413f 100644 --- a/src/Prism.Maui/Navigation/Builder/TabbedSegmentBuilder.cs +++ b/src/Prism.Maui/Navigation/Builder/TabbedSegmentBuilder.cs @@ -1,4 +1,4 @@ -using Prism.Common; +using Prism.Common; using Prism.Mvvm; namespace Prism.Navigation.Builder; @@ -48,7 +48,7 @@ public ITabbedSegmentBuilder CreateTab(Action configureSegmen throw new ArgumentNullException(nameof(configureSegment)); } - var builder = new CreateTabBuilder(); + var builder = new CreateTabBuilder(((IRegistryAware)_builder).Registry); configureSegment(builder); return AddSegmentParameter(KnownNavigationParameters.CreateTab, builder.Segment); } diff --git a/tests/Prism.Maui.Tests/Fixtures/Navigation/NavigationBuilderFixture.cs b/tests/Prism.Maui.Tests/Fixtures/Navigation/NavigationBuilderFixture.cs index d9f317e..1487f5a 100644 --- a/tests/Prism.Maui.Tests/Fixtures/Navigation/NavigationBuilderFixture.cs +++ b/tests/Prism.Maui.Tests/Fixtures/Navigation/NavigationBuilderFixture.cs @@ -1,4 +1,9 @@ -using Moq; +using System.Linq; +using System.Web; +using Microsoft.Maui.Controls; +using Moq; +using Prism.Common; +using Prism.Maui.Tests.Mocks.Ioc; namespace Prism.Maui.Tests.Fixtures.Navigation; @@ -53,4 +58,88 @@ public void GeneratesAbsoluteUriWithMultipleSegments() Assert.Equal("app://prismapp.maui/ViewA/ViewB/ViewC", uri.ToString()); } + + [Fact] + public void GeneratesTabbedPageUriWithCreatedTabs() + { + var container = new TestContainer(); + container.RegisterForNavigation(); + + var navigationService = new Mock(); + navigationService + .As() + .Setup(x => x.Registry) + .Returns(container.Resolve()); + var uri = navigationService.Object + .CreateBuilder() + .AddTabbedSegment(b => + { + b.CreateTab("ViewA") + .CreateTab("ViewB") + .CreateTab("ViewC"); + }) + .Uri; + + Assert.Equal("TabbedPage?createTab=ViewA&createTab=ViewB&createTab=ViewC", uri.ToString()); + } + + [Fact] + public void GeneratesTabbedPageUriWithCreatedTabsWithParameters() + { + var container = new TestContainer(); + container.RegisterForNavigation(); + + var navigationService = new Mock(); + navigationService + .As() + .Setup(x => x.Registry) + .Returns(container.Resolve()); + var uri = navigationService.Object + .CreateBuilder() + .AddTabbedSegment(b => + { + b.CreateTab("ViewA", t => t.AddParameter("id", 5)) + .CreateTab("ViewB", t => t.AddParameter("foo", "bar")) + .CreateTab("ViewC"); + }) + .Uri; + + Assert.Equal("TabbedPage?createTab=ViewA%3Fid%3D5&createTab=ViewB%3Ffoo%3Dbar&createTab=ViewC", uri.ToString()); + + var parameters = UriParsingHelper.GetSegmentParameters(uri.ToString()); + Assert.Equal(3, parameters.Count); + Assert.True(parameters.All(x => x.Key == KnownNavigationParameters.CreateTab)); + + Assert.Contains(parameters, x => HttpUtility.UrlDecode(x.Value.ToString()) == "ViewA?id=5"); + Assert.Contains(parameters, x => HttpUtility.UrlDecode(x.Value.ToString()) == "ViewB?foo=bar"); + } + + [Fact] + public void GeneratesDeepLinkTabCreation() + { + var container = new TestContainer(); + container.RegisterForNavigation(); + container.RegisterForNavigation(); + + var navigationService = new Mock(); + navigationService + .As() + .Setup(x => x.Registry) + .Returns(container.Resolve()); + var uri = navigationService.Object + .CreateBuilder() + .AddTabbedSegment(b => + b.CreateTab(t => t.AddNavigationPage() + .AddSegment("ViewA") + .AddSegment("ViewB", s => s.AddParameter("id", 5)) + .AddSegment("ViewC")) + .CreateTab("ViewD")) + .Uri; + + Assert.Equal("TabbedPage?createTab=NavigationPage%2FViewA%2FViewB%3Fid%3D5%2FViewC&createTab=ViewD", uri.ToString()); + + var parameters = UriParsingHelper.GetSegmentParameters(uri.ToString()); + Assert.Equal(2, parameters.Count); + Assert.Contains(parameters, x => HttpUtility.UrlDecode(x.Value.ToString()) == "NavigationPage/ViewA/ViewB?id=5/ViewC"); + } } From 11d5bbcb64ecbe66e5ec1593086d6c2a22cb3c52 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sat, 18 Jun 2022 09:32:22 -0700 Subject: [PATCH 08/12] adding Navigation Tests --- .../Fixtures/Navigation/NavigationTests.cs | 86 ++++++++++++++++++- 1 file changed, 85 insertions(+), 1 deletion(-) diff --git a/tests/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs b/tests/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs index 949aa15..653fdfb 100644 --- a/tests/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs +++ b/tests/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs @@ -1,6 +1,7 @@ using Prism.Controls; using Prism.DryIoc.Maui.Tests.Mocks.ViewModels; using Prism.DryIoc.Maui.Tests.Mocks.Views; +using Prism.Navigation.Xaml; namespace Prism.DryIoc.Maui.Tests.Fixtures.Navigation; @@ -11,7 +12,7 @@ public class NavigationTests [InlineData("MockHome/NavigationPage/MockViewA")] public void PagesInjectScopedInstanceOfIPageAccessor(string uri) { - var mauiApp = CreateBuilder(prism => prism.OnAppStart(navigation => navigation.NavigateAsync(uri))) + var mauiApp = CreateBuilder(prism => prism.OnAppStart(uri)) .Build(); var app = mauiApp.Services.GetRequiredService() as Application; var window = app!.Windows.First(); @@ -32,6 +33,89 @@ public void PagesInjectScopedInstanceOfIPageAccessor(string uri) } } + [Fact] + public async Task AddsPageFromRelativeURI() + { + var mauiApp = CreateBuilder(prism => prism.OnAppStart("NavigationPage/MockViewA")) + .Build(); + var app = mauiApp.Services.GetRequiredService() as Application; + var window = app!.Windows.First(); + + var rootPage = window.Page as NavigationPage; + Assert.NotNull(rootPage); + TestPage(rootPage); + var currentPage = rootPage.CurrentPage; + Assert.IsType(currentPage); + TestPage(currentPage); + var container = currentPage.GetContainerProvider(); + var navService = container.Resolve(); + Assert.Single(rootPage.Navigation.NavigationStack); + await navService.NavigateAsync("MockViewB"); + Assert.IsType(rootPage.CurrentPage); + TestPage(rootPage.CurrentPage); + Assert.Equal(2, rootPage.Navigation.NavigationStack.Count); + } + + [Fact] + public async Task RelativeNavigation_RemovesPage_AndNavigates() + { + var mauiApp = CreateBuilder(prism => prism.OnAppStart("NavigationPage/MockViewA/MockViewB")) + .Build(); + var app = mauiApp.Services.GetRequiredService() as Application; + var window = app!.Windows.First(); + + var rootPage = window.Page as NavigationPage; + Assert.NotNull(rootPage); + TestPage(rootPage); + var currentPage = rootPage.CurrentPage; + Assert.IsType(currentPage); + TestPage(currentPage); + var container = currentPage.GetContainerProvider(); + var navService = container.Resolve(); + Assert.Equal(2, rootPage.Navigation.NavigationStack.Count); + await navService.NavigateAsync("../MockViewC"); + Assert.IsType(rootPage.CurrentPage); + TestPage(rootPage.CurrentPage); + Assert.Equal(2, rootPage.Navigation.NavigationStack.Count); + } + + [Fact] + public async Task AbsoluteNavigation_ResetsWindowPage() + { + var mauiApp = CreateBuilder(prism => prism.OnAppStart("MockViewA")) + .Build(); + var app = mauiApp.Services.GetRequiredService() as Application; + var window = app!.Windows.First(); + + var rootPage = window.Page as MockViewA; + Assert.NotNull(rootPage); + var container = rootPage.GetContainerProvider(); + var navService = container.Resolve(); + var result = await navService.NavigateAsync("/MockViewB"); + Assert.True(result.Success); + Assert.NotEqual(rootPage, window.Page); + } + + [Fact] + public async Task AddsModalPageFromRelativeURI() + { + var mauiApp = CreateBuilder(prism => prism.OnAppStart("MockViewA")) + .Build(); + var app = mauiApp.Services.GetRequiredService() as Application; + var window = app!.Windows.First(); + + var rootPage = window.Page as MockViewA; + Assert.NotNull(rootPage); + Assert.IsType(rootPage); + var container = rootPage.GetContainerProvider(); + var navService = container.Resolve(); + Assert.Empty(rootPage.Navigation.ModalStack); + var result = await navService.NavigateAsync("MockViewB"); + Assert.True(result.Success); + Assert.Single(rootPage.Navigation.ModalStack); + Assert.IsType(rootPage.Navigation.ModalStack.Last()); + } + private void TestPage(Page page) { Assert.NotNull(page.BindingContext); From e483c6e5cddcd4de7717b45422094bd0771556ac Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sun, 19 Jun 2022 13:41:44 -0700 Subject: [PATCH 09/12] add TabPage Deep Linking --- sample/MauiModule/ViewModels/ViewModelBase.cs | 2 + sample/PrismMauiDemo/MauiProgram.cs | 20 +- .../Resources/Styles/Styles.xaml | 702 +++++++++--------- src/Prism.Maui/Common/MvvmHelpers.cs | 1 + .../Navigation/NavigationRegistry.cs | 3 +- .../Navigation/PageNavigationService.cs | 183 +++-- .../Navigation/Xaml/TabBindingSource.cs | 7 + src/Prism.Maui/Navigation/Xaml/TabbedPage.cs | 31 + src/Prism.Maui/PrismAppBuilder.cs | 6 +- .../Fixtures/Navigation/NavigationTests.cs | 21 +- .../Fixtures/TestBase.cs | 59 ++ .../Mocks/Logging/XUnitLogger.cs | 47 ++ .../Mocks/Logging/XUnitLoggerProvider.cs | 23 + .../Mocks/Logging/XUnitLogger{T}.cs | 11 + .../Prism.DryIoc.Maui.Tests.csproj | 4 +- 15 files changed, 686 insertions(+), 434 deletions(-) create mode 100644 src/Prism.Maui/Navigation/Xaml/TabBindingSource.cs create mode 100644 src/Prism.Maui/Navigation/Xaml/TabbedPage.cs create mode 100644 tests/Prism.DryIoc.Maui.Tests/Fixtures/TestBase.cs create mode 100644 tests/Prism.DryIoc.Maui.Tests/Mocks/Logging/XUnitLogger.cs create mode 100644 tests/Prism.DryIoc.Maui.Tests/Mocks/Logging/XUnitLoggerProvider.cs create mode 100644 tests/Prism.DryIoc.Maui.Tests/Mocks/Logging/XUnitLogger{T}.cs diff --git a/sample/MauiModule/ViewModels/ViewModelBase.cs b/sample/MauiModule/ViewModels/ViewModelBase.cs index 9202e17..6155d21 100644 --- a/sample/MauiModule/ViewModels/ViewModelBase.cs +++ b/sample/MauiModule/ViewModels/ViewModelBase.cs @@ -50,6 +50,8 @@ private void OnShowPageDialog() public void Initialize(INavigationParameters parameters) { Messages.Add("ViewModel Initialized"); + foreach (var parameter in parameters.Where(x => x.Key.Contains("message"))) + Messages.Add(parameter.Value.ToString()); } public void OnNavigatedFrom(INavigationParameters parameters) diff --git a/sample/PrismMauiDemo/MauiProgram.cs b/sample/PrismMauiDemo/MauiProgram.cs index 8d8c591..342d3ba 100644 --- a/sample/PrismMauiDemo/MauiProgram.cs +++ b/sample/PrismMauiDemo/MauiProgram.cs @@ -28,7 +28,7 @@ public static MauiApp CreateMauiApp() .AddGlobalNavigationObserver(context => context.Subscribe(x => { if (x.Type == NavigationRequestType.Navigate) - Console.WriteLine($"Navigation: {x.Type} - {x.Uri}"); + Console.WriteLine($"Navigation: {x.Uri}"); else Console.WriteLine($"Navigation: {x.Type}"); @@ -38,9 +38,21 @@ public static MauiApp CreateMauiApp() if (status == "Failed" && !string.IsNullOrEmpty(x.Result?.Exception?.Message)) Console.Error.WriteLine(x.Result.Exception.Message); })) - .OnAppStart(navigationService => navigationService.CreateBuilder() - .AddSegment() - .Navigate(HandleNavigationError)) + .OnAppStart(nav => nav.CreateBuilder() + .AddTabbedSegment(page => + page.CreateTab("ViewC") + .CreateTab(t => + t.AddNavigationPage() + .AddSegment("ViewA", s => s.AddParameter("message", "Hello Tab - ViewA")) + .AddSegment("ViewB", s => s.AddParameter("message", "Hello Tab - ViewB"))) + //.CreateTab("ViewC", s => s.AddParameter("message", "Hello Tab - ViewC")) + .SelectedTab("NavigationPage|ViewB")) + .AddParameter("message_global", "This is a Global Message") + .Navigate()) + //.OnAppStart("ViewA/ViewB/ViewC") + //.OnAppStart(navigationService => navigationService.CreateBuilder() + // .AddSegment() + // .Navigate(HandleNavigationError)) ) .ConfigureFonts(fonts => { diff --git a/sample/PrismMauiDemo/Resources/Styles/Styles.xaml b/sample/PrismMauiDemo/Resources/Styles/Styles.xaml index 3b0fdc4..8aeecab 100644 --- a/sample/PrismMauiDemo/Resources/Styles/Styles.xaml +++ b/sample/PrismMauiDemo/Resources/Styles/Styles.xaml @@ -2,383 +2,385 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" + xmlns:android="clr-namespace:Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific;assembly=Microsoft.Maui.Controls"> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/src/Prism.Maui/Common/MvvmHelpers.cs b/src/Prism.Maui/Common/MvvmHelpers.cs index be1919f..d99a07e 100644 --- a/src/Prism.Maui/Common/MvvmHelpers.cs +++ b/src/Prism.Maui/Common/MvvmHelpers.cs @@ -4,6 +4,7 @@ using Prism.Navigation.Xaml; using Prism.Regions.Navigation; using NavigationMode = Prism.Navigation.NavigationMode; +using TabbedPage = Microsoft.Maui.Controls.TabbedPage; namespace Prism.Common; diff --git a/src/Prism.Maui/Navigation/NavigationRegistry.cs b/src/Prism.Maui/Navigation/NavigationRegistry.cs index e8d1e38..bbbd021 100644 --- a/src/Prism.Maui/Navigation/NavigationRegistry.cs +++ b/src/Prism.Maui/Navigation/NavigationRegistry.cs @@ -3,12 +3,13 @@ using Prism.Ioc; using Prism.Mvvm; using Prism.Navigation.Xaml; +using TabbedPage = Microsoft.Maui.Controls.TabbedPage; namespace Prism.Navigation; internal class NavigationRegistry : ViewRegistryBase, INavigationRegistry { - public NavigationRegistry(IEnumerable registrations) + public NavigationRegistry(IEnumerable registrations) : base(ViewType.Page, registrations) { } diff --git a/src/Prism.Maui/Navigation/PageNavigationService.cs b/src/Prism.Maui/Navigation/PageNavigationService.cs index 93bdb35..99eeb07 100644 --- a/src/Prism.Maui/Navigation/PageNavigationService.cs +++ b/src/Prism.Maui/Navigation/PageNavigationService.cs @@ -1,8 +1,12 @@ +using System.Text.RegularExpressions; +using System.Web; +using Microsoft.Maui.Controls; using Prism.Common; using Prism.Events; using Prism.Ioc; using Prism.Mvvm; using Application = Microsoft.Maui.Controls.Application; +using XamlTab = Prism.Navigation.Xaml.TabbedPage; namespace Prism.Navigation; @@ -213,7 +217,7 @@ public virtual async Task GoBackToRootAsync(INavigationParame /// The navigation parameters /// Navigation parameters can be provided in the Uri and by using the . /// - /// NavigateAsync(new Uri("MainPage?id=3&name=brian", UriKind.RelativeSource), parameters); + /// NavigateAsync(new Uri("MainPage?id=3&name=dan", UriKind.RelativeSource), parameters); /// public virtual async Task NavigateAsync(Uri uri, INavigationParameters parameters) { @@ -384,6 +388,8 @@ protected virtual Task ProcessNavigationForAbsoluteUri(Queue segments, I protected virtual async Task ProcessNavigationForRootPage(string nextSegment, Queue segments, INavigationParameters parameters, bool? useModalNavigation, bool animated) { var nextPage = CreatePageFromSegment(nextSegment); + if (nextPage is TabbedPage tabbedPage) + await ConfigureTabbedPage(tabbedPage, nextSegment, parameters); await ProcessNavigation(nextPage, segments, parameters, useModalNavigation, animated); @@ -406,6 +412,8 @@ protected virtual async Task ProcessNavigationForContentPage(Page currentPage, s if (!useReverse) { var nextPage = CreatePageFromSegment(nextSegment); + if (nextPage is TabbedPage tabbedPage) + await ConfigureTabbedPage(tabbedPage, nextSegment, parameters); await ProcessNavigation(nextPage, segments, parameters, useModalNavigation, animated); @@ -482,6 +490,8 @@ await DoNavigateAction(topPage, nextSegment, topPage, parameters, onNavigationAc protected virtual async Task ProcessNavigationForTabbedPage(TabbedPage currentPage, string nextSegment, Queue segments, INavigationParameters parameters, bool? useModalNavigation, bool animated) { var nextPage = CreatePageFromSegment(nextSegment); + if (nextPage is TabbedPage tabbedPage) + await ConfigureTabbedPage(tabbedPage, nextSegment, parameters); await ProcessNavigation(nextPage, segments, parameters, useModalNavigation, animated); await DoNavigateAction(currentPage, nextSegment, nextPage, parameters, async () => { @@ -497,6 +507,8 @@ protected virtual async Task ProcessNavigationForFlyoutPage(FlyoutPage currentPa if (detail is null) { var newDetail = CreatePageFromSegment(nextSegment); + if (newDetail is TabbedPage tabbedPage) + await ConfigureTabbedPage(tabbedPage, nextSegment, parameters); await ProcessNavigation(newDetail, segments, parameters, useModalNavigation, animated); await DoNavigateAction(null, nextSegment, newDetail, parameters, onNavigationActionCompleted: (p) => { @@ -509,6 +521,8 @@ await DoNavigateAction(null, nextSegment, newDetail, parameters, onNavigationAct if (useModalNavigation.HasValue && useModalNavigation.Value) { var nextPage = CreatePageFromSegment(nextSegment); + if (nextPage is TabbedPage tabbedPage) + await ConfigureTabbedPage(tabbedPage, nextSegment, parameters); await ProcessNavigation(nextPage, segments, parameters, useModalNavigation, animated); await DoNavigateAction(currentPage, nextSegment, nextPage, parameters, async () => { @@ -565,6 +579,8 @@ await DoNavigateAction(null, nextSegment, detail, parameters, onNavigationAction else { var newDetail = CreatePageFromSegment(nextSegment); + if (newDetail is TabbedPage tabbedPage) + await ConfigureTabbedPage(tabbedPage, nextSegment, parameters); await ProcessNavigation(newDetail, segments, parameters, newDetail is NavigationPage ? false : true, animated); await DoNavigateAction(detail, nextSegment, newDetail, parameters, onNavigationActionCompleted: (p) => { @@ -714,59 +730,69 @@ protected virtual Page CreatePageFromSegment(string segment) throw new NavigationException(NavigationException.NoPageIsRegistered, _pageAccessor.Page, innerException); } - ConfigurePages(page, segment); - return page; } - void ConfigurePages(Page page, string segment) + async Task ConfigureTabbedPage(TabbedPage tabbedPage, string segment, INavigationParameters parameters) { - if (page is TabbedPage) - { - ConfigureTabbedPage((TabbedPage)page, segment); - } - } + var tabParameters = UriParsingHelper.GetSegmentParameters(segment); - void ConfigureTabbedPage(TabbedPage tabbedPage, string segment) - { - var parameters = UriParsingHelper.GetSegmentParameters(segment); - - var tabsToCreate = parameters.GetValues(KnownNavigationParameters.CreateTab); - if (tabsToCreate.Count() > 0) + var tabsToCreate = tabParameters.GetValues(KnownNavigationParameters.CreateTab); + foreach (var tabToCreateEncoded in tabsToCreate ?? Array.Empty()) { - foreach (var tabToCreate in tabsToCreate) + //created tab can be a single view or a view nested in a NavigationPage with the syntax "NavigationPage|ViewToCreate" + var tabToCreate = HttpUtility.UrlDecode(tabToCreateEncoded); + var tabSegments = tabToCreate.Split('/', '|'); + NavigationPage navigationPage = null; + for(int i = 0; i < tabSegments.Length; i++) { - //created tab can be a single view or a view nested in a NavigationPage with the syntax "NavigationPage|ViewToCreate" - var tabSegments = tabToCreate.Split('|'); - if (tabSegments.Length > 1) + var tabSegment = tabSegments[i]; + var child = CreatePageFromSegment(tabSegment); + var childParameters = UriParsingHelper.GetSegmentParameters(tabSegment, parameters); + await MvvmHelpers.OnInitializedAsync(child, childParameters); + if (i == 0 && child is NavigationPage navPage) { - var navigationPage = CreatePageFromSegment(tabSegments[0]) as NavigationPage; - if (navigationPage != null) - { - var navigationPageChild = CreatePageFromSegment(tabSegments[1]); - - navigationPage.PushAsync(navigationPageChild); - - //when creating a NavigationPage w/ DI, a blank Page object is injected into the ctor. Let's remove it - if (navigationPage.Navigation.NavigationStack.Count > 1) - navigationPage.Navigation.RemovePage(navigationPage.Navigation.NavigationStack[0]); - - //set the title because Xamarin doesn't do this for us. - navigationPage.Title = navigationPageChild.Title; - navigationPage.IconImageSource = navigationPageChild.IconImageSource; - - tabbedPage.Children.Add(navigationPage); - } + navigationPage = navPage; } - else + else if(i == 0) { - var tab = CreatePageFromSegment(tabToCreate); - tabbedPage.Children.Add(tab); + tabbedPage.Children.Add(child); + break; + } + else if(i > 0 && navigationPage is not null) + { + await navigationPage.Navigation.PushAsync(child); } } + + if(navigationPage is null) + { + continue; + } + + tabbedPage.Children.Add(navigationPage); + if (navigationPage.RootPage.IsSet(XamlTab.TitleProperty)) + { + navigationPage.Title = XamlTab.GetTitle(navigationPage.RootPage); + navigationPage.IconImageSource = XamlTab.GetIconImageSource(navigationPage.RootPage); + } + else if(!navigationPage.IsSet(Page.TitleProperty)) + { + var source = navigationPage.IsSet(XamlTab.TitleBindingSourceProperty) ? + XamlTab.GetTitleBindingSource(navigationPage) : + navigationPage.RootPage.IsSet(XamlTab.TitleBindingSourceProperty) ? + XamlTab.GetTitleBindingSource(navigationPage.RootPage) : + Xaml.TabBindingSource.RootPage; + + //set the title because Xamarin doesn't do this for us. + if (!navigationPage.IsSet(Page.TitleProperty)) + navigationPage.SetBinding(Page.TitleProperty, new Binding($"{source}.Title", BindingMode.OneWay, source: navigationPage)); + if (!navigationPage.IsSet(Page.IconImageSourceProperty)) + navigationPage.SetBinding(Page.IconImageSourceProperty, new Binding($"{source}.IconImageSource", BindingMode.OneWay, source: navigationPage)); + } } - TabbedPageSelectTab(tabbedPage, parameters); + TabbedPageSelectTab(tabbedPage, tabParameters); } private void SelectPageTab(Page page, INavigationParameters parameters) @@ -779,29 +805,60 @@ private void SelectPageTab(Page page, INavigationParameters parameters) private void TabbedPageSelectTab(TabbedPage tabbedPage, INavigationParameters parameters) { - var selectedTab = parameters?.GetValue(KnownNavigationParameters.SelectedTab); - if (!string.IsNullOrWhiteSpace(selectedTab)) + if (!parameters.TryGetValue(KnownNavigationParameters.SelectedTab, out var selectedTab) + || string.IsNullOrEmpty(selectedTab)) + return; + + var segments = selectedTab.Split('|').Where(x => !string.IsNullOrEmpty(x)); + if (segments.Count() == 1) + TabbedPageSelectRootTab(tabbedPage, selectedTab); + else if (segments.Count() > 1) + TabbedPageSelectNavigationChildTab(tabbedPage, segments.Last()); + } + + private void TabbedPageSelectRootTab(TabbedPage tabbedPage, string selectedTab) + { + var child = tabbedPage.Children + .FirstOrDefault(x => selectedTab == (string)x.GetValue(ViewModelLocator.NavigationNameProperty)); + if (child is not null) { - var selectedTabType = Registry.GetViewType(UriParsingHelper.GetSegmentName(selectedTab)); + tabbedPage.CurrentPage = child; + return; + } - var childFound = false; - foreach (var child in tabbedPage.Children) - { - if (!childFound && child.GetType() == selectedTabType) - { - tabbedPage.CurrentPage = child; - childFound = true; - } + var registration = Registry.Registrations + .LastOrDefault(x => x.Type == ViewType.Page && x.Name == selectedTab); + if (registration is null) + throw new InvalidOperationException($"No Tab has been registered with the name '{selectedTab}'"); - if (child is NavigationPage) - { - if (!childFound && ((NavigationPage)child).CurrentPage.GetType() == selectedTabType) - { - tabbedPage.CurrentPage = child; - childFound = true; - } - } - } + child = tabbedPage.Children + .FirstOrDefault(x => x.GetType() == registration.View); + if (child is not null) + { + tabbedPage.CurrentPage = child; + } + } + + private void TabbedPageSelectNavigationChildTab(TabbedPage tabbedPage, string rootTab, string selectedTab) + { + var child = tabbedPage.Children + .FirstOrDefault(x => x is NavigationPage navPage && selectedTab == (string)navPage.CurrentPage.GetValue(ViewModelLocator.NavigationNameProperty)); + if (child is not null) + { + tabbedPage.CurrentPage = child; + return; + } + + var registration = Registry.Registrations + .LastOrDefault(x => x.Type == ViewType.Page && x.Name == selectedTab); + if (registration is null) + throw new InvalidOperationException($"No Tab has been registered with the name '{selectedTab}'"); + + child = tabbedPage.Children + .FirstOrDefault(x => x is NavigationPage navPage && navPage.CurrentPage.GetType() == registration.View); + if (child is not null) + { + tabbedPage.CurrentPage = child; } } @@ -866,6 +923,8 @@ protected virtual async Task UseReverseNavigation(Page currentPage, string nextS { var segment = navigationStack.Pop(); var nextPage = CreatePageFromSegment(segment); + if (nextPage is TabbedPage tabbedPage) + await ConfigureTabbedPage(tabbedPage, nextSegment, parameters); await DoNavigateAction(onNavigatedFromTarget, segment, nextPage, parameters, async () => { await DoPush(currentPage, nextPage, useModalNavigation, animated, insertBefore, pageOffset); @@ -1032,12 +1091,14 @@ private INavigationResult Notify(Uri uri, INavigationParameters parameters, Exce Exception = exception }; + var temp = Regex.Replace(uri.ToString(), RemovePageInstruction, RemovePageRelativePath); + _eventAggregator.GetEvent().Publish(new NavigationRequestContext { Parameters = parameters, Result = result, Type = NavigationRequestType.Navigate, - Uri = uri, + Uri = new Uri(temp, UriKind.RelativeOrAbsolute), }); return result; diff --git a/src/Prism.Maui/Navigation/Xaml/TabBindingSource.cs b/src/Prism.Maui/Navigation/Xaml/TabBindingSource.cs new file mode 100644 index 0000000..0a0df96 --- /dev/null +++ b/src/Prism.Maui/Navigation/Xaml/TabBindingSource.cs @@ -0,0 +1,7 @@ +namespace Prism.Navigation.Xaml; + +public enum TabBindingSource +{ + RootPage, + CurrentPage +} diff --git a/src/Prism.Maui/Navigation/Xaml/TabbedPage.cs b/src/Prism.Maui/Navigation/Xaml/TabbedPage.cs new file mode 100644 index 0000000..0c2b35f --- /dev/null +++ b/src/Prism.Maui/Navigation/Xaml/TabbedPage.cs @@ -0,0 +1,31 @@ +namespace Prism.Navigation.Xaml; + +public static class TabbedPage +{ + public static BindableProperty TitleBindingSourceProperty = + BindableProperty.CreateAttached("TitleBindingSource", typeof(TabBindingSource), typeof(TabbedPage), TabBindingSource.RootPage); + + public static BindableProperty TitleProperty = + BindableProperty.CreateAttached("Title", typeof(string), typeof(TabbedPage), null); + + public static BindableProperty IconImageSourceProperty = + BindableProperty.CreateAttached("IconImageSource", typeof(ImageSource), typeof(TabbedPage), null); + + public static TabBindingSource GetTitleBindingSource(Page page) => + (TabBindingSource)page.GetValue(TitleBindingSourceProperty); + + public static void SetTitleBindingSource(Page page, TabBindingSource bindingSource) => + page.SetValue(TitleBindingSourceProperty, bindingSource); + + public static string GetTitle(Page page) => + (string)page.GetValue(TitleProperty); + + public static void SetTitle(Page page, string title) => + page.SetValue(TitleProperty, title); + + public static ImageSource GetIconImageSource(Page page) => + (ImageSource)page.GetValue(IconImageSourceProperty); + + public static void SetIconImageSource(Page page, ImageSource imageSource) => + page.SetValue(IconImageSourceProperty, imageSource); +} \ No newline at end of file diff --git a/src/Prism.Maui/PrismAppBuilder.cs b/src/Prism.Maui/PrismAppBuilder.cs index 742acfd..8a2e021 100644 --- a/src/Prism.Maui/PrismAppBuilder.cs +++ b/src/Prism.Maui/PrismAppBuilder.cs @@ -11,6 +11,7 @@ using Prism.Regions.Adapters; using Prism.Regions.Behaviors; using Prism.Services; +using TabbedPage = Microsoft.Maui.Controls.TabbedPage; namespace Prism; @@ -117,7 +118,10 @@ internal void OnInitialized() } if (!navRegistry.IsRegistered(nameof(TabbedPage))) - ((IContainerRegistry)_container).RegisterForNavigation(); + { + var registry = _container as IContainerRegistry; + registry.RegisterForNavigation(); + } if (app is ILegacyPrismApplication prismApp) prismApp.OnInitialized(); diff --git a/tests/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs b/tests/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs index 653fdfb..5577e1a 100644 --- a/tests/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs +++ b/tests/Prism.DryIoc.Maui.Tests/Fixtures/Navigation/NavigationTests.cs @@ -5,8 +5,13 @@ namespace Prism.DryIoc.Maui.Tests.Fixtures.Navigation; -public class NavigationTests +public class NavigationTests : TestBase { + public NavigationTests(ITestOutputHelper testOutputHelper) + : base(testOutputHelper) + { + } + [Theory] [InlineData("NavigationPage/MockViewA/MockViewB/MockViewC")] [InlineData("MockHome/NavigationPage/MockViewA")] @@ -136,18 +141,4 @@ private void TestPage(Page page) Assert.NotNull(viewModel); Assert.Same(page, viewModel!.Page); } - - private MauiAppBuilder CreateBuilder(Action configurePrism) => - MauiApp.CreateBuilder() - .UsePrismApp(prism => - { - prism.RegisterTypes(container => - { - container.RegisterForNavigation() - .RegisterForNavigation() - .RegisterForNavigation() - .RegisterForNavigation(); - }); - configurePrism(prism); - }); } diff --git a/tests/Prism.DryIoc.Maui.Tests/Fixtures/TestBase.cs b/tests/Prism.DryIoc.Maui.Tests/Fixtures/TestBase.cs new file mode 100644 index 0000000..2e304e3 --- /dev/null +++ b/tests/Prism.DryIoc.Maui.Tests/Fixtures/TestBase.cs @@ -0,0 +1,59 @@ +using Prism.DryIoc.Maui.Tests.Mocks.ViewModels; +using Prism.DryIoc.Maui.Tests.Mocks.Views; +using Microsoft.Extensions.Logging; +using Prism.DryIoc.Maui.Tests.Mocks.Logging; + +namespace Prism.DryIoc.Maui.Tests.Fixtures; + +public abstract class TestBase +{ + protected readonly ITestOutputHelper _testOutputHelper; + + protected TestBase(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + protected MauiAppBuilder CreateBuilder(Action configurePrism) + { + return MauiApp.CreateBuilder() + .UsePrismApp(prism => + { + prism.RegisterTypes(container => + { + container.RegisterForNavigation() + .RegisterForNavigation() + .RegisterForNavigation() + .RegisterForNavigation(); + }) + .ConfigureLogging(builder => + builder.AddProvider(new XUnitLoggerProvider(_testOutputHelper))) + .OnInitialized(container => + { + var ea = container.Resolve(); + ea.GetEvent().Subscribe(context => + { + if (System.Diagnostics.Debugger.IsAttached) + System.Diagnostics.Debugger.Break(); + + var logger = container.Resolve() + .CreateLogger(GetType().Name); + var message = context.Type == NavigationRequestType.Navigate ? $"{context.Type}: {context.Uri}" : $"{context.Type}"; + + message += context.Cancelled ? " - Cancelled" : context.Result.Exception is null ? " - Success" : " - Error"; + logger.LogInformation(message); + if (!context.Cancelled && context.Result.Exception is not null) + { + var ex = context.Result.Exception; + while(ex is not null) + { + logger.LogError(ex, "Navigation Error"); + ex = ex.InnerException; + } + } + }); + }); + configurePrism(prism); + }); + } +} diff --git a/tests/Prism.DryIoc.Maui.Tests/Mocks/Logging/XUnitLogger.cs b/tests/Prism.DryIoc.Maui.Tests/Mocks/Logging/XUnitLogger.cs new file mode 100644 index 0000000..a42a27e --- /dev/null +++ b/tests/Prism.DryIoc.Maui.Tests/Mocks/Logging/XUnitLogger.cs @@ -0,0 +1,47 @@ +using System.Text; +using Microsoft.Extensions.Logging; + +namespace Prism.DryIoc.Maui.Tests.Mocks.Logging; + +internal class XUnitLogger : ILogger +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly string _categoryName; + private readonly LoggerExternalScopeProvider _scopeProvider; + + public static ILogger CreateLogger(ITestOutputHelper testOutputHelper) => new XUnitLogger(testOutputHelper, new LoggerExternalScopeProvider(), ""); + public static ILogger CreateLogger(ITestOutputHelper testOutputHelper) => new XUnitLogger(testOutputHelper, new LoggerExternalScopeProvider()); + + public XUnitLogger(ITestOutputHelper testOutputHelper, LoggerExternalScopeProvider scopeProvider, string categoryName) + { + _testOutputHelper = testOutputHelper; + _scopeProvider = scopeProvider; + _categoryName = categoryName; + } + + public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; + + public IDisposable BeginScope(TState state) => _scopeProvider.Push(state); + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + var sb = new StringBuilder(); + sb.Append(logLevel) + .Append(" [").Append(_categoryName).Append("] ") + .Append(formatter(state, exception)); + + if (exception != null) + { + sb.Append('\n').Append(exception); + } + + // Append scopes + _scopeProvider.ForEachScope((scope, state) => + { + state.Append("\n => "); + state.Append(scope); + }, sb); + + _testOutputHelper.WriteLine(sb.ToString()); + } +} diff --git a/tests/Prism.DryIoc.Maui.Tests/Mocks/Logging/XUnitLoggerProvider.cs b/tests/Prism.DryIoc.Maui.Tests/Mocks/Logging/XUnitLoggerProvider.cs new file mode 100644 index 0000000..0e0712f --- /dev/null +++ b/tests/Prism.DryIoc.Maui.Tests/Mocks/Logging/XUnitLoggerProvider.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Logging; + +namespace Prism.DryIoc.Maui.Tests.Mocks.Logging; + +internal sealed class XUnitLoggerProvider : ILoggerProvider +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly LoggerExternalScopeProvider _scopeProvider = new (); + + public XUnitLoggerProvider(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + public ILogger CreateLogger(string categoryName) + { + return new XUnitLogger(_testOutputHelper, _scopeProvider, categoryName); + } + + public void Dispose() + { + } +} diff --git a/tests/Prism.DryIoc.Maui.Tests/Mocks/Logging/XUnitLogger{T}.cs b/tests/Prism.DryIoc.Maui.Tests/Mocks/Logging/XUnitLogger{T}.cs new file mode 100644 index 0000000..fd67ef6 --- /dev/null +++ b/tests/Prism.DryIoc.Maui.Tests/Mocks/Logging/XUnitLogger{T}.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.Logging; + +namespace Prism.DryIoc.Maui.Tests.Mocks.Logging; + +internal sealed class XUnitLogger : XUnitLogger, ILogger +{ + public XUnitLogger(ITestOutputHelper testOutputHelper, LoggerExternalScopeProvider scopeProvider) + : base(testOutputHelper, scopeProvider, typeof(T).FullName) + { + } +} diff --git a/tests/Prism.DryIoc.Maui.Tests/Prism.DryIoc.Maui.Tests.csproj b/tests/Prism.DryIoc.Maui.Tests/Prism.DryIoc.Maui.Tests.csproj index 73f74ab..62ee5b4 100644 --- a/tests/Prism.DryIoc.Maui.Tests/Prism.DryIoc.Maui.Tests.csproj +++ b/tests/Prism.DryIoc.Maui.Tests/Prism.DryIoc.Maui.Tests.csproj @@ -24,9 +24,9 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 32b729ebfe75802c90dcb7875b18a85212c764a3 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sun, 19 Jun 2022 13:43:16 -0700 Subject: [PATCH 10/12] add more context rich exceptions around failed navigation --- src/Prism.Maui/Mvvm/ViewCreationException.cs | 22 ++++ .../Mvvm/ViewModelCreationException.cs | 18 +++ src/Prism.Maui/Mvvm/ViewRegistryBase.cs | 7 +- .../Navigation/NavigationException.cs | 18 ++- .../Navigation/PageNavigationService.cs | 120 +++++++++++------- src/Prism.Maui/PrismAppBuilder.cs | 15 ++- 6 files changed, 139 insertions(+), 61 deletions(-) create mode 100644 src/Prism.Maui/Mvvm/ViewCreationException.cs create mode 100644 src/Prism.Maui/Mvvm/ViewModelCreationException.cs diff --git a/src/Prism.Maui/Mvvm/ViewCreationException.cs b/src/Prism.Maui/Mvvm/ViewCreationException.cs new file mode 100644 index 0000000..01180a2 --- /dev/null +++ b/src/Prism.Maui/Mvvm/ViewCreationException.cs @@ -0,0 +1,22 @@ +using Prism.Common; + +namespace Prism.Mvvm; + +public class ViewCreationException : Exception +{ + public ViewCreationException(string viewName, ViewType viewType) + : this(viewName, viewType, null) + { + } + + public ViewCreationException(string viewName, ViewType viewType, Exception innerException) + : base($"Unable to create {viewType} '{viewName}'.", innerException) + { + ViewName = viewName; + ViewType = viewType; + } + + public ViewType ViewType { get; } + + public string ViewName { get; } +} diff --git a/src/Prism.Maui/Mvvm/ViewModelCreationException.cs b/src/Prism.Maui/Mvvm/ViewModelCreationException.cs new file mode 100644 index 0000000..2e1c8f7 --- /dev/null +++ b/src/Prism.Maui/Mvvm/ViewModelCreationException.cs @@ -0,0 +1,18 @@ +namespace Prism.Mvvm; + +public class ViewModelCreationException : Exception +{ + public ViewModelCreationException(object view, Exception innerException) + : base($"Unable to Create ViewModel for '{view.GetType().FullName}'.", innerException) + { + if (view is VisualElement visualElement) + { + View = visualElement; + ViewName = (string)visualElement.GetValue(ViewModelLocator.NavigationNameProperty); + } + } + + public string ViewName { get; } + + public VisualElement View { get; } +} diff --git a/src/Prism.Maui/Mvvm/ViewRegistryBase.cs b/src/Prism.Maui/Mvvm/ViewRegistryBase.cs index 2c2d732..f7791dd 100644 --- a/src/Prism.Maui/Mvvm/ViewRegistryBase.cs +++ b/src/Prism.Maui/Mvvm/ViewRegistryBase.cs @@ -1,4 +1,5 @@ using System.Text.RegularExpressions; +using System.Xml.Linq; using Prism.Common; using Prism.Ioc; using Prism.Navigation.Xaml; @@ -47,9 +48,13 @@ public object CreateView(IContainerProvider container, string name) { throw; } + catch (ViewModelCreationException) + { + throw; + } catch (Exception ex) { - throw new Exception($"Unable to create {_registryType} '{name}'.", ex); + throw new ViewCreationException(name, _registryType, ex); } } diff --git a/src/Prism.Maui/Navigation/NavigationException.cs b/src/Prism.Maui/Navigation/NavigationException.cs index 9ac0c0e..848c671 100644 --- a/src/Prism.Maui/Navigation/NavigationException.cs +++ b/src/Prism.Maui/Navigation/NavigationException.cs @@ -9,10 +9,14 @@ public class NavigationException : Exception public const string IConfirmNavigationReturnedFalse = "IConfirmNavigation returned false"; public const string NoPageIsRegistered = "No Page has been registered with the provided key"; public const string ErrorCreatingPage = "An error occurred while resolving the page. This is most likely the result of invalid XAML or other type initialization exception"; + public const string UnsupportedMauiCreation = "An unsupported Maui Exception occurred. This may be due to a bug with MAUI or something that is otherwise not supported by MAUI."; + public const string UnsupportedMauiNavigation = "An unsupported event occurred while Navigating. The attempted Navigation Stack is not supported by .NET MAUI"; + public const string ErrorCreatingViewModel = "A dependency issue occurred while resolving the ViewModel. Check the InnerException for the ContainerResolutionException"; public const string MvvmPatternBreak = "You have referenced a View type and are likely breaking the MVVM pattern. You should never reference a View type from a ViewModel."; public const string UnknownException = "An unknown error occurred. You may need to specify whether to Use Modal Navigation or not."; public NavigationException() + : this(UnknownException) { } @@ -21,8 +25,8 @@ public NavigationException(string message) { } - public NavigationException(string message, Page page) - : this(message, page, null) + public NavigationException(string message, VisualElement view) + : this(message, view, null) { } @@ -36,18 +40,18 @@ public NavigationException(string message, string navigationKey, Exception inner { } - public NavigationException(string message, Page page, Exception innerException) - : this(message, null, page, innerException) + public NavigationException(string message, VisualElement view, Exception innerException) + : this(message, null, view, innerException) { } - public NavigationException(string message, string navigationKey, Page page, Exception innerException) : base(message, innerException) + public NavigationException(string message, string navigationKey, VisualElement view, Exception innerException) : base(message, innerException) { - Page = page; + View = view; NavigationKey = navigationKey; } - public Page Page { get; } + public VisualElement View { get; } public string NavigationKey { get; } } \ No newline at end of file diff --git a/src/Prism.Maui/Navigation/PageNavigationService.cs b/src/Prism.Maui/Navigation/PageNavigationService.cs index 99eeb07..3201ab2 100644 --- a/src/Prism.Maui/Navigation/PageNavigationService.cs +++ b/src/Prism.Maui/Navigation/PageNavigationService.cs @@ -708,14 +708,32 @@ protected virtual Page CreatePage(string segmentName) return page; } + catch(NavigationException) + { + throw; + } + catch(KeyNotFoundException knfe) + { + throw new NavigationException(NavigationException.NoPageIsRegistered, segmentName, knfe); + } + catch(ViewModelCreationException vmce) + { + throw new NavigationException(NavigationException.ErrorCreatingViewModel, segmentName, _pageAccessor.Page, vmce); + } + //catch(ViewCreationException viewCreationException) + //{ + // if(!string.IsNullOrEmpty(viewCreationException.InnerException?.Message) && viewCreationException.InnerException.Message.Contains("Maui")) + // throw new NavigationException(NavigationException.) + //} catch (Exception ex) { - if (ex is NavigationException) - throw; - - else if(ex is KeyNotFoundException) - throw new NavigationException(NavigationException.NoPageIsRegistered, segmentName, ex); - + var inner = ex.InnerException; + while(inner is not null) + { + if (inner.Message.Contains("thread with a dispatcher")) + throw new NavigationException(NavigationException.UnsupportedMauiCreation, segmentName, _pageAccessor.Page, ex); + inner = inner.InnerException; + } throw new NavigationException(NavigationException.ErrorCreatingPage, segmentName, ex); } } @@ -727,7 +745,7 @@ protected virtual Page CreatePageFromSegment(string segment) if (page is null) { var innerException = new NullReferenceException(string.Format("{0} could not be created. Please make sure you have registered {0} for navigation.", segmentName)); - throw new NavigationException(NavigationException.NoPageIsRegistered, _pageAccessor.Page, innerException); + throw new NavigationException(NavigationException.NoPageIsRegistered, segmentName, _pageAccessor.Page, innerException); } return page; @@ -937,71 +955,75 @@ await DoNavigateAction(onNavigatedFromTarget, segment, nextPage, parameters, asy await ProcessNavigation(currentPage.Navigation.NavigationStack.Last(), illegalSegments, parameters, true, animated); } - protected virtual Task DoPush(Page currentPage, Page page, bool? useModalNavigation, bool animated, bool insertBeforeLast = false, int navigationOffset = 0) + protected virtual async Task DoPush(Page currentPage, Page page, bool? useModalNavigation, bool animated, bool insertBeforeLast = false, int navigationOffset = 0) { if (page is null) throw new ArgumentNullException(nameof(page)); - // Prevent Page from using Parent's ViewModel - if (page.BindingContext is null) - page.BindingContext = new object(); - - if (currentPage is null) + try { - if (_application.Windows.OfType().Any(x => x.Name == PrismWindow.DefaultWindowName)) - _window = _application.Windows.OfType().First(x => x.Name == PrismWindow.DefaultWindowName); + // Prevent Page from using Parent's ViewModel + if (page.BindingContext is null) + page.BindingContext = new object(); - if (Window is null) + if (currentPage is null) { - _window = new PrismWindow + if (_application.Windows.OfType().Any(x => x.Name == PrismWindow.DefaultWindowName)) + _window = _application.Windows.OfType().First(x => x.Name == PrismWindow.DefaultWindowName); + + if (Window is null) + { + _window = new PrismWindow + { + Page = page + }; + ((List)_application.Windows).Add(_window as PrismWindow); + } + else { - Page = page - }; - ((List)_application.Windows).Add(_window as PrismWindow); - } - else - { #if !ANDROID - // BUG: https://github.com/dotnet/maui/issues/7275 - Window.Page = page; + // BUG: https://github.com/dotnet/maui/issues/7275 + Window.Page = page; #if WINDOWS - page.ForceLayout(); + page.ForceLayout(); #endif #else - - // HACK: This is the only way CURRENTLY to ensure that the UI resets for Absolute Navigation - var newWindow = new PrismWindow - { - Page = page - }; - _application.OpenWindow(newWindow); - _application.CloseWindow(Window); - _window = null; + // HACK: This is the only way CURRENTLY to ensure that the UI resets for Absolute Navigation + var newWindow = new PrismWindow + { + Page = page + }; + _application.OpenWindow(newWindow); + _application.CloseWindow(Window); + _window = null; #endif - } - - return Task.FromResult(null); - } - else - { - bool useModalForPush = UseModalNavigation(currentPage, useModalNavigation); - - if (useModalForPush) - { - return currentPage.Navigation.PushModalAsync(page, animated); + } } else { - if (insertBeforeLast) + bool useModalForPush = UseModalNavigation(currentPage, useModalNavigation); + + if (useModalForPush) { - return InsertPageBefore(currentPage, page, navigationOffset); + await currentPage.Navigation.PushModalAsync(page, animated); } else { - return currentPage.Navigation.PushAsync(page, animated); + if (insertBeforeLast) + { + await InsertPageBefore(currentPage, page, navigationOffset); + } + else + { + await currentPage.Navigation.PushAsync(page, animated); + } } } } + catch (Exception ex) + { + throw new NavigationException(NavigationException.UnsupportedMauiNavigation, _pageAccessor.Page, ex); + } } protected virtual Task InsertPageBefore(Page currentPage, Page page, int pageOffset) diff --git a/src/Prism.Maui/PrismAppBuilder.cs b/src/Prism.Maui/PrismAppBuilder.cs index 8a2e021..61f3f19 100644 --- a/src/Prism.Maui/PrismAppBuilder.cs +++ b/src/Prism.Maui/PrismAppBuilder.cs @@ -72,12 +72,19 @@ private void ConfigureViewModelLocator(IContainerProvider container) internal static object DefaultViewModelLocator(object view, Type viewModelType) { - if (view is not BindableObject bindable) - return null; + try + { + if (view is not BindableObject bindable) + return null; - var container = bindable.GetContainerProvider(); + var container = bindable.GetContainerProvider(); - return container.Resolve(viewModelType); + return container.Resolve(viewModelType); + } + catch (Exception ex) + { + throw new ViewModelCreationException(view, ex); + } } public PrismAppBuilder RegisterTypes(Action registerTypes) From c90133b9670187000334796cdb55d04e2f9add03 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sun, 19 Jun 2022 15:30:23 -0700 Subject: [PATCH 11/12] validate select tab --- sample/PrismMauiDemo/MauiProgram.cs | 30 +++++------ .../Navigation/Builder/NavigationBuilder.cs | 18 +++++++ .../Builder/NavigationBuilderExtensions.cs | 3 ++ .../Navigation/PageNavigationService.cs | 51 ++++++++----------- 4 files changed, 57 insertions(+), 45 deletions(-) diff --git a/sample/PrismMauiDemo/MauiProgram.cs b/sample/PrismMauiDemo/MauiProgram.cs index 342d3ba..60dc876 100644 --- a/sample/PrismMauiDemo/MauiProgram.cs +++ b/sample/PrismMauiDemo/MauiProgram.cs @@ -1,4 +1,4 @@ -using MauiModule; +using MauiModule; using MauiModule.ViewModels; using MauiRegionsModule; using PrismMauiDemo.ViewModels; @@ -38,21 +38,21 @@ public static MauiApp CreateMauiApp() if (status == "Failed" && !string.IsNullOrEmpty(x.Result?.Exception?.Message)) Console.Error.WriteLine(x.Result.Exception.Message); })) - .OnAppStart(nav => nav.CreateBuilder() - .AddTabbedSegment(page => - page.CreateTab("ViewC") - .CreateTab(t => - t.AddNavigationPage() - .AddSegment("ViewA", s => s.AddParameter("message", "Hello Tab - ViewA")) - .AddSegment("ViewB", s => s.AddParameter("message", "Hello Tab - ViewB"))) - //.CreateTab("ViewC", s => s.AddParameter("message", "Hello Tab - ViewC")) - .SelectedTab("NavigationPage|ViewB")) - .AddParameter("message_global", "This is a Global Message") - .Navigate()) + //.OnAppStart(nav => nav.CreateBuilder() + // .AddTabbedSegment(page => + // page.CreateTab("ViewC") + // .CreateTab(t => + // t.AddNavigationPage() + // .AddSegment("ViewA", s => s.AddParameter("message", "Hello Tab - ViewA")) + // .AddSegment("ViewB", s => s.AddParameter("message", "Hello Tab - ViewB"))) + // //.CreateTab("ViewC", s => s.AddParameter("message", "Hello Tab - ViewC")) + // .SelectedTab("NavigationPage|ViewB")) + // .AddParameter("message_global", "This is a Global Message") + // .Navigate()) //.OnAppStart("ViewA/ViewB/ViewC") - //.OnAppStart(navigationService => navigationService.CreateBuilder() - // .AddSegment() - // .Navigate(HandleNavigationError)) + .OnAppStart(navigationService => navigationService.CreateBuilder() + .AddSegment() + .Navigate(HandleNavigationError)) ) .ConfigureFonts(fonts => { diff --git a/src/Prism.Maui/Navigation/Builder/NavigationBuilder.cs b/src/Prism.Maui/Navigation/Builder/NavigationBuilder.cs index 1fd2d56..dc59c49 100644 --- a/src/Prism.Maui/Navigation/Builder/NavigationBuilder.cs +++ b/src/Prism.Maui/Navigation/Builder/NavigationBuilder.cs @@ -89,6 +89,24 @@ internal Uri BuildUri() var uri = (_absoluteNavigation ? "/" : string.Empty) + string.Join("/", _uriSegments.Select(x => x.Segment)); + if(uri.Contains("../")) + { + if (_absoluteNavigation) + throw new InvalidOperationException("The generated URI has one or more relative back operators and was marked as an absolute path. This is not supported."); + + var segments = uri.Split('/'); + if (!segments.Any(x => x != "..")) + throw new InvalidOperationException($"The constructed URI contains one or more relative back operators, but contains no other navigation segments - '{uri}'."); + var hasNonBackSegment = false; + for(int i = 0; i < segments.Length; i++) + { + if (hasNonBackSegment && segments[i] == "..") + throw new InvalidOperationException($"The constructed URI has a relative back operator after a new Navigation Segment which is not supported - '{uri}'."); + else if (segments[i] != "..") + hasNonBackSegment = true; + } + } + return UriParsingHelper.Parse(uri); } } diff --git a/src/Prism.Maui/Navigation/Builder/NavigationBuilderExtensions.cs b/src/Prism.Maui/Navigation/Builder/NavigationBuilderExtensions.cs index 03cea01..04addd2 100644 --- a/src/Prism.Maui/Navigation/Builder/NavigationBuilderExtensions.cs +++ b/src/Prism.Maui/Navigation/Builder/NavigationBuilderExtensions.cs @@ -25,6 +25,9 @@ internal static string GetNavigationKey(object builder) return registryAware.Registry.GetViewModelNavigationKey(vmType); } + public static INavigationBuilder RelativeBack(this INavigationBuilder builder) => + builder.AddSegment(".."); + /// /// This will force the generated Navigation URI to return an Absolute URI resetting the current 's property. /// diff --git a/src/Prism.Maui/Navigation/PageNavigationService.cs b/src/Prism.Maui/Navigation/PageNavigationService.cs index 3201ab2..4b3a8e3 100644 --- a/src/Prism.Maui/Navigation/PageNavigationService.cs +++ b/src/Prism.Maui/Navigation/PageNavigationService.cs @@ -831,53 +831,44 @@ private void TabbedPageSelectTab(TabbedPage tabbedPage, INavigationParameters pa if (segments.Count() == 1) TabbedPageSelectRootTab(tabbedPage, selectedTab); else if (segments.Count() > 1) - TabbedPageSelectNavigationChildTab(tabbedPage, segments.Last()); + TabbedPageSelectNavigationChildTab(tabbedPage, segments.First(), segments.Last()); } private void TabbedPageSelectRootTab(TabbedPage tabbedPage, string selectedTab) { - var child = tabbedPage.Children - .FirstOrDefault(x => selectedTab == (string)x.GetValue(ViewModelLocator.NavigationNameProperty)); - if (child is not null) - { - tabbedPage.CurrentPage = child; - return; - } - - var registration = Registry.Registrations - .LastOrDefault(x => x.Type == ViewType.Page && x.Name == selectedTab); - if (registration is null) - throw new InvalidOperationException($"No Tab has been registered with the name '{selectedTab}'"); + var registry = Registry; + var selectRegistration = registry.Registrations.FirstOrDefault(x => x.Name == selectedTab); + if (selectRegistration is null) + throw new KeyNotFoundException($"No Registration found to select tab '{selectedTab}'."); - child = tabbedPage.Children - .FirstOrDefault(x => x.GetType() == registration.View); + var child = tabbedPage.Children + .FirstOrDefault(x => IsPage(x, selectRegistration)); if (child is not null) { tabbedPage.CurrentPage = child; } } + private static bool IsPage(Page page, ViewRegistration registration) => + (string)page.GetValue(ViewModelLocator.NavigationNameProperty) == registration.Name || page.GetType() == registration.View; + private void TabbedPageSelectNavigationChildTab(TabbedPage tabbedPage, string rootTab, string selectedTab) { - var child = tabbedPage.Children - .FirstOrDefault(x => x is NavigationPage navPage && selectedTab == (string)navPage.CurrentPage.GetValue(ViewModelLocator.NavigationNameProperty)); - if (child is not null) - { - tabbedPage.CurrentPage = child; - return; - } + 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 registration = Registry.Registrations - .LastOrDefault(x => x.Type == ViewType.Page && x.Name == selectedTab); - if (registration is null) - throw new InvalidOperationException($"No Tab has been registered with the name '{selectedTab}'"); + var child = tabbedPage.Children + .FirstOrDefault(x => x is NavigationPage navPage && IsPage(x, rootRegistration) && (IsPage(navPage.RootPage, selectRegistration) || IsPage(navPage.CurrentPage, selectRegistration))); - child = tabbedPage.Children - .FirstOrDefault(x => x is NavigationPage navPage && navPage.CurrentPage.GetType() == registration.View); if (child is not null) - { tabbedPage.CurrentPage = child; - } } protected virtual async Task UseReverseNavigation(Page currentPage, string nextSegment, Queue segments, INavigationParameters parameters, bool? useModalNavigation, bool animated) From 4bb6a54031072a0881e688e59a68700ab8370c37 Mon Sep 17 00:00:00 2001 From: Dan Siegel Date: Sun, 19 Jun 2022 16:25:58 -0700 Subject: [PATCH 12/12] fix ambiguous namespace --- .../Prism.Maui.Tests/Fixtures/Navigation/ViewRegistryFixture.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Prism.Maui.Tests/Fixtures/Navigation/ViewRegistryFixture.cs b/tests/Prism.Maui.Tests/Fixtures/Navigation/ViewRegistryFixture.cs index fc3f394..3897504 100644 --- a/tests/Prism.Maui.Tests/Fixtures/Navigation/ViewRegistryFixture.cs +++ b/tests/Prism.Maui.Tests/Fixtures/Navigation/ViewRegistryFixture.cs @@ -8,6 +8,7 @@ using Prism.Maui.Tests.Mocks.ViewModels; using Prism.Maui.Tests.Mocks.Views; using Prism.Navigation.Xaml; +using TabbedPage = Microsoft.Maui.Controls.TabbedPage; namespace Prism.Maui.Tests.Fixtures.Navigation;