From: Shane Neuville Date: Wed, 24 Apr 2019 22:25:12 +0000 (-0600) Subject: fix infinite loops and remove public apis (#5954) X-Git-Tag: accepted/tizen/5.5/unified/20200421.150457~402^2~16 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=48606a5f1fad511e1fa839469fa3b1d4092dcc63;p=platform%2Fcore%2Fcsapi%2Fxsf.git fix infinite loops and remove public apis (#5954) * fix infinite loops and remove public apis * - remove comments, fix location to not have implicit * force uri to be relative on ios when no scheme specified * address PR comments * add error message * reformat absolute uris * additional suggestions * Update Xamarin.Forms.Core/Shell/ShellNavigationState.cs Co-Authored-By: PureWeen * Update Xamarin.Forms.Core/Shell/ShellUriHandler.cs Co-Authored-By: PureWeen * Update Xamarin.Forms.Core/Shell/ShellUriHandler.cs Co-Authored-By: PureWeen * Update Xamarin.Forms.Core/Shell/ShellUriHandler.cs Co-Authored-By: PureWeen --- diff --git a/Xamarin.Forms.Core.UnitTests/ShellTestBase.cs b/Xamarin.Forms.Core.UnitTests/ShellTestBase.cs index c8cb062..010cc62 100644 --- a/Xamarin.Forms.Core.UnitTests/ShellTestBase.cs +++ b/Xamarin.Forms.Core.UnitTests/ShellTestBase.cs @@ -24,7 +24,7 @@ namespace Xamarin.Forms.Core.UnitTests } - protected Uri CreateUri(string uri) => new Uri(uri, UriKind.RelativeOrAbsolute); + protected Uri CreateUri(string uri) => ShellUriHandler.CreateUri(uri); protected ShellSection MakeSimpleShellSection(string route, string contentRoute) { diff --git a/Xamarin.Forms.Core.UnitTests/ShellTests.cs b/Xamarin.Forms.Core.UnitTests/ShellTests.cs index 495647f..045a825 100644 --- a/Xamarin.Forms.Core.UnitTests/ShellTests.cs +++ b/Xamarin.Forms.Core.UnitTests/ShellTests.cs @@ -94,11 +94,11 @@ namespace Xamarin.Forms.Core.UnitTests shell.Items.Add(one); shell.Items.Add(two); - Assert.That(shell.CurrentState.Location.ToString(), Is.EqualTo("app:///s/one/tabone/content/")); + Assert.That(shell.CurrentState.Location.ToString(), Is.EqualTo("app:///s/one/tabone/content")); shell.GoToAsync(new ShellNavigationState("app:///s/two/tabfour/")); - Assert.That(shell.CurrentState.Location.ToString(), Is.EqualTo("app:///s/two/tabfour/content/")); + Assert.That(shell.CurrentState.Location.ToString(), Is.EqualTo("app:///s/two/tabfour/content")); } [Test] @@ -161,14 +161,14 @@ namespace Xamarin.Forms.Core.UnitTests await shell.GoToAsync("app:///s/two/tab21/"); - await shell.GoToAsync("/tab22"); - Assert.That(shell.CurrentState.Location.ToString(), Is.EqualTo("app:///s/two/tab22/content/")); + await shell.GoToAsync("/tab22", false, true); + Assert.That(shell.CurrentState.Location.ToString(), Is.EqualTo("app:///s/two/tab22/content")); - await shell.GoToAsync("tab21"); - Assert.That(shell.CurrentState.Location.ToString(), Is.EqualTo("app:///s/two/tab21/content/")); + await shell.GoToAsync("tab21", false, true); + Assert.That(shell.CurrentState.Location.ToString(), Is.EqualTo("app:///s/two/tab21/content")); - await shell.GoToAsync("/tab23"); - Assert.That(shell.CurrentState.Location.ToString(), Is.EqualTo("app:///s/two/tab23/content/")); + await shell.GoToAsync("/tab23", false, true); + Assert.That(shell.CurrentState.Location.ToString(), Is.EqualTo("app:///s/two/tab23/content")); /* * removing support for .. notation for now diff --git a/Xamarin.Forms.Core.UnitTests/ShellUriHandlerTests.cs b/Xamarin.Forms.Core.UnitTests/ShellUriHandlerTests.cs index c6fc643..3ad5999 100644 --- a/Xamarin.Forms.Core.UnitTests/ShellUriHandlerTests.cs +++ b/Xamarin.Forms.Core.UnitTests/ShellUriHandlerTests.cs @@ -17,13 +17,57 @@ namespace Xamarin.Forms.Core.UnitTests } [Test] - public async Task GlobalRegisterAbsoluteMatching() + public async Task RouteWithGlobalPageRoute() + { + + var shell = new Shell() { RouteScheme = "app", Route= "xaminals", RouteHost = "thehost" }; + var item1 = CreateShellItem(asImplicit: true, shellItemRoute: "animals", shellSectionRoute: "domestic", shellContentRoute: "dogs"); + var item2 = CreateShellItem(asImplicit: true, shellItemRoute: "animals", shellSectionRoute: "domestic", shellContentRoute: "cats"); + + shell.Items.Add(item1); + shell.Items.Add(item2); + + Routing.RegisterRoute("catdetails", typeof(ContentPage)); + await shell.GoToAsync("//cats/catdetails?name=3"); + + Assert.AreEqual("app://thehost/xaminals/animals/domestic/cats/catdetails", shell.CurrentState.Location.ToString()); + } + + [Test] + public async Task AbsoluteRoutingToPage() + { + + var shell = new Shell() { RouteScheme = "app", Route = "xaminals", RouteHost = "thehost" }; + var item1 = CreateShellItem(asImplicit: true, shellItemRoute: "animals", shellSectionRoute: "domestic", shellContentRoute: "dogs"); + shell.Items.Add(item1); + + Routing.RegisterRoute("catdetails", typeof(ContentPage)); + + Assert.That(async () => await shell.GoToAsync($"//catdetails"), Throws.Exception); + } + + + [Test] + public async Task LocationRemovesImplicit() + { + + var shell = new Shell() { RouteScheme = "app" }; + var item1 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent1"); + + shell.Items.Add(item1); + + Assert.AreEqual("app:///rootlevelcontent1", shell.CurrentState.Location.ToString()); + } + + + [Test] + public async Task GlobalRegisterAbsoluteMatching() { var shell = new Shell() { RouteScheme = "app", Route = "shellroute" }; Routing.RegisterRoute("/seg1/seg2/seg3", typeof(object)); - var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("app://seg1/seg2/seg3")); + var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("/seg1/seg2/seg3")); - Assert.AreEqual("/seg1/seg2/seg3", request.Request.ShortUri.ToString()); + Assert.AreEqual("app:///shellroute/seg1/seg2/seg3", request.Request.FullUri.ToString()); } [Test] @@ -54,7 +98,7 @@ namespace Xamarin.Forms.Core.UnitTests shell.Items.Add(item1); shell.Items.Add(item2); await shell.GoToAsync("//item1/section1/rootlevelcontent1"); - var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("section1/edit")); + var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("section1/edit"), true); Assert.AreEqual(1, request.Request.GlobalRoutes.Count); Assert.AreEqual("item1/section1/edit", request.Request.GlobalRoutes.First()); @@ -123,7 +167,7 @@ namespace Xamarin.Forms.Core.UnitTests shell.Items.Add(item1); await shell.GoToAsync("//rootlevelcontent1"); - var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("edit")); + var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("edit"), true); Assert.AreEqual("section1/edit", request.Request.GlobalRoutes.First()); } @@ -140,8 +184,8 @@ namespace Xamarin.Forms.Core.UnitTests shell.Items.Add(item1); await shell.GoToAsync("//rootlevelcontent1"); - var location = shell.CurrentState.Location; - await shell.GoToAsync("edit"); + var location = shell.CurrentState.FullLocation; + await shell.GoToAsync("edit", false, true); Assert.AreEqual(editShellContent, shell.CurrentItem.CurrentItem.CurrentItem); } @@ -213,6 +257,50 @@ namespace Xamarin.Forms.Core.UnitTests [Test] + public async Task AbsoluteNavigationToRelativeWithGlobal() + { + var shell = new Shell() { RouteScheme = "app", RouteHost = "xamarin.com", Route = "xaminals" }; + + var item1 = CreateShellItem(asImplicit: true, shellContentRoute: "dogs"); + var item2 = CreateShellItem(asImplicit: true, shellSectionRoute: "domestic", shellContentRoute: "cats", shellItemRoute: "animals"); + + shell.Items.Add(item1); + shell.Items.Add(item2); + + Routing.RegisterRoute("catdetails", typeof(ContentPage)); + await shell.GoToAsync($"app://xamarin.com/xaminals/animals/domestic/cats/catdetails?name=domestic"); + + Assert.AreEqual( + "app://xamarin.com/xaminals/animals/domestic/cats/catdetails", + shell.CurrentState.FullLocation.ToString() + ); + } + + [Test] + public async Task RelativeNavigationWithRoute() + { + var shell = new Shell() { RouteScheme = "app", RouteHost = "xamarin.com", Route = "xaminals" }; + + var item1 = CreateShellItem(asImplicit: true, shellContentRoute: "dogs"); + var item2 = CreateShellItem(asImplicit: true, shellSectionRoute: "domestic", shellContentRoute: "cats", shellItemRoute: "animals"); + + shell.Items.Add(item1); + shell.Items.Add(item2); + + Routing.RegisterRoute("catdetails", typeof(ContentPage)); + Assert.That(async () => await shell.GoToAsync($"cats/catdetails?name=domestic"), Throws.Exception); + + // once relative routing with a stack is fixed then we can remove the above exception check and add below back in + // await shell.GoToAsync($"cats/catdetails?name=domestic") + //Assert.AreEqual( + // "app://xamarin.com/xaminals/animals/domestic/cats/catdetails", + // shell.CurrentState.Location.ToString() + // ); + + } + + + [Test] public async Task ConvertToStandardFormat() { var shell = new Shell() { RouteScheme = "app", Route = "shellroute", RouteHost = "host" }; @@ -237,7 +325,8 @@ namespace Xamarin.Forms.Core.UnitTests CreateUri("app://shellroute/path"), CreateUri("app:/shellroute/path"), CreateUri("app://host/shellroute/path"), - CreateUri("app:/host/shellroute/path") + CreateUri("app:/host/shellroute/path"), + CreateUri("app:/host/shellroute\\path") }; diff --git a/Xamarin.Forms.Core/Routing.cs b/Xamarin.Forms.Core/Routing.cs index 6a54744..be1efd7 100644 --- a/Xamarin.Forms.Core/Routing.cs +++ b/Xamarin.Forms.Core/Routing.cs @@ -10,6 +10,7 @@ namespace Xamarin.Forms static Dictionary s_routes = new Dictionary(); internal const string ImplicitPrefix = "IMPL_"; + const string _pathSeparator = "/"; internal static string GenerateImplicitRoute(string source) { @@ -42,7 +43,7 @@ namespace Xamarin.Forms return bindable.GetType().Name + ++s_routeCount; } - public static string[] GetRouteKeys() + internal static string[] GetRouteKeys() { string[] keys = new string[s_routes.Count]; s_routes.Keys.CopyTo(keys, 0); @@ -84,9 +85,26 @@ namespace Xamarin.Forms return $"{source}/"; } + internal static Uri RemoveImplicit(Uri uri) + { + uri = ShellUriHandler.FormatUri(uri); + + if (!uri.IsAbsoluteUri) + return uri; + + string[] parts = uri.OriginalString.TrimEnd(_pathSeparator[0]).Split(_pathSeparator[0]); + + List toKeep = new List(); + for (int i = 0; i < parts.Length; i++) + if (!IsImplicit(parts[i])) + toKeep.Add(parts[i]); + + return new Uri(string.Join(_pathSeparator, toKeep)); + } + public static string FormatRoute(List segments) { - var route = FormatRoute(String.Join("/", segments)); + var route = FormatRoute(String.Join(_pathSeparator, segments)); return route; } @@ -128,7 +146,7 @@ namespace Xamarin.Forms static void ValidateRoute(string route) { if (string.IsNullOrWhiteSpace(route)) - throw new ArgumentNullException("Route cannot be an empty string"); + throw new ArgumentNullException(nameof(route), "Route cannot be an empty string"); var uri = new Uri(route, UriKind.RelativeOrAbsolute); diff --git a/Xamarin.Forms.Core/Shell/IShellItemController.cs b/Xamarin.Forms.Core/Shell/IShellItemController.cs index 0ca8839..f394943 100644 --- a/Xamarin.Forms.Core/Shell/IShellItemController.cs +++ b/Xamarin.Forms.Core/Shell/IShellItemController.cs @@ -6,8 +6,6 @@ namespace Xamarin.Forms { public interface IShellItemController : IElementController { - Task GoToPart(NavigationRequest navigationRequest, Dictionary queryData); - bool ProposeSection(ShellSection shellSection, bool setValue = true); } } \ No newline at end of file diff --git a/Xamarin.Forms.Core/Shell/IShellSectionController.cs b/Xamarin.Forms.Core/Shell/IShellSectionController.cs index 6e51040..de745eb 100644 --- a/Xamarin.Forms.Core/Shell/IShellSectionController.cs +++ b/Xamarin.Forms.Core/Shell/IShellSectionController.cs @@ -15,8 +15,6 @@ namespace Xamarin.Forms void AddDisplayedPageObserver(object observer, Action callback); - Task GoToPart(NavigationRequest request, Dictionary queryData); - bool RemoveContentInsetObserver(IShellContentInsetObserver observer); bool RemoveDisplayedPageObserver(object observer); diff --git a/Xamarin.Forms.Core/Shell/Shell.cs b/Xamarin.Forms.Core/Shell/Shell.cs index bae08e6..0945642 100644 --- a/Xamarin.Forms.Core/Shell/Shell.cs +++ b/Xamarin.Forms.Core/Shell/Shell.cs @@ -363,12 +363,10 @@ namespace Xamarin.Forms var content = section.Items[k]; string longUri = $"{RouteScheme}://{RouteHost}/{Routing.GetRoute(this)}/{Routing.GetRoute(item)}/{Routing.GetRoute(section)}/{Routing.GetRoute(content)}"; - string shortUri = $"{RouteScheme}://{RouteHost}/{Routing.GetRoutePathIfNotImplicit(this)}{Routing.GetRoutePathIfNotImplicit(item)}{Routing.GetRoutePathIfNotImplicit(section)}{Routing.GetRoutePathIfNotImplicit(content)}"; longUri = longUri.TrimEnd('/'); - shortUri = shortUri.TrimEnd('/'); - routes.Add(new RequestDefinition(longUri, shortUri, item, section, content, new List())); + routes.Add(new RequestDefinition(longUri, item, section, content, new List())); } } } @@ -376,7 +374,12 @@ namespace Xamarin.Forms return routes; } - public async Task GoToAsync(ShellNavigationState state, bool animate = true) + public Task GoToAsync(ShellNavigationState state, bool animate = true) + { + return GoToAsync(state, animate, false); + } + + internal async Task GoToAsync(ShellNavigationState state, bool animate, bool enableRelativeShellRoutes) { // FIXME: This should not be none, we need to compute the delta and set flags correctly var accept = ProposeNavigation(ShellNavigationSource.Unknown, state, true); @@ -385,7 +388,7 @@ namespace Xamarin.Forms _accumulateNavigatedEvents = true; - var navigationRequest = ShellUriHandler.GetNavigationRequest(this, state.Location); + var navigationRequest = ShellUriHandler.GetNavigationRequest(this, state.FullLocation, enableRelativeShellRoutes); var uri = navigationRequest.Request.FullUri; var queryString = navigationRequest.Query; var queryData = ParseQueryString(queryString); @@ -419,7 +422,7 @@ namespace Xamarin.Forms parts.RemoveAt(0); if (parts.Count > 0) - await ((IShellItemController)shellItem).GoToPart(navigationRequest, queryData); + await shellItem.GoToPart(navigationRequest, queryData); } else { @@ -502,29 +505,20 @@ namespace Xamarin.Forms if (shellItem != null) { var shellItemRoute = shellItem.Route; - //if (!shellItemRoute.StartsWith(Routing.ImplicitPrefix, StringComparison.Ordinal)) - { - stateBuilder.Append(shellItemRoute); - stateBuilder.Append("/"); - } + stateBuilder.Append(shellItemRoute); + stateBuilder.Append("/"); if (shellSection != null) { var shellSectionRoute = shellSection.Route; - //if (!shellSectionRoute.StartsWith(Routing.ImplicitPrefix, StringComparison.Ordinal)) - { - stateBuilder.Append(shellSectionRoute); - stateBuilder.Append("/"); - } + stateBuilder.Append(shellSectionRoute); + stateBuilder.Append("/"); if (shellContent != null) { var shellContentRoute = shellContent.Route; - //if (!shellContentRoute.StartsWith(Routing.ImplicitPrefix, StringComparison.Ordinal)) - { - stateBuilder.Append(shellContentRoute); - stateBuilder.Append("/"); - } + stateBuilder.Append(shellContentRoute); + stateBuilder.Append("/"); } if (!stackAtRoot) diff --git a/Xamarin.Forms.Core/Shell/ShellItem.cs b/Xamarin.Forms.Core/Shell/ShellItem.cs index a4cff83..7163c8f 100644 --- a/Xamarin.Forms.Core/Shell/ShellItem.cs +++ b/Xamarin.Forms.Core/Shell/ShellItem.cs @@ -27,7 +27,7 @@ namespace Xamarin.Forms #region IShellItemController - Task IShellItemController.GoToPart(NavigationRequest request, Dictionary queryData) + internal Task GoToPart(NavigationRequest request, Dictionary queryData) { var shellSection = request.Request.Section; @@ -39,7 +39,7 @@ namespace Xamarin.Forms if (CurrentItem != shellSection) SetValueFromRenderer(CurrentItemProperty, shellSection); - return ((IShellSectionController)shellSection).GoToPart(request, queryData); + return shellSection.GoToPart(request, queryData); } bool IShellItemController.ProposeSection(ShellSection shellSection, bool setValue) diff --git a/Xamarin.Forms.Core/Shell/ShellNavigationState.cs b/Xamarin.Forms.Core/Shell/ShellNavigationState.cs index 93e80b2..0e8c64f 100644 --- a/Xamarin.Forms.Core/Shell/ShellNavigationState.cs +++ b/Xamarin.Forms.Core/Shell/ShellNavigationState.cs @@ -7,12 +7,33 @@ namespace Xamarin.Forms [DebuggerDisplay("Location = {Location}")] public class ShellNavigationState { - public Uri Location { get; set; } + Uri _fullLocation; + + internal Uri FullLocation + { + get => _fullLocation; + set + { + _fullLocation = value; + Location = Routing.RemoveImplicit(value); + } + } + + public Uri Location + { + get; + private set; + } public ShellNavigationState() { } - public ShellNavigationState(string location) => Location = new Uri(location, UriKind.RelativeOrAbsolute); - public ShellNavigationState(Uri location) => Location = location; + public ShellNavigationState(string location) + { + FullLocation = ShellUriHandler.CreateUri(location); + + } + + public ShellNavigationState(Uri location) => FullLocation = location; public static implicit operator ShellNavigationState(Uri uri) => new ShellNavigationState(uri); public static implicit operator ShellNavigationState(string value) => new ShellNavigationState(value); } -} \ No newline at end of file +} diff --git a/Xamarin.Forms.Core/Shell/ShellSection.cs b/Xamarin.Forms.Core/Shell/ShellSection.cs index 6d46577..41a5b8f 100644 --- a/Xamarin.Forms.Core/Shell/ShellSection.cs +++ b/Xamarin.Forms.Core/Shell/ShellSection.cs @@ -68,7 +68,7 @@ namespace Xamarin.Forms callback(DisplayedPage); } - Task IShellSectionController.GoToPart(NavigationRequest request, Dictionary queryData) + internal Task GoToPart(NavigationRequest request, Dictionary queryData) { ShellContent shellContent = request.Request.Content; diff --git a/Xamarin.Forms.Core/Shell/ShellUriHandler.cs b/Xamarin.Forms.Core/Shell/ShellUriHandler.cs index d9be121..285a389 100644 --- a/Xamarin.Forms.Core/Shell/ShellUriHandler.cs +++ b/Xamarin.Forms.Core/Shell/ShellUriHandler.cs @@ -9,19 +9,36 @@ namespace Xamarin.Forms internal class ShellUriHandler { - static readonly char[] _pathSeparator = { '/', '\\' }; + static readonly char[] _pathSeparators = { '/', '\\' }; + const string _pathSeparator = "/"; - static Uri FormatUri(Uri path) + internal static Uri FormatUri(Uri path) { if (path.IsAbsoluteUri) - return path; + return new Uri(FormatUri(path.OriginalString), UriKind.Absolute); return new Uri(FormatUri(path.OriginalString), UriKind.Relative); } - static string FormatUri(string path) + internal static string FormatUri(string path) { - return path.Replace("\\", "/"); + return path.Replace(_pathSeparators[1], _pathSeparator[0]); + } + + internal static Uri CreateUri(string path) + { + path = FormatUri(path); + + // on iOS if the uri starts with // it'll instantiate as absolute with + // file: as the default scheme where as android just crashes + // so this checks if it starts with / and just forces relative + if (path.StartsWith(_pathSeparator, StringComparison.Ordinal)) + return new Uri(path, UriKind.Relative); + + if (Uri.TryCreate(path, UriKind.Absolute, out Uri result)) + return result; + + return new Uri(path, UriKind.Relative); } public static Uri ConvertToStandardFormat(Shell shell, Uri request) @@ -33,7 +50,7 @@ namespace Xamarin.Forms else pathAndQuery = request.OriginalString; - var segments = new List(pathAndQuery.Split(_pathSeparator, StringSplitOptions.RemoveEmptyEntries)); + var segments = new List(pathAndQuery.Split(_pathSeparators, StringSplitOptions.RemoveEmptyEntries)); if (segments[0] != shell.RouteHost) @@ -42,27 +59,27 @@ namespace Xamarin.Forms if (segments[1] != shell.Route) segments.Insert(1, shell.Route); - var path = String.Join("/", segments.ToArray()); + var path = String.Join(_pathSeparator, segments.ToArray()); string uri = $"{shell.RouteScheme}://{path}"; return new Uri(uri); } - public static NavigationRequest GetNavigationRequest(Shell shell, Uri uri) + internal static NavigationRequest GetNavigationRequest(Shell shell, Uri uri, bool enableRelativeShellRoutes = false) { uri = FormatUri(uri); // figure out the intent of the Uri NavigationRequest.WhatToDoWithTheStack whatDoIDo = NavigationRequest.WhatToDoWithTheStack.PushToIt; if (uri.IsAbsoluteUri) whatDoIDo = NavigationRequest.WhatToDoWithTheStack.ReplaceIt; - else if (uri.OriginalString.StartsWith("//") || uri.OriginalString.StartsWith("\\\\")) + else if (uri.OriginalString.StartsWith("//", StringComparison.Ordinal) || uri.OriginalString.StartsWith("\\\\", StringComparison.Ordinal)) whatDoIDo = NavigationRequest.WhatToDoWithTheStack.ReplaceIt; else whatDoIDo = NavigationRequest.WhatToDoWithTheStack.PushToIt; Uri request = ConvertToStandardFormat(shell, uri); - var possibleRouteMatches = GenerateRoutePaths(shell, request, uri); + var possibleRouteMatches = GenerateRoutePaths(shell, request, uri, enableRelativeShellRoutes); if (possibleRouteMatches.Count == 0) @@ -85,8 +102,7 @@ namespace Xamarin.Forms var theWinningRoute = possibleRouteMatches[0]; RequestDefinition definition = new RequestDefinition( - ConvertToStandardFormat(shell, new Uri(theWinningRoute.PathFull, UriKind.RelativeOrAbsolute)), - new Uri(theWinningRoute.PathNoImplicit, UriKind.RelativeOrAbsolute), + ConvertToStandardFormat(shell, CreateUri(theWinningRoute.PathFull)), theWinningRoute.Item, theWinningRoute.Section, theWinningRoute.Content, @@ -100,20 +116,25 @@ namespace Xamarin.Forms internal static List GenerateRoutePaths(Shell shell, Uri request) { request = FormatUri(request); - return GenerateRoutePaths(shell, request, request); + return GenerateRoutePaths(shell, request, request, false); } - internal static List GenerateRoutePaths(Shell shell, Uri request, Uri originalRequest) + internal static List GenerateRoutePaths(Shell shell, Uri request, Uri originalRequest, bool enableRelativeShellRoutes) { - request = FormatUri(request); - originalRequest = FormatUri(originalRequest); - 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]); } + request = FormatUri(request); + originalRequest = FormatUri(originalRequest); + List possibleRoutePaths = new List(); if (!request.IsAbsoluteUri) request = ConvertToStandardFormat(shell, request); @@ -121,22 +142,23 @@ namespace Xamarin.Forms string localPath = request.LocalPath; bool relativeMatch = false; - if (!originalRequest.IsAbsoluteUri && !originalRequest.OriginalString.StartsWith("/") && !originalRequest.OriginalString.StartsWith("\\")) + if (!originalRequest.IsAbsoluteUri && + !originalRequest.OriginalString.StartsWith("//", StringComparison.Ordinal)) relativeMatch = true; - var segments = localPath.Split(_pathSeparator, StringSplitOptions.RemoveEmptyEntries); + var segments = localPath.Split(_pathSeparator.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); if (!relativeMatch) { for (int i = 0; i < routeKeys.Length; i++) { var route = routeKeys[i]; - var uri = ConvertToStandardFormat(shell, new Uri(route, UriKind.RelativeOrAbsolute)); - // Todo is this supported? + var uri = ConvertToStandardFormat(shell, CreateUri(route)); if (uri.Equals(request)) { - var builder = new RouteRequestBuilder(route, route, null, segments); - return new List { builder }; + 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 }; } } } @@ -160,24 +182,42 @@ namespace Xamarin.Forms while (currentLocation.Shell != null) { - List pureRoutesMatch = new List(); - List pureGlobalRoutesMatch = new List(); + var pureRoutesMatch = new List(); + var pureGlobalRoutesMatch = new List(); - SearchPath(currentLocation.LowestChild, null, segments, pureRoutesMatch, 0); - SearchPath(currentLocation.LowestChild, null, segments, pureGlobalRoutesMatch, 0, ignoreGlobalRoutes: false); - pureRoutesMatch = GetBestMatches(pureRoutesMatch); - pureGlobalRoutesMatch = GetBestMatches(pureGlobalRoutesMatch); + //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; + } + } - if (pureRoutesMatch.Count > 0) - return pureRoutesMatch; + SearchPath(currentLocation.LowestChild, null, segments, pureGlobalRoutesMatch, 0, ignoreGlobalRoutes: false); + ExpandOutGlobalRoutes(pureGlobalRoutesMatch, routeKeys); + pureGlobalRoutesMatch = GetBestMatches(pureGlobalRoutesMatch); if (pureGlobalRoutesMatch.Count > 0) + { + // 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 suddent starts creating routes + if (!enableRelativeShellRoutes && pureGlobalRoutesMatch[0].SegmentsMatched.Count > 0) + { + throw new Exception($"Relative routing to shell elements is currently not supported. Try prefixing your uri with ///: ///{originalRequest}"); + } + return pureGlobalRoutesMatch; + } currentLocation.Pop(); } - string searchPath = String.Join("/", segments); + string searchPath = String.Join(_pathSeparator, segments); if (routeKeys.Contains(searchPath)) { @@ -209,45 +249,50 @@ namespace Xamarin.Forms return bestMatches; bestMatches.Clear(); - foreach (var possibleRoutePath in possibleRoutePaths) + ExpandOutGlobalRoutes(possibleRoutePaths, routeKeys); + } + + possibleRoutePaths = GetBestMatches(possibleRoutePaths); + return possibleRoutePaths; + } + + internal static void ExpandOutGlobalRoutes(List possibleRoutePaths, string[] routeKeys) + { + foreach (var possibleRoutePath in possibleRoutePaths) + { + while (routeKeys.Contains(possibleRoutePath.NextSegment) || routeKeys.Contains(possibleRoutePath.RemainingPath)) { - while (routeKeys.Contains(possibleRoutePath.NextSegment) || routeKeys.Contains(possibleRoutePath.RemainingPath)) + if (routeKeys.Contains(possibleRoutePath.NextSegment)) + possibleRoutePath.AddGlobalRoute(possibleRoutePath.NextSegment, possibleRoutePath.NextSegment); + else + possibleRoutePath.AddGlobalRoute(possibleRoutePath.RemainingPath, possibleRoutePath.RemainingPath); + } + + while (!possibleRoutePath.IsFullMatch) + { + NodeLocation nodeLocation = new NodeLocation(); + nodeLocation.SetNode(possibleRoutePath.LowestChild); + List pureGlobalRoutesMatch = new List(); + while (nodeLocation.Shell != null && pureGlobalRoutesMatch.Count == 0) { - if(routeKeys.Contains(possibleRoutePath.NextSegment)) - possibleRoutePath.AddGlobalRoute(possibleRoutePath.NextSegment, possibleRoutePath.NextSegment); - else - possibleRoutePath.AddGlobalRoute(possibleRoutePath.RemainingPath, possibleRoutePath.RemainingPath); + SearchPath(nodeLocation.LowestChild, null, possibleRoutePath.RemainingSegments, pureGlobalRoutesMatch, 0, ignoreGlobalRoutes: false); + nodeLocation.Pop(); } - while (!possibleRoutePath.IsFullMatch) + // nothing found or too many things found + if (pureGlobalRoutesMatch.Count != 1 || pureGlobalRoutesMatch[0].GlobalRouteMatches.Count == 0) { - NodeLocation nodeLocation = new NodeLocation(); - nodeLocation.SetNode(possibleRoutePath.LowestChild); - List pureGlobalRoutesMatch = new List(); - while (nodeLocation.Shell != null && pureGlobalRoutesMatch.Count == 0) - { - SearchPath(nodeLocation.LowestChild, null, possibleRoutePath.RemainingSegments, pureGlobalRoutesMatch, 0, ignoreGlobalRoutes: false); - nodeLocation.Pop(); - } - - // nothing found or too many things found - if (pureGlobalRoutesMatch.Count != 1) - { - break; - } + break; + } - for (var i = 0; i < pureGlobalRoutesMatch[0].GlobalRouteMatches.Count; i++) - { - var match = pureGlobalRoutesMatch[0]; - possibleRoutePath.AddGlobalRoute(match.GlobalRouteMatches[i], match.SegmentsMatched[i]); - } + for (var i = 0; i < pureGlobalRoutesMatch[0].GlobalRouteMatches.Count; i++) + { + var match = pureGlobalRoutesMatch[0]; + possibleRoutePath.AddGlobalRoute(match.GlobalRouteMatches[i], match.SegmentsMatched[i]); } } } - - possibleRoutePaths = GetBestMatches(possibleRoutePaths); - return possibleRoutePaths; } internal static List GetBestMatches(List possibleRoutePaths) @@ -341,7 +386,7 @@ namespace Xamarin.Forms if (Content != null && !Routing.IsImplicit(Content)) paths.Add(Content.Route); - string uri = String.Join("/", paths); + string uri = String.Join(_pathSeparator, paths); return new Uri($"{Shell.RouteScheme}://{uri}"); } @@ -485,10 +530,10 @@ namespace Xamarin.Forms for (var i = 0; i < keys.Length; i++) { var key = FormatUri(keys[i]); - if (key.StartsWith("/") && !(node is Shell)) + if (key.StartsWith(_pathSeparator, StringComparison.Ordinal) && !(node is Shell)) continue; - var segments = key.Split(_pathSeparator, StringSplitOptions.RemoveEmptyEntries); + var segments = key.Split(_pathSeparator.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); if (segments[0] == route) { @@ -511,7 +556,7 @@ namespace Xamarin.Forms { get { - var segments = _path.Split(_pathSeparator, StringSplitOptions.RemoveEmptyEntries).ToList().Skip(1).ToList(); + var segments = _path.Split(_pathSeparator.ToCharArray(), StringSplitOptions.RemoveEmptyEntries).ToList().Skip(1).ToList(); if (segments.Count == 0) return new object[0]; @@ -526,7 +571,7 @@ namespace Xamarin.Forms { get { - var segments = _path.Split(_pathSeparator, StringSplitOptions.RemoveEmptyEntries); + var segments = _path.Split(_pathSeparator.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); if (segments.Length == 0) return string.Empty; @@ -539,7 +584,7 @@ namespace Xamarin.Forms { get { - var segments = _path.Split(_pathSeparator, StringSplitOptions.RemoveEmptyEntries).ToList().Skip(1).ToList(); + var segments = _path.Split(_pathSeparator.ToCharArray(), StringSplitOptions.RemoveEmptyEntries).ToList().Skip(1).ToList(); if (segments.Count == 0) return true; @@ -669,7 +714,7 @@ namespace Xamarin.Forms if (nextMatch >= _allSegments.Length) return null; - return Routing.FormatRoute(String.Join("/", _allSegments.Skip(nextMatch))); + return Routing.FormatRoute(String.Join(_uriSeparator, _allSegments.Skip(nextMatch))); } } public string[] RemainingSegments @@ -686,7 +731,7 @@ namespace Xamarin.Forms string MakeUriString(List segments) { - if (segments[0].StartsWith("/") || segments[0].StartsWith("\\")) + if (segments[0].StartsWith(_uriSeparator, StringComparison.Ordinal) || segments[0].StartsWith("\\", StringComparison.Ordinal)) return String.Join(_uriSeparator, segments); return $"//{String.Join(_uriSeparator, segments)}"; @@ -704,7 +749,7 @@ namespace Xamarin.Forms [DebuggerDisplay("RequestDefinition = {Request}, StackRequest = {StackRequest}")] - public class NavigationRequest + internal class NavigationRequest { public enum WhatToDoWithTheStack { @@ -728,25 +773,23 @@ namespace Xamarin.Forms [DebuggerDisplay("Full = {FullUri}, Short = {ShortUri}")] - public class RequestDefinition + internal class RequestDefinition { - public RequestDefinition(Uri fullUri, Uri shortUri, ShellItem item, ShellSection section, ShellContent content, List globalRoutes) + public RequestDefinition(Uri fullUri, ShellItem item, ShellSection section, ShellContent content, List globalRoutes) { FullUri = fullUri; - ShortUri = shortUri; Item = item; Section = section; Content = content; GlobalRoutes = globalRoutes; } - public RequestDefinition(string fullUri, string shortUri, ShellItem item, ShellSection section, ShellContent content, List globalRoutes) : - this(new Uri(fullUri, UriKind.Absolute), new Uri(shortUri, UriKind.Absolute), item, section, content, globalRoutes) + public RequestDefinition(string fullUri, ShellItem item, ShellSection section, ShellContent content, List globalRoutes) : + this(new Uri(fullUri, UriKind.Absolute), item, section, content, globalRoutes) { } public Uri FullUri { get; } - public Uri ShortUri { get; } public ShellItem Item { get; } public ShellSection Section { get; } public ShellContent Content { get; }