From cc0caa6f50467ad552bcb841ce8d9b047f832c4d Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 9 Feb 2021 12:12:43 -0600 Subject: [PATCH] Fix additional scenarios with nested defined routes (#13648) * Fix additional scenarios with nested defined routes * - pick better starting point for uri matching * - fix merge * - formating changes and more accurate test fix --- .../ShellNavigatingTests.cs | 65 +++- Xamarin.Forms.Core/Shell/ShellUriHandler.cs | 309 ++++++++++++------ 2 files changed, 264 insertions(+), 110 deletions(-) diff --git a/Xamarin.Forms.Core.UnitTests/ShellNavigatingTests.cs b/Xamarin.Forms.Core.UnitTests/ShellNavigatingTests.cs index 6368a54bd25..dcfff1c3ea4 100644 --- a/Xamarin.Forms.Core.UnitTests/ShellNavigatingTests.cs +++ b/Xamarin.Forms.Core.UnitTests/ShellNavigatingTests.cs @@ -916,7 +916,6 @@ public async Task GoBackFromRouteWithMultiplePaths() await shell.Navigation.PopAsync(); } - [Test] public async Task GoBackFromRouteWithMultiplePathsHierarchical() { @@ -933,6 +932,70 @@ public async Task GoBackFromRouteWithMultiplePathsHierarchical() await shell.Navigation.PopAsync(); } + [Test] + public async Task HierarchicalNavigation() + { + Routing.RegisterRoute("page1/page2", typeof(ShellTestPage)); + var shell = new TestShell( + CreateShellItem(shellSectionRoute: "page1") + ); + + await shell.GoToAsync($"page1/page2?{nameof(ShellTestPage.SomeQueryParameter)}=1"); + + Assert.AreEqual("1", ((ShellTestPage)shell.CurrentPage).SomeQueryParameter); + } + + [Test] + public async Task HierarchicalNavigationMultipleRoutes() + { + Routing.RegisterRoute("page1/page2", typeof(ShellTestPage)); + Routing.RegisterRoute("page1/page2/page3", typeof(TestPage1)); + var shell = new TestShell( + CreateShellItem(shellSectionRoute: "page1") + ); + + await shell.GoToAsync($"page1/page2?{nameof(ShellTestPage.SomeQueryParameter)}=1"); + + Assert.AreEqual("1", ((ShellTestPage)shell.CurrentPage).SomeQueryParameter); + await shell.GoToAsync($"page1/page2/page3"); + + Assert.IsTrue(shell.CurrentPage is TestPage1); + Assert.IsTrue(shell.Navigation.NavigationStack[1] is ShellTestPage); + } + + [Test] + public async Task HierarchicalNavigationMultipleRoutesVariation1() + { + Routing.RegisterRoute("page1/page2", typeof(ShellTestPage)); + Routing.RegisterRoute("page1/page2/page3", typeof(TestPage1)); + var shell = new TestShell( + CreateShellItem(shellSectionRoute: "page1") + ); + + await shell.GoToAsync($"page1/page2/page3"); + + Assert.IsTrue(shell.CurrentPage is TestPage1); + Assert.IsTrue(shell.Navigation.NavigationStack[1] is ShellTestPage); + } + + [Test] + public async Task HierarchicalNavigationWithBackNavigation() + { + Routing.RegisterRoute("page1/page2", typeof(ShellTestPage)); + Routing.RegisterRoute("page1/page2/page3", typeof(TestPage1)); + var shell = new TestShell( + CreateShellItem(shellSectionRoute: "page1") + ); + + await shell.GoToAsync($"page1/page2"); + await shell.GoToAsync($"page1/page2/page3"); + Assert.IsTrue(shell.CurrentPage is TestPage1); + await shell.GoToAsync($".."); + Assert.IsTrue(shell.CurrentPage is ShellTestPage); + await shell.GoToAsync($".."); + Assert.IsTrue(shell.CurrentPage is ContentPage); + } + public class NavigationMonitoringTab : Tab { public List NavigationsFired = new List(); diff --git a/Xamarin.Forms.Core/Shell/ShellUriHandler.cs b/Xamarin.Forms.Core/Shell/ShellUriHandler.cs index bc84a03eec6..92b527421c6 100644 --- a/Xamarin.Forms.Core/Shell/ShellUriHandler.cs +++ b/Xamarin.Forms.Core/Shell/ShellUriHandler.cs @@ -54,9 +54,18 @@ internal static Uri FormatUri(Uri path, Shell shell) restOfPath = buildUpPages; } + string[] shellRoutes = new[] + { + shell.CurrentItem.Route, + shell.CurrentItem.CurrentItem.Route, + shell.CurrentItem.CurrentItem.CurrentItem.Route, + }; + + restOfPath = CollapsePath(restOfPath.ToArray(), shellRoutes, true); restOfPath.Insert(0, shell.CurrentItem.CurrentItem.CurrentItem.Route); restOfPath.Insert(0, shell.CurrentItem.CurrentItem.Route); restOfPath.Insert(0, shell.CurrentItem.Route); + var result = $"{String.Join(_pathSeparator, restOfPath)}{queryString}"; var returnValue = ConvertToStandardFormat("scheme", "host", null, new Uri(result, UriKind.Relative)); return new Uri(FormatUri(returnValue.PathAndQuery), UriKind.Relative); @@ -221,11 +230,6 @@ internal static List GenerateRoutePaths(Shell shell, Uri re var routeKeys = Routing.GetRouteKeys(); for (int i = 0; i < routeKeys.Length; i++) { - if (routeKeys[i] == originalRequest.OriginalString) - { - var builder = new RouteRequestBuilder(routeKeys[i], routeKeys[i], null, new string[] { routeKeys[i] }); - return new List { builder }; - } routeKeys[i] = FormatUri(routeKeys[i]); } @@ -245,21 +249,6 @@ internal static List GenerateRoutePaths(Shell shell, Uri re var segments = RetrievePaths(localPath); - if (!relativeMatch) - { - for (int i = 0; i < routeKeys.Length; i++) - { - var route = routeKeys[i]; - var uri = ConvertToStandardFormat(shell, CreateUri(route)); - if (uri.Equals(request)) - { - throw new Exception($"Global routes currently cannot be the only page on the stack, so absolute routing to global routes is not supported. For now, just navigate to: {originalRequest.OriginalString.Replace("//", "")}"); - //var builder = new RouteRequestBuilder(route, route, null, segments); - //return new List { builder }; - } - } - } - var depthStart = 0; if (segments[0] == shell?.Route) @@ -274,124 +263,207 @@ internal static List GenerateRoutePaths(Shell shell, Uri re if (relativeMatch && shell?.CurrentItem != null) { - // retrieve current location - var currentLocation = NodeLocation.Create(shell); + var result = ProcessRelativeRoute(shell, routeKeys, segments, enableRelativeShellRoutes, originalRequest); + if (result.Count > 0) + return result; + } - while (currentLocation.Shell != null) - { - var pureRoutesMatch = new List(); - var pureGlobalRoutesMatch = new List(); + possibleRoutePaths.Clear(); + SearchPath(shell, null, segments, possibleRoutePaths, depthStart); - //currently relative routes to shell routes isn't supported as we aren't creating navigation stacks - if (enableRelativeShellRoutes) - { - SearchPath(currentLocation.LowestChild, null, segments, pureRoutesMatch, 0); - ExpandOutGlobalRoutes(pureRoutesMatch, routeKeys); - pureRoutesMatch = GetBestMatches(pureRoutesMatch); - if (pureRoutesMatch.Count > 0) - { - return pureRoutesMatch; - } - } + var bestMatches = GetBestMatches(possibleRoutePaths); + if (bestMatches.Count > 0) + return bestMatches; + bestMatches.Clear(); + ExpandOutGlobalRoutes(possibleRoutePaths, routeKeys); - SearchPath(currentLocation.LowestChild, null, segments, pureGlobalRoutesMatch, 0, ignoreGlobalRoutes: false); - ExpandOutGlobalRoutes(pureGlobalRoutesMatch, routeKeys); + foreach (var possibleRoutePath in possibleRoutePaths) + { + if (possibleRoutePath.IsFullMatch) + continue; + var url = possibleRoutePath.PathFull; - if (currentLocation.Content != null && pureGlobalRoutesMatch.Count == 0) - { - string newPath = $"{shell.CurrentState.Location.OriginalString}{_pathSeparator}{String.Join(_pathSeparator, segments)}"; - var currentSegments = RetrievePaths(shell.CurrentState.FullLocation.OriginalString); - var newSegments = RetrievePaths(newPath); + var globalRouteMatches = + SearchForGlobalRoutes( + possibleRoutePath.RemainingSegments, + new ShellNavigationState(url, false).FullLocation, + possibleRoutePath.GetNodeLocation(), + routeKeys); - RouteRequestBuilder routeRequestBuilder = new RouteRequestBuilder(newSegments); - RouteRequestBuilder requestBuilderWithNewSegments = new RouteRequestBuilder(segments); + if (globalRouteMatches.Count != 1) + continue; - // add shell element routes - routeRequestBuilder.AddMatch(currentLocation); + var globalRouteMatch = globalRouteMatches[0]; - // add routes that are contributed by global routes - for (var i = 0; i < currentSegments.Length; i++) - { - var currentSeg = currentSegments[i]; - if (routeRequestBuilder.FullSegments.Count <= i || currentSeg != routeRequestBuilder.FullSegments[i]) - { - routeRequestBuilder.AddGlobalRoute(currentSeg, currentSeg); - } - } + while (possibleRoutePath.NextSegment != null) + { + var matchIndex = globalRouteMatch.SegmentsMatched.IndexOf(possibleRoutePath.NextSegment); + if (matchIndex < 0) + break; - var existingGlobalRoutes = routeRequestBuilder.GlobalRouteMatches.ToList(); - ExpandOutGlobalRoutes(new List { routeRequestBuilder }, routeKeys); - if (routeRequestBuilder.IsFullMatch) - { - var additionalRouteMatches = routeRequestBuilder.GlobalRouteMatches; - for (int i = existingGlobalRoutes.Count; i < additionalRouteMatches.Count; i++) - requestBuilderWithNewSegments.AddGlobalRoute(additionalRouteMatches[i], segments[i - existingGlobalRoutes.Count]); + possibleRoutePath.AddGlobalRoute( + globalRouteMatch.GlobalRouteMatches[matchIndex], + globalRouteMatch.SegmentsMatched[matchIndex]); + } + } - pureGlobalRoutesMatch.Add(requestBuilderWithNewSegments); - } - } + possibleRoutePaths = GetBestMatches(possibleRoutePaths); - pureGlobalRoutesMatch = GetBestMatches(pureGlobalRoutesMatch); - if (pureGlobalRoutesMatch.Count > 0) + if (possibleRoutePaths.Count == 0) + { + foreach (var routeKey in routeKeys) + { + if (routeKey == originalRequest.OriginalString) { - // currently relative routes to shell routes isn't supported as we aren't creating navigation stacks - // So right now we will just throw an exception so that once this is implemented - // GotoAsync doesn't start acting inconsistently and all of a sudden starts creating routes - - int shellElementsMatched = - pureGlobalRoutesMatch[0].SegmentsMatched.Count - - pureGlobalRoutesMatch[0].GlobalRouteMatches.Count; + var builder = new RouteRequestBuilder(routeKey, routeKey, null, new string[] { routeKey }); + return new List { builder }; + } + } - if (!enableRelativeShellRoutes && shellElementsMatched > 0) + if (!relativeMatch) + { + for (int i = 0; i < routeKeys.Length; i++) + { + var route = routeKeys[i]; + var uri = ConvertToStandardFormat(shell, CreateUri(route)); + if (uri.Equals(request)) { - throw new Exception($"Relative routing to shell elements is currently not supported. Try prefixing your uri with ///: ///{originalRequest}"); + throw new Exception($"Global routes currently cannot be the only page on the stack, so absolute routing to global routes is not supported. For now, just navigate to: {originalRequest.OriginalString.Replace("//", "")}"); } - - return pureGlobalRoutesMatch; } + } + } + return possibleRoutePaths; + } - currentLocation.Pop(); + static List ProcessRelativeRoute( + Shell shell, + string[] routeKeys, + string[] segments, + bool enableRelativeShellRoutes, + Uri originalRequest) + { + // retrieve current location + var currentLocation = NodeLocation.Create(shell); + + while (currentLocation.Shell != null) + { + var pureRoutesMatch = new List(); + var pureGlobalRoutesMatch = new List(); + + //currently relative routes to shell routes isn't supported as we aren't creating navigation stacks + if (enableRelativeShellRoutes) + { + SearchPath(currentLocation.LowestChild, null, segments, pureRoutesMatch, 0); + ExpandOutGlobalRoutes(pureRoutesMatch, routeKeys); + pureRoutesMatch = GetBestMatches(pureRoutesMatch); + if (pureRoutesMatch.Count > 0) + { + return pureRoutesMatch; + } } - string searchPath = String.Join(_pathSeparator, segments); + SearchPath(currentLocation.LowestChild, null, segments, pureGlobalRoutesMatch, 0, ignoreGlobalRoutes: false); + ExpandOutGlobalRoutes(pureGlobalRoutesMatch, routeKeys); - if (routeKeys.Contains(searchPath)) + if (currentLocation.Content != null && pureGlobalRoutesMatch.Count == 0) { - return new List { new RouteRequestBuilder(searchPath, searchPath, null, segments) }; + var matches = SearchForGlobalRoutes(segments, shell.CurrentState.FullLocation, currentLocation, routeKeys); + pureGlobalRoutesMatch.AddRange(matches); } - RouteRequestBuilder builder = null; - foreach (var segment in segments) + pureGlobalRoutesMatch = GetBestMatches(pureGlobalRoutesMatch); + if (pureGlobalRoutesMatch.Count > 0) { - if (routeKeys.Contains(segment)) + // currently relative routes to shell routes isn't supported as we aren't creating navigation stacks + // So right now we will just throw an exception so that once this is implemented + // GotoAsync doesn't start acting inconsistently and all of a sudden starts creating routes + + int shellElementsMatched = + pureGlobalRoutesMatch[0].SegmentsMatched.Count - + pureGlobalRoutesMatch[0].GlobalRouteMatches.Count; + + if (!enableRelativeShellRoutes && shellElementsMatched > 0) { - if (builder == null) - builder = new RouteRequestBuilder(segment, segment, null, segments); - else - builder.AddGlobalRoute(segment, segment); + throw new Exception($"Relative routing to shell elements is currently not supported. Try prefixing your uri with ///: ///{originalRequest}"); } + + return pureGlobalRoutesMatch; } - if (builder != null && builder.IsFullMatch) - return new List { builder }; + currentLocation.Pop(); } - else + + string searchPath = String.Join(_pathSeparator, segments); + + if (routeKeys.Contains(searchPath)) + { + return new List { new RouteRequestBuilder(searchPath, searchPath, null, segments) }; + } + + RouteRequestBuilder builder = null; + foreach (var segment in segments) { - possibleRoutePaths.Clear(); - SearchPath(shell, null, segments, possibleRoutePaths, depthStart); + if (routeKeys.Contains(segment)) + { + if (builder == null) + builder = new RouteRequestBuilder(segment, segment, null, segments); + else + builder.AddGlobalRoute(segment, segment); + } + } - var bestMatches = GetBestMatches(possibleRoutePaths); - if (bestMatches.Count > 0) - return bestMatches; + if (builder != null && builder.IsFullMatch) + return new List { builder }; - bestMatches.Clear(); - ExpandOutGlobalRoutes(possibleRoutePaths, routeKeys); + return new List(); + } + + static List SearchForGlobalRoutes( + string[] segments, + Uri startingFrom, + NodeLocation currentLocation, + string[] routeKeys) + { + List pureGlobalRoutesMatch = new List(); + string newPath = String.Join(_pathSeparator, segments); + var currentSegments = RetrievePaths(startingFrom.OriginalString); + var newSegments = CollapsePath(newPath, currentSegments, true).ToArray(); + List fullRouteWithNewSegments = new List(currentSegments); + fullRouteWithNewSegments.AddRange(newSegments); + + // This is used to calculate if the global route matches + RouteRequestBuilder routeRequestBuilder = new RouteRequestBuilder(fullRouteWithNewSegments.ToArray()); + + // add shell element routes + routeRequestBuilder.AddMatch(currentLocation); + + // add routes that are contributed by global routes + for (var i = 0; i < currentSegments.Length; i++) + { + var currentSeg = currentSegments[i]; + if (routeRequestBuilder.FullSegments.Count <= i || currentSeg != routeRequestBuilder.FullSegments[i]) + { + routeRequestBuilder.AddGlobalRoute(currentSeg, currentSeg); + } } - possibleRoutePaths = GetBestMatches(possibleRoutePaths); - return possibleRoutePaths; + var existingGlobalRoutes = routeRequestBuilder.GlobalRouteMatches.ToList(); + ExpandOutGlobalRoutes(new List { routeRequestBuilder }, routeKeys); + if (routeRequestBuilder.IsFullMatch) + { + RouteRequestBuilder requestBuilderWithNewSegments = new RouteRequestBuilder(newSegments); + + var additionalRouteMatches = routeRequestBuilder.GlobalRouteMatches; + for (int i = existingGlobalRoutes.Count; i < additionalRouteMatches.Count; i++) + requestBuilderWithNewSegments.AddGlobalRoute(additionalRouteMatches[i], segments[i - existingGlobalRoutes.Count]); + + pureGlobalRoutesMatch.Add(requestBuilderWithNewSegments); + } + + return pureGlobalRoutesMatch; } // The purpose of this method is to give an accurate representation of what a target URI means based @@ -404,26 +476,38 @@ internal static List GenerateRoutePaths(Shell shell, Uri re internal static List CollapsePath( string myRoute, IEnumerable currentRouteStack, - bool userDefinedRoute) + bool userDefinedRoute) => + CollapsePath(RetrievePaths(myRoute), currentRouteStack, userDefinedRoute); + + internal static List CollapsePath( + string[] myRoute, + IEnumerable currentRouteStack, + bool removeUserDefinedRoute) { var localRouteStack = currentRouteStack.ToList(); for (var i = localRouteStack.Count - 1; i >= 0; i--) { var route = localRouteStack[i]; if (Routing.IsImplicit(route) || - (Routing.IsDefault(route) && userDefinedRoute)) + (Routing.IsDefault(route) && removeUserDefinedRoute)) { localRouteStack.RemoveAt(i); } } - var paths = RetrievePaths(myRoute).ToList(); + var paths = myRoute.ToList(); // collapse similar leaves - int walkBackCurrentStackIndex = localRouteStack.Count - (paths.Count - 1); + int walkBackCurrentStackIndex = -1; + + if (paths.Count > 0) + walkBackCurrentStackIndex = localRouteStack.IndexOf(paths[0]); while (paths.Count > 1 && walkBackCurrentStackIndex >= 0) { + if (localRouteStack.Count <= walkBackCurrentStackIndex) + break; + if (paths[0] == localRouteStack[walkBackCurrentStackIndex]) { paths.RemoveAt(0); @@ -444,10 +528,17 @@ static bool FindAndAddSegmentMatch(RouteRequestBuilder possibleRoutePath, string // First search by collapsing global routes if user is registering routes like "route1/route2/route3" foreach (var routeKey in routeKeys) { - var collapsedRoute = String.Join(_pathSeparator, CollapsePath(routeKey, possibleRoutePath.SegmentsMatched, true)); + var collapsedRoutes = CollapsePath(routeKey, possibleRoutePath.SegmentsMatched, true); + var collapsedRoute = String.Join(_pathSeparator, collapsedRoutes); if (routeKey.StartsWith("//")) - collapsedRoute = "//" + collapsedRoute; + { + var routeKeyPaths = + routeKey.Split(_pathSeparators, StringSplitOptions.RemoveEmptyEntries); + + if (routeKeyPaths[0] == collapsedRoutes[0]) + collapsedRoute = "//" + collapsedRoute; + } string collapsedMatch = possibleRoutePath.GetNextSegmentMatch(collapsedRoute); if (!String.IsNullOrWhiteSpace(collapsedMatch))