[Shell] refactor of processing uris (#5852)
authorShane Neuville <shane94@hotmail.com>
Thu, 11 Apr 2019 04:54:50 +0000 (22:54 -0600)
committerSamantha Houts <samhouts@users.noreply.github.com>
Thu, 11 Apr 2019 04:54:50 +0000 (21:54 -0700)
fixes #5790

17 files changed:
Xamarin.Forms.Core.UnitTests/ShellTestBase.cs [new file with mode: 0644]
Xamarin.Forms.Core.UnitTests/ShellTests.cs
Xamarin.Forms.Core.UnitTests/ShellUriHandlerTests.cs [new file with mode: 0644]
Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj
Xamarin.Forms.Core/Routing.cs
Xamarin.Forms.Core/Shell/BaseShellItem.cs
Xamarin.Forms.Core/Shell/IShellController.cs
Xamarin.Forms.Core/Shell/IShellItemController.cs
Xamarin.Forms.Core/Shell/IShellSectionController.cs
Xamarin.Forms.Core/Shell/Shell.cs
Xamarin.Forms.Core/Shell/ShellItem.cs
Xamarin.Forms.Core/Shell/ShellNavigationState.cs
Xamarin.Forms.Core/Shell/ShellSection.cs
Xamarin.Forms.Core/Shell/ShellUriHandler.cs [new file with mode: 0644]
Xamarin.Forms.Sandbox.Android/Properties/AndroidManifest.xml
Xamarin.Forms.Sandbox.Android/Xamarin.Forms.Sandbox.Android.csproj
Xamarin.Forms.Sandbox/MainPage.xaml

diff --git a/Xamarin.Forms.Core.UnitTests/ShellTestBase.cs b/Xamarin.Forms.Core.UnitTests/ShellTestBase.cs
new file mode 100644 (file)
index 0000000..c8cb062
--- /dev/null
@@ -0,0 +1,115 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using Xamarin.Forms.Internals;
+
+namespace Xamarin.Forms.Core.UnitTests
+{
+       [TestFixture]
+       public class ShellTestBase : BaseTestFixture
+       {
+               [SetUp]
+               public override void Setup()
+               {
+                       Device.SetFlags(new[] { Shell.ShellExperimental });
+                       base.Setup();
+
+               }
+
+               [TearDown]
+               public override void TearDown()
+               {
+                       base.TearDown();
+
+               }
+
+               protected Uri CreateUri(string uri) => new Uri(uri, UriKind.RelativeOrAbsolute);
+
+               protected ShellSection MakeSimpleShellSection(string route, string contentRoute)
+               {
+                       return MakeSimpleShellSection(route, contentRoute, new ShellTestPage());
+               }
+
+               protected ShellSection MakeSimpleShellSection(string route, string contentRoute, ContentPage contentPage)
+               {
+                       var shellSection = new ShellSection();
+                       shellSection.Route = route;
+                       var shellContent = new ShellContent { Content = contentPage, Route = contentRoute };
+                       shellSection.Items.Add(shellContent);
+                       return shellSection;
+               }
+
+               [QueryProperty("SomeQueryParameter", "SomeQueryParameter")]
+               public class ShellTestPage : ContentPage
+               {
+                       public string SomeQueryParameter { get; set; }
+               }
+
+               protected ShellItem CreateShellItem(TemplatedPage page = null, bool asImplicit = false, string shellContentRoute = null, string shellSectionRoute = null, string shellItemRoute = null)
+               {
+                       page = page ?? new ContentPage();
+                       ShellItem item = null;
+                       var section = CreateShellSection(page, asImplicit, shellContentRoute, shellSectionRoute);
+
+                       if (!String.IsNullOrWhiteSpace(shellItemRoute))
+                       {
+                               item = new ShellItem();
+                               item.Route = shellItemRoute;
+                               item.Items.Add(section);
+                       }
+                       else if (asImplicit)
+                               item = ShellItem.CreateFromShellSection(section);
+                       else
+                       {
+                               item = new ShellItem();
+                               item.Items.Add(section);
+                       }
+
+                       return item;
+               }
+
+               protected ShellSection CreateShellSection(TemplatedPage page = null, bool asImplicit = false, string shellContentRoute = null, string shellSectionRoute = null)
+               {
+                       var content = CreateShellContent(page, asImplicit, shellContentRoute);
+
+                       ShellSection section = null;
+
+                       if (!String.IsNullOrWhiteSpace(shellSectionRoute))
+                       {
+                               section = new ShellSection();
+                               section.Route = shellSectionRoute;
+                               section.Items.Add(content);
+                       }
+                       else if (asImplicit)
+                               section = ShellSection.CreateFromShellContent(content);
+                       else
+                       {
+                               section = new ShellSection();
+                               section.Items.Add(content);
+                       }
+
+                       return section;
+               }
+
+               protected ShellContent CreateShellContent(TemplatedPage page = null, bool asImplicit = false, string shellContentRoute = null)
+               {
+                       page = page ?? new ContentPage();
+                       ShellContent content = null;
+
+                       if(!String.IsNullOrWhiteSpace(shellContentRoute))
+                       {
+                               content = new ShellContent() { Content = page };
+                               content.Route = shellContentRoute;
+                       }
+                       else if (asImplicit)
+                               content = (ShellContent)page;
+                       else
+                               content = new ShellContent() { Content = page };
+
+
+                       return content;
+               }
+
+       }
+}
index e4ab03c..84cfcb1 100644 (file)
@@ -1,4 +1,5 @@
 using System;
+using System.Linq;
 using System.Threading.Tasks;
 using NUnit.Framework;
 using Xamarin.Forms.Internals;
@@ -6,15 +7,8 @@ using Xamarin.Forms.Internals;
 namespace Xamarin.Forms.Core.UnitTests
 {
        [TestFixture]
-       public class ShellTests : BaseTestFixture
+       public class ShellTests : ShellTestBase
        {
-               [SetUp]
-               public override void Setup()
-               {
-                       Device.SetFlags(new[] { Shell.ShellExperimental });
-                       base.Setup();
-
-               }
 
                [Test]
                public void DefaultState()
@@ -77,26 +71,6 @@ namespace Xamarin.Forms.Core.UnitTests
                        Assert.AreEqual(shellItem, shell.CurrentItem);
                }
 
-               ShellSection MakeSimpleShellSection(string route, string contentRoute)
-               {
-                       return MakeSimpleShellSection(route, contentRoute, new ShellTestPage());
-               }
-
-               ShellSection MakeSimpleShellSection (string route, string contentRoute, ContentPage contentPage)
-               {
-                       var shellSection = new ShellSection();
-                       shellSection.Route = route;
-                       var shellContent = new ShellContent { Content = contentPage, Route = contentRoute };
-                       shellSection.Items.Add(shellContent);
-                       return shellSection;
-               }
-
-               [QueryProperty("SomeQueryParameter", "SomeQueryParameter")]
-               public class ShellTestPage : ContentPage
-               {
-                       public string SomeQueryParameter { get; set; }
-               }
-
                [Test]
                public void SimpleGoTo()
                {
@@ -128,6 +102,37 @@ namespace Xamarin.Forms.Core.UnitTests
                }
 
                [Test]
+               public async Task CaseIgnoreRouting()
+               {
+                       var routes = new[] { "Tab1", "TAB2", "@-_-@", "+:~", "=%", "Super_Simple+-Route.doc", "1/2", @"1\2/3", "app://tab" };
+
+                       foreach (var route in routes)
+                       {
+                               var formattedRoute = Routing.FormatRoute(route);
+                               Routing.RegisterRoute(formattedRoute, typeof(ShellItem));
+
+                               var content1 = Routing.GetOrCreateContent(formattedRoute);
+                               Assert.IsNotNull(content1);
+                               Assert.AreEqual(Routing.GetRoute(content1), formattedRoute);
+                       }
+
+                       Assert.Catch(typeof(ArgumentException), () => Routing.RegisterRoute("app://IMPL_tab21", typeof(ShellItem)));
+
+                       Assert.Catch(typeof(ArgumentException), () => Routing.RegisterRoute(@"app:\\IMPL_tab21", typeof(ShellItem)));
+
+                       Assert.Catch(typeof(ArgumentException), () => Routing.RegisterRoute(string.Empty, typeof(ShellItem)));
+
+                       Assert.Catch(typeof(ArgumentNullException), () => Routing.RegisterRoute(null, typeof(ShellItem)));
+
+                       Assert.Catch(typeof(ArgumentException), () => Routing.RegisterRoute("tab1/IMPL_tab11", typeof(ShellItem)));
+
+                       Assert.Catch(typeof(ArgumentException), () => Routing.RegisterRoute("IMPL_shell", typeof(ShellItem)));
+
+                       Assert.Catch(typeof(ArgumentException), () => Routing.RegisterRoute("app://tab2/IMPL_tab21", typeof(ShellItem)));
+               }
+
+
+               [Test]
                public async Task RelativeGoTo()
                {
                        var shell = new Shell
@@ -165,6 +170,8 @@ namespace Xamarin.Forms.Core.UnitTests
                        await shell.GoToAsync("/tab23");
                        Assert.That(shell.CurrentState.Location.ToString(), Is.EqualTo("app:///s/two/tab23/content/"));
 
+                       /*
+                        * removing support for .. notation for now
                        await shell.GoToAsync("../one/tab11");
                        Assert.That(shell.CurrentState.Location.ToString(), Is.EqualTo("app:///s/one/tab11/content/"));
 
@@ -180,6 +187,7 @@ namespace Xamarin.Forms.Core.UnitTests
 
                        await shell.GoToAsync(new ShellNavigationState($"../one/tab11#fragment"));
                        Assert.That(shell.CurrentState.Location.ToString(), Is.EqualTo("app:///s/one/tab11/content/"));
+                       */
                }
 
                [Test]
@@ -313,5 +321,54 @@ namespace Xamarin.Forms.Core.UnitTests
 
                        Assert.AreEqual(((IShellController)shell).FlyoutHeader, label);
                }
+
+               [Test]
+               public async Task FlyoutNavigateToImplicitContentPage()
+               {
+                       var shell = new Shell();
+                       var shellITem = new ShellItem() { FlyoutDisplayOptions = FlyoutDisplayOptions.AsMultipleItems,  };
+                       var shellSection = new ShellSection() { Title = "can navigate to" };
+                       shellSection.Items.Add(new ContentPage());
+
+                       var shellSection2 = new ShellSection() { Title = "can navigate to" };
+                       shellSection2.Items.Add(new ContentPage());
+
+                       var implicitSection = CreateShellSection(new ContentPage(), asImplicit: true);
+
+                       shellITem.Items.Add(shellSection);
+                       shellITem.Items.Add(shellSection2);
+                       shellITem.Items.Add(implicitSection);
+
+                       shell.Items.Add(shellITem);
+                       IShellController shellController = (IShellController)shell;
+
+                       await shellController.OnFlyoutItemSelectedAsync(shellSection2);
+                       Assert.AreEqual(shellSection2, shell.CurrentItem.CurrentItem);
+
+                       await shellController.OnFlyoutItemSelectedAsync(shellSection);
+                       Assert.AreEqual(shellSection, shell.CurrentItem.CurrentItem);
+
+                       await shellController.OnFlyoutItemSelectedAsync(implicitSection);
+                       Assert.AreEqual(implicitSection, shell.CurrentItem.CurrentItem);
+
+               }
+
+
+               [Test]
+               public async Task UriNavigationTests()
+               {
+                       var shell = new Shell();
+                       var item1 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent1");
+                       var item2 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent2");
+
+                       shell.Items.Add(item1);
+                       shell.Items.Add(item2);
+
+                       shell.GoToAsync("//rootlevelcontent2");
+                       Assert.AreEqual(shell.CurrentItem, item2);
+
+                       shell.GoToAsync("//rootlevelcontent1");
+                       Assert.AreEqual(shell.CurrentItem, item1);
+               }
        }
 }
diff --git a/Xamarin.Forms.Core.UnitTests/ShellUriHandlerTests.cs b/Xamarin.Forms.Core.UnitTests/ShellUriHandlerTests.cs
new file mode 100644 (file)
index 0000000..c6fc643
--- /dev/null
@@ -0,0 +1,257 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using Xamarin.Forms.Internals;
+
+namespace Xamarin.Forms.Core.UnitTests
+{
+       [TestFixture]
+       public class ShellUriHandlerTests : ShellTestBase
+       {
+               [TearDown]
+               public override void TearDown()
+               {
+                       base.TearDown();
+                       Routing.Clear();
+               }
+
+               [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"));
+
+                       Assert.AreEqual("/seg1/seg2/seg3", request.Request.ShortUri.ToString());
+               }
+
+               [Test]
+               public async Task ShellContentOnlyWithGlobalEdit()
+               {
+                       var shell = new Shell();
+                       var item1 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent1");
+                       var item2 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent2");
+
+                       shell.Items.Add(item1);
+                       shell.Items.Add(item2);
+
+                       Routing.RegisterRoute("//rootlevelcontent1/edit", typeof(ContentPage));
+                       await shell.GoToAsync("//rootlevelcontent1/edit");
+               }
+
+               [Test]
+               public async Task ShellRelativeGlobalRegistration()
+               {
+                       var shell = new Shell();
+                       var item1 = CreateShellItem(asImplicit: true, shellItemRoute: "item1", shellContentRoute: "rootlevelcontent1", shellSectionRoute: "section1");
+                       var item2 = CreateShellItem(asImplicit: true, shellItemRoute: "item2", shellContentRoute: "rootlevelcontent1", shellSectionRoute: "section1");
+
+                       Routing.RegisterRoute("section0/edit", typeof(ContentPage));
+                       Routing.RegisterRoute("item1/section1/edit", typeof(ContentPage));
+                       Routing.RegisterRoute("item2/section1/edit", typeof(ContentPage));
+                       Routing.RegisterRoute("//edit", typeof(ContentPage));
+                       shell.Items.Add(item1);
+                       shell.Items.Add(item2);
+                       await shell.GoToAsync("//item1/section1/rootlevelcontent1");
+                       var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("section1/edit"));
+
+                       Assert.AreEqual(1, request.Request.GlobalRoutes.Count);
+                       Assert.AreEqual("item1/section1/edit", request.Request.GlobalRoutes.First());
+               }
+
+               [Test]
+               public async Task ShellSectionWithRelativeEditUpOneLevelMultiple()
+               {
+                       var shell = new Shell();
+                       var item1 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent1", shellSectionRoute: "section1");
+
+                       Routing.RegisterRoute("section1/edit", typeof(ContentPage));
+                       Routing.RegisterRoute("section1/add", typeof(ContentPage));
+
+                       shell.Items.Add(item1);
+
+                       var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("//rootlevelcontent1/add/edit"));
+
+                       Assert.AreEqual(2, request.Request.GlobalRoutes.Count);
+                       Assert.AreEqual("section1/add", request.Request.GlobalRoutes.First());
+                       Assert.AreEqual("section1/edit", request.Request.GlobalRoutes.Skip(1).First());
+               }
+
+               [Test]
+               public async Task ShellSectionWithGlobalRouteAbsolute()
+               {
+                       var shell = new Shell();
+                       var item1 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent1", shellSectionRoute: "section1");
+
+                       Routing.RegisterRoute("edit", typeof(ContentPage));
+
+                       shell.Items.Add(item1);
+
+                       var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("//rootlevelcontent1/edit"));
+
+                       Assert.AreEqual(1, request.Request.GlobalRoutes.Count);                 
+                       Assert.AreEqual("edit", request.Request.GlobalRoutes.First());
+               }
+
+               [Test]
+               public async Task ShellSectionWithGlobalRouteRelative()
+               {
+                       var shell = new Shell();
+                       var item1 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent1", shellSectionRoute: "section1");
+
+                       Routing.RegisterRoute("edit", typeof(ContentPage));
+
+                       shell.Items.Add(item1);
+
+                       await shell.GoToAsync("//rootlevelcontent1");
+                       var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("edit"));
+
+                       Assert.AreEqual(1, request.Request.GlobalRoutes.Count);
+                       Assert.AreEqual("edit", request.Request.GlobalRoutes.First());
+               }
+
+
+               [Test]
+               public async Task ShellSectionWithRelativeEditUpOneLevel()
+               {
+                       var shell = new Shell();
+                       var item1 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent1", shellSectionRoute: "section1");
+
+                       Routing.RegisterRoute("section1/edit", typeof(ContentPage));
+
+                       shell.Items.Add(item1);
+
+                       await shell.GoToAsync("//rootlevelcontent1");
+                       var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("edit"));
+
+                       Assert.AreEqual("section1/edit", request.Request.GlobalRoutes.First());
+               }
+
+               [Test]
+               public async Task ShellSectionWithRelativeEdit()
+               {
+                       var shell = new Shell();
+                       var item1 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent1", shellSectionRoute:"section1");
+                       var editShellContent = CreateShellContent(shellContentRoute: "edit");
+
+
+                       item1.Items[0].Items.Add(editShellContent);
+                       shell.Items.Add(item1);
+
+                       await shell.GoToAsync("//rootlevelcontent1");
+                       var location = shell.CurrentState.Location;
+                       await shell.GoToAsync("edit");
+
+                       Assert.AreEqual(editShellContent, shell.CurrentItem.CurrentItem.CurrentItem);
+               }
+
+
+               [Test]
+               public async Task ShellContentOnly()
+               {
+                       var shell = new Shell();
+                       var item1 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent1");
+                       var item2 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent2");
+
+                       shell.Items.Add(item1);
+                       shell.Items.Add(item2);
+
+
+                       var builders = ShellUriHandler.GenerateRoutePaths(shell, CreateUri("//rootlevelcontent1"));
+
+                       Assert.AreEqual(1, builders.Count);
+                       Assert.AreEqual("//rootlevelcontent1", builders.First().PathNoImplicit);
+
+                       builders = ShellUriHandler.GenerateRoutePaths(shell, CreateUri("//rootlevelcontent2"));
+                       Assert.AreEqual(1, builders.Count);
+                       Assert.AreEqual("//rootlevelcontent2", builders.First().PathNoImplicit);
+               }
+
+
+               [Test]
+               public async Task ShellSectionAndContentOnly()
+               {
+                       var shell = new Shell();
+                       var item1 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent", shellSectionRoute:"section1");
+                       var item2 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent", shellSectionRoute: "section2");
+
+                       shell.Items.Add(item1);
+                       shell.Items.Add(item2);
+
+
+                       var builders = ShellUriHandler.GenerateRoutePaths(shell, CreateUri("//section1/rootlevelcontent")).Select(x=> x.PathNoImplicit).ToArray();
+
+                       Assert.AreEqual(1, builders.Length);
+                       Assert.IsTrue(builders.Contains("//section1/rootlevelcontent"));
+
+                       builders = ShellUriHandler.GenerateRoutePaths(shell, CreateUri("//section2/rootlevelcontent")).Select(x => x.PathNoImplicit).ToArray();
+                       Assert.AreEqual(1, builders.Length);
+                       Assert.IsTrue(builders.Contains("//section2/rootlevelcontent"));
+               }
+
+               [Test]
+               public async Task ShellItemAndContentOnly()
+               {
+                       var shell = new Shell();
+                       var item1 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent", shellItemRoute: "item1");
+                       var item2 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent", shellItemRoute: "item2");
+
+                       shell.Items.Add(item1);
+                       shell.Items.Add(item2);
+
+
+                       var builders = ShellUriHandler.GenerateRoutePaths(shell, CreateUri("//item1/rootlevelcontent")).Select(x => x.PathNoImplicit).ToArray();
+
+                       Assert.AreEqual(1, builders.Length);
+                       Assert.IsTrue(builders.Contains("//item1/rootlevelcontent"));
+
+                       builders = ShellUriHandler.GenerateRoutePaths(shell, CreateUri("//item2/rootlevelcontent")).Select(x => x.PathNoImplicit).ToArray();
+                       Assert.AreEqual(1, builders.Length);
+                       Assert.IsTrue(builders.Contains("//item2/rootlevelcontent"));
+               }
+
+
+               [Test]
+               public async Task ConvertToStandardFormat()
+               {
+                       var shell = new Shell() { RouteScheme = "app", Route = "shellroute", RouteHost = "host" };
+
+                       Uri[] TestUris = new Uri[] {
+                               CreateUri("path"),
+                               CreateUri("//path"),
+                               CreateUri("/path"),
+                               CreateUri("host/path"),
+                               CreateUri("//host/path"),
+                               CreateUri("/host/path"),
+                               CreateUri("shellroute/path"),
+                               CreateUri("//shellroute/path"),
+                               CreateUri("/shellroute/path"),
+                               CreateUri("host/shellroute/path"),
+                               CreateUri("//host/shellroute/path"),
+                               CreateUri("/host/shellroute/path"),
+                               CreateUri("app://path"),
+                               CreateUri("app:/path"),
+                               CreateUri("app://host/path"),
+                               CreateUri("app:/host/path"),
+                               CreateUri("app://shellroute/path"),
+                               CreateUri("app:/shellroute/path"),
+                               CreateUri("app://host/shellroute/path"),
+                               CreateUri("app:/host/shellroute/path")
+                       };
+
+
+                       foreach(var uri in TestUris)
+                       {
+                               Assert.AreEqual(new Uri("app://host/shellroute/path"), ShellUriHandler.ConvertToStandardFormat(shell, uri));
+
+                               if(!uri.IsAbsoluteUri)
+                               {
+                                       var reverse = new Uri(uri.OriginalString.Replace("/", "\\"), UriKind.Relative);
+                                       Assert.AreEqual(new Uri("app://host/shellroute/path"), ShellUriHandler.ConvertToStandardFormat(shell, reverse));
+                               }
+                               
+                       }
+               }
+       }
+}
index 048e5cf..f9ee573 100644 (file)
@@ -74,6 +74,8 @@
     <Compile Include="CommandTests.cs" />
     <Compile Include="DependencyResolutionTests.cs" />
     <Compile Include="EffectiveFlowDirectionExtensions.cs" />
+    <Compile Include="ShellTestBase.cs" />
+    <Compile Include="ShellUriHandlerTests.cs" />
     <Compile Include="VisualTests.cs" />
     <Compile Include="FlowDirectionTests.cs" />
     <Compile Include="ImageButtonUnitTest.cs" />
index e0e84bc..6a54744 100644 (file)
@@ -11,28 +11,30 @@ namespace Xamarin.Forms
 
                internal const string ImplicitPrefix = "IMPL_";
 
-               internal static string GenerateImplicitRoute (string source)
+               internal static string GenerateImplicitRoute(string source)
                {
-                       if (source.StartsWith(ImplicitPrefix, StringComparison.Ordinal))
+                       if (IsImplicit(source))
                                return source;
-                       return ImplicitPrefix + source;
+                       return String.Concat(ImplicitPrefix, source);
+               }
+               internal static bool IsImplicit(string source)
+               {
+                       return source.StartsWith(ImplicitPrefix, StringComparison.Ordinal);
+               }
+               internal static bool IsImplicit(Element source)
+               {
+                       return IsImplicit(GetRoute(source));
                }
 
                internal static bool CompareWithRegisteredRoutes(string compare) => s_routes.ContainsKey(compare);
 
-               internal static bool CompareRoutes(string route, string compare, out bool isImplicit)
+               internal static void Clear()
                {
-                       if (isImplicit = route.StartsWith(ImplicitPrefix, StringComparison.Ordinal))
-                               route = route.Substring(ImplicitPrefix.Length);
-
-                       if (compare.StartsWith(ImplicitPrefix, StringComparison.Ordinal))
-                               throw new Exception();
-
-                       return route == compare;
+                       s_routes.Clear();
                }
 
                public static readonly BindableProperty RouteProperty =
-                       BindableProperty.CreateAttached("Route", typeof(string), typeof(Routing), null, 
+                       BindableProperty.CreateAttached("Route", typeof(string), typeof(Routing), null,
                                defaultValueCreator: CreateDefaultRoute);
 
                static object CreateDefaultRoute(BindableObject bindable)
@@ -40,6 +42,13 @@ namespace Xamarin.Forms
                        return bindable.GetType().Name + ++s_routeCount;
                }
 
+               public static string[] GetRouteKeys()
+               {
+                       string[] keys = new string[s_routes.Count];
+                       s_routes.Keys.CopyTo(keys, 0);
+                       return keys;
+               }
+
                public static Element GetOrCreateContent(string route)
                {
                        Element result = null;
@@ -66,18 +75,47 @@ namespace Xamarin.Forms
                        return (string)obj.GetValue(RouteProperty);
                }
 
+               internal static string GetRoutePathIfNotImplicit(Element obj)
+               {
+                       var source = GetRoute(obj);
+                       if (IsImplicit(source))
+                               return String.Empty;
+
+                       return $"{source}/";
+               }
+
+               public static string FormatRoute(List<string> segments)
+               {
+                       var route = FormatRoute(String.Join("/", segments));
+                       return route;
+               }
+
+               public static string FormatRoute(string route)
+               {
+                       return route;
+               }
+
                public static void RegisterRoute(string route, RouteFactory factory)
                {
-                       if (!ValidateRoute(route))
-                               throw new ArgumentException("Route must contain only lowercase letters");
+                       if (!String.IsNullOrWhiteSpace(route))
+                               route = FormatRoute(route);
+                       ValidateRoute(route);
 
                        s_routes[route] = factory;
                }
 
+               public static void UnRegisterRoute(string route)
+               {
+                       if (s_routes.TryGetValue(route, out _))
+                               s_routes.Remove(route);
+               }
+
                public static void RegisterRoute(string route, Type type)
                {
-                       if (!ValidateRoute(route))
-                               throw new ArgumentException("Route must contain only lowercase letters");
+                       if(!String.IsNullOrWhiteSpace(route))
+                               route = FormatRoute(route);
+
+                       ValidateRoute(route);
 
                        s_routes[route] = new TypeRouteFactory(type);
                }
@@ -87,13 +125,19 @@ namespace Xamarin.Forms
                        obj.SetValue(RouteProperty, value);
                }
 
-               static bool ValidateRoute(string route)
+               static void ValidateRoute(string route)
                {
-                       // Honestly this could probably be expanded to allow any URI allowable character
-                       // I just dont want to figure out what that validation looks like.
-                       // It does however need to be lowercase since uri case sensitivity is a bit touchy
-                       Regex r = new Regex(@"^[a-z|\/]*$");
-                       return r.IsMatch(route);
+                       if (string.IsNullOrWhiteSpace(route))
+                               throw new ArgumentNullException("Route cannot be an empty string");
+
+                       var uri = new Uri(route, UriKind.RelativeOrAbsolute);
+
+                       var parts = uri.OriginalString.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
+                       foreach (var part in parts)
+                       {
+                               if (IsImplicit(part))
+                                       throw new ArgumentException($"Route contains invalid characters in \"{part}\"");
+                       }
                }
 
                class TypeRouteFactory : RouteFactory
index 57691a4..8311d83 100644 (file)
@@ -1,9 +1,11 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using Xamarin.Forms.Internals;
 
 namespace Xamarin.Forms
 {
+       [DebuggerDisplay("Title = {Title}, Route = {Route}")]
        public class BaseShellItem : NavigableElement, IPropertyPropagationController, IVisualController, IFlowDirectionController
        {
                #region PropertyKeys
index 8179d84..9d62c05 100644 (file)
@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Threading.Tasks;
 
 namespace Xamarin.Forms
 {
@@ -35,6 +36,8 @@ namespace Xamarin.Forms
 
                void OnFlyoutItemSelected(Element element);
 
+               Task OnFlyoutItemSelectedAsync(Element element);
+
                bool ProposeNavigation(ShellNavigationSource source, ShellItem item, ShellSection shellSection, ShellContent shellContent, IReadOnlyList<Page> stack, bool canCancel);
 
                bool RemoveAppearanceObserver(IAppearanceObserver observer);
index 6713cc3..0ca8839 100644 (file)
@@ -6,7 +6,7 @@ namespace Xamarin.Forms
 {
        public interface IShellItemController : IElementController
        {
-               Task GoToPart(List<string> parts, Dictionary<string, string> queryData);
+               Task GoToPart(NavigationRequest navigationRequest, Dictionary<string, string> queryData);
 
                bool ProposeSection(ShellSection shellSection, bool setValue = true);
        }
index 9889e17..6e51040 100644 (file)
@@ -15,7 +15,7 @@ namespace Xamarin.Forms
 
                void AddDisplayedPageObserver(object observer, Action<Page> callback);
 
-               Task GoToPart(List<string> parts, Dictionary<string, string> queryData);
+               Task GoToPart(NavigationRequest request, Dictionary<string, string> queryData);
 
                bool RemoveContentInsetObserver(IShellContentInsetObserver observer);
 
index 52c128d..f24daaf 100644 (file)
@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.ComponentModel;
+using System.Diagnostics;
 using System.Linq;
 using System.Reflection;
 using System.Runtime.CompilerServices;
@@ -80,13 +81,14 @@ namespace Xamarin.Forms
                {
                        var element = (Element)bindable;
 
-                       while (!Application.IsApplicationOrNull(element)) {
+                       while (!Application.IsApplicationOrNull(element))
+                       {
                                if (element is Shell shell)
                                        shell.NotifyFlyoutBehaviorObservers();
                                element = element.Parent;
                        }
                }
-               
+
                public static readonly BindableProperty ShellBackgroundColorProperty =
                        BindableProperty.CreateAttached("ShellBackgroundColor", typeof(Color), typeof(Shell), Color.Default,
                                propertyChanged: OnShellColorValueChanged);
@@ -266,34 +268,40 @@ namespace Xamarin.Forms
 
                async void IShellController.OnFlyoutItemSelected(Element element)
                {
+                       await (this as IShellController).OnFlyoutItemSelectedAsync(element);
+               }
+
+               async Task IShellController.OnFlyoutItemSelectedAsync(Element element)
+               {
                        ShellItem shellItem = null;
                        ShellSection shellSection = null;
                        ShellContent shellContent = null;
 
-                       switch (element) {
-                       case MenuShellItem menuShellItem:
-                               ((IMenuItemController)menuShellItem.MenuItem).Activate();
-                               break;
-                       case ShellItem i:
-                               shellItem = i;
-                               break;
-                       case ShellSection s:
-                               shellItem = s.Parent as ShellItem;
-                               shellSection = s;
-                               break;
-                       case ShellContent c:
-                               shellItem = c.Parent.Parent as ShellItem;
-                               shellSection = c.Parent as ShellSection;
-                               shellContent = c;
-                               break;
-                       case MenuItem m:
-                               ((IMenuItemController)m).Activate();
-                               break;
+                       switch (element)
+                       {
+                               case MenuShellItem menuShellItem:
+                                       ((IMenuItemController)menuShellItem.MenuItem).Activate();
+                                       break;
+                               case ShellItem i:
+                                       shellItem = i;
+                                       break;
+                               case ShellSection s:
+                                       shellItem = s.Parent as ShellItem;
+                                       shellSection = s;
+                                       break;
+                               case ShellContent c:
+                                       shellItem = c.Parent.Parent as ShellItem;
+                                       shellSection = c.Parent as ShellSection;
+                                       shellContent = c;
+                                       break;
+                               case MenuItem m:
+                                       ((IMenuItemController)m).Activate();
+                                       break;
                        }
 
                        if (shellItem == null || !shellItem.IsEnabled)
                                return;
-                               
+
                        shellSection = shellSection ?? shellItem.CurrentItem;
                        shellContent = shellContent ?? shellSection?.CurrentItem;
 
@@ -343,40 +351,36 @@ namespace Xamarin.Forms
 
                public static Shell Current => Application.Current?.MainPage as Shell;
 
-               Uri GetAbsoluteUri(Uri relativeUri)
-               {
-                       if (CurrentItem == null)
-                               throw new InvalidOperationException("Relative path is used after selecting Current item.");
 
-                       var parseUri = Regex.Match(relativeUri.OriginalString, @"(?<u>.+?)(\?(?<q>.+?))?(#(?<f>.+))?$").Groups;
-                       var url = parseUri["u"].Value;
-                       var query = parseUri["q"].Value;
-                       var fragment = parseUri["f"].Value;
+               List<RequestDefinition> BuildAllTheRoutes()
+               {
+                       List<RequestDefinition> routes = new List<RequestDefinition>();
+                       // todo make better maybe
 
-                       Element item = CurrentItem;
-                       var list = new List<string>();
-                       while (item != null && !(item is IApplicationController))
+                       for (var i = 0; i < Items.Count; i++)
                        {
-                               var route = Routing.GetRoute(item)?.Trim('/');
-                               if (string.IsNullOrEmpty(route))
-                                       break;
-                               list.Insert(0, route);
-                               item = item.Parent;
-                       }
+                               var item = Items[i];
 
-                       var isGlobalRegisteredRoute = Routing.CompareWithRegisteredRoutes(url);
-                       if (isGlobalRegisteredRoute)
-                               list.RemoveRange(1, list.Count - 1);
+                               for (var j = 0; j < item.Items.Count; j++)
+                               {
+                                       var section = item.Items[j];
 
-                       list.Add(url.Trim('/'));
+                                       for (var k = 0; k < section.Items.Count; k++)
+                                       {
+                                               var content = section.Items[k];
 
-                       var parentUriBuilder = new UriBuilder(RouteScheme)
-                       {
-                               Path = string.Join("/", list),
-                               Query = query,
-                               Fragment = fragment
-                       };
-                       return parentUriBuilder.Uri;
+                                               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<string>()));
+                                       }
+                               }
+                       }
+
+                       return routes;
                }
 
                public async Task GoToAsync(ShellNavigationState state, bool animate = true)
@@ -388,9 +392,9 @@ namespace Xamarin.Forms
 
                        _accumulateNavigatedEvents = true;
 
-                       var uri = state.Location.IsAbsoluteUri ? state.Location : GetAbsoluteUri(state.Location);
-
-                       var queryString = uri.Query;
+                       var navigationRequest = ShellUriHandler.GetNavigationRequest(this, state.Location);
+                       var uri = navigationRequest.Request.FullUri;
+                       var queryString = navigationRequest.Query;
                        var queryData = ParseQueryString(queryString);
                        var path = uri.AbsolutePath;
 
@@ -409,42 +413,39 @@ namespace Xamarin.Forms
                        else
                                parts.RemoveAt(0);
 
-                       var shellItemRoute = parts[0];
                        ApplyQueryAttributes(this, queryData, false);
 
-                       var items = Items;
-                       for (int i = 0; i < items.Count; i++)
+                       var shellItem = navigationRequest.Request.Item;
+                       if (shellItem != null)
                        {
-                               var shellItem = items[i];
-                               if (Routing.CompareRoutes(shellItem.Route, shellItemRoute, out var isImplicit))
-                               {
-                                       ApplyQueryAttributes(shellItem, queryData, parts.Count == 1);
+                               ApplyQueryAttributes(shellItem, queryData, navigationRequest.Request.Section == null);
 
-                                       if (CurrentItem != shellItem)
-                                               SetValueFromRenderer(CurrentItemProperty, shellItem);
-
-                                       if (!isImplicit)
-                                               parts.RemoveAt(0);
+                               if (CurrentItem != shellItem)
+                                       SetValueFromRenderer(CurrentItemProperty, shellItem);
 
-                                       if (parts.Count > 0)
-                                               await ((IShellItemController)shellItem).GoToPart(parts, queryData);
+                               parts.RemoveAt(0);
 
-                                       break;
-                               }
+                               if (parts.Count > 0)
+                                       await ((IShellItemController)shellItem).GoToPart(navigationRequest, queryData);
                        }
-
-                       if (Routing.CompareWithRegisteredRoutes(shellItemRoute))
+                       else
                        {
-                               var shellItem = ShellItem.GetShellItemFromRouteName(shellItemRoute);
+                               await CurrentItem.CurrentItem.GoToAsync(navigationRequest.Request.GlobalRoutes, queryData, animate);
+                       }
+                       
+                       //if (Routing.CompareWithRegisteredRoutes(shellItemRoute))
+                       //{
+                       //      var shellItem = ShellItem.GetShellItemFromRouteName(shellItemRoute);
 
-                               ApplyQueryAttributes(shellItem, queryData, parts.Count == 1);
+                       //      ApplyQueryAttributes(shellItem, queryData, parts.Count == 1);
 
-                               if (CurrentItem != shellItem)
-                                       SetValueFromRenderer(CurrentItemProperty, shellItem);
+                       //      if (CurrentItem != shellItem)
+                       //              SetValueFromRenderer(CurrentItemProperty, shellItem);
+
+                       //      if (parts.Count > 0)
+                       //              await ((IShellItemController)shellItem).GoToPart(parts, queryData);
+                       //}
 
-                               if (parts.Count > 0)
-                                       await ((IShellItemController)shellItem).GoToPart(parts, queryData);
-                       }
                        _accumulateNavigatedEvents = false;
 
                        // this can be null in the event that no navigation actually took place!
@@ -467,7 +468,8 @@ namespace Xamarin.Forms
                        }
 
                        //if the lastItem is implicitly wrapped, get the actual ShellContent
-                       if (isLastItem) {
+                       if (isLastItem)
+                       {
                                if (element is ShellItem shellitem && shellitem.Items.FirstOrDefault() is ShellSection section)
                                        element = section;
                                if (element is ShellSection shellsection && shellsection.Items.FirstOrDefault() is ShellContent content)
@@ -481,7 +483,8 @@ namespace Xamarin.Forms
 
                        //filter the query to only apply the keys with matching prefix
                        var filteredQuery = new Dictionary<string, string>(query.Count);
-                       foreach (var q in query) {
+                       foreach (var q in query)
+                       {
                                if (!q.Key.StartsWith(prefix, StringComparison.Ordinal))
                                        continue;
                                var key = q.Key.Substring(prefix.Length);
@@ -506,7 +509,7 @@ namespace Xamarin.Forms
                        if (shellItem != null)
                        {
                                var shellItemRoute = shellItem.Route;
-                               if (!shellItemRoute.StartsWith(Routing.ImplicitPrefix, StringComparison.Ordinal))
+                               //if (!shellItemRoute.StartsWith(Routing.ImplicitPrefix, StringComparison.Ordinal))
                                {
                                        stateBuilder.Append(shellItemRoute);
                                        stateBuilder.Append("/");
@@ -515,7 +518,7 @@ namespace Xamarin.Forms
                                if (shellSection != null)
                                {
                                        var shellSectionRoute = shellSection.Route;
-                                       if (!shellSectionRoute.StartsWith(Routing.ImplicitPrefix, StringComparison.Ordinal))
+                                       //if (!shellSectionRoute.StartsWith(Routing.ImplicitPrefix, StringComparison.Ordinal))
                                        {
                                                stateBuilder.Append(shellSectionRoute);
                                                stateBuilder.Append("/");
@@ -524,7 +527,7 @@ namespace Xamarin.Forms
                                        if (shellContent != null)
                                        {
                                                var shellContentRoute = shellContent.Route;
-                                               if (!shellContentRoute.StartsWith(Routing.ImplicitPrefix, StringComparison.Ordinal))
+                                               //if (!shellContentRoute.StartsWith(Routing.ImplicitPrefix, StringComparison.Ordinal))
                                                {
                                                        stateBuilder.Append(shellContentRoute);
                                                        stateBuilder.Append("/");
@@ -598,9 +601,11 @@ namespace Xamarin.Forms
 
                internal Shell(bool checkFlag)
                {
+                       Navigation = new NavigationImpl(this);
                        _checkExperimentalFlag = checkFlag;
                        VerifyShellFlagEnabled(constructorHint: nameof(Shell));
                        ((INotifyCollectionChanged)Items).CollectionChanged += (s, e) => SendStructureChanged();
+                       Route = Routing.GenerateImplicitRoute("shell");
                }
 
                internal const string ShellExperimental = ExperimentalFlags.ShellExperimental;
@@ -608,7 +613,7 @@ namespace Xamarin.Forms
                [EditorBrowsable(EditorBrowsableState.Never)]
                internal void VerifyShellFlagEnabled(string constructorHint = null, [CallerMemberName] string memberName = "")
                {
-                       if(_checkExperimentalFlag)
+                       if (_checkExperimentalFlag)
                                ExperimentalFlags.VerifyFlagEnabled("Shell", ShellExperimental, constructorHint, memberName);
                }
 
@@ -622,44 +627,52 @@ namespace Xamarin.Forms
                        set => SetValue(FlyoutIconProperty, value);
                }
 
-               public ShellItem CurrentItem {
+               public ShellItem CurrentItem
+               {
                        get => (ShellItem)GetValue(CurrentItemProperty);
                        set => SetValue(CurrentItemProperty, value);
                }
 
                public ShellNavigationState CurrentState => (ShellNavigationState)GetValue(CurrentStateProperty);
 
-               public Color FlyoutBackgroundColor {
+               public Color FlyoutBackgroundColor
+               {
                        get => (Color)GetValue(FlyoutBackgroundColorProperty);
                        set => SetValue(FlyoutBackgroundColorProperty, value);
                }
 
-               public FlyoutBehavior FlyoutBehavior {
+               public FlyoutBehavior FlyoutBehavior
+               {
                        get => (FlyoutBehavior)GetValue(FlyoutBehaviorProperty);
                        set => SetValue(FlyoutBehaviorProperty, value);
                }
 
-               public object FlyoutHeader {
+               public object FlyoutHeader
+               {
                        get => GetValue(FlyoutHeaderProperty);
                        set => SetValue(FlyoutHeaderProperty, value);
                }
 
-               public FlyoutHeaderBehavior FlyoutHeaderBehavior {
+               public FlyoutHeaderBehavior FlyoutHeaderBehavior
+               {
                        get => (FlyoutHeaderBehavior)GetValue(FlyoutHeaderBehaviorProperty);
                        set => SetValue(FlyoutHeaderBehaviorProperty, value);
                }
 
-               public DataTemplate FlyoutHeaderTemplate {
+               public DataTemplate FlyoutHeaderTemplate
+               {
                        get => (DataTemplate)GetValue(FlyoutHeaderTemplateProperty);
                        set => SetValue(FlyoutHeaderTemplateProperty, value);
                }
 
-               public bool FlyoutIsPresented {
+               public bool FlyoutIsPresented
+               {
                        get => (bool)GetValue(FlyoutIsPresentedProperty);
                        set => SetValue(FlyoutIsPresentedProperty, value);
                }
 
-               public DataTemplate GroupHeaderTemplate {
+               public DataTemplate GroupHeaderTemplate
+               {
                        get => (DataTemplate)GetValue(GroupHeaderTemplateProperty);
                        set => SetValue(GroupHeaderTemplateProperty, value);
                }
@@ -667,19 +680,22 @@ namespace Xamarin.Forms
                public ShellItemCollection Items => (ShellItemCollection)GetValue(ItemsProperty);
                public ShellItemCollection Flyout => Items;
 
-               public DataTemplate ItemTemplate {
+               public DataTemplate ItemTemplate
+               {
                        get => (DataTemplate)GetValue(ItemTemplateProperty);
                        set => SetValue(ItemTemplateProperty, value);
                }
 
                public MenuItemCollection MenuItems => (MenuItemCollection)GetValue(MenuItemsProperty);
 
-               public DataTemplate MenuItemTemplate {
+               public DataTemplate MenuItemTemplate
+               {
                        get => (DataTemplate)GetValue(MenuItemTemplateProperty);
                        set => SetValue(MenuItemTemplateProperty, value);
                }
 
-               public string Route {
+               public string Route
+               {
                        get => Routing.GetRoute(this);
                        set => Routing.SetRoute(this, value);
                }
@@ -688,9 +704,11 @@ namespace Xamarin.Forms
 
                public string RouteScheme { get; set; } = "app";
 
-               View FlyoutHeaderView {
+               View FlyoutHeaderView
+               {
                        get => _flyoutHeaderView;
-                       set {
+                       set
+                       {
                                if (_flyoutHeaderView == value)
                                        return;
 
@@ -816,7 +834,8 @@ namespace Xamarin.Forms
                {
                        if (_accumulateNavigatedEvents)
                                _accumulatedEvent = args;
-                       else {
+                       else
+                       {
                                /* Removing this check for now as it doesn't properly cover all implicit scenarios
                                 * if (args.Current.Location.AbsolutePath.TrimEnd('/') != _lastNavigating.Location.AbsolutePath.TrimEnd('/'))
                                        throw new InvalidOperationException($"Navigation: Current location doesn't match navigation uri {args.Current.Location.AbsolutePath} != {_lastNavigating.Location.AbsolutePath}");
@@ -1060,19 +1079,20 @@ namespace Xamarin.Forms
 
                Element WalkToPage(Element element)
                {
-                       switch (element) {
-                       case Shell shell:
-                               element = shell.CurrentItem;
-                               break;
-                       case ShellItem shellItem:
-                               element = shellItem.CurrentItem;
-                               break;
-                       case ShellSection shellSection:
-                               var controller = (IShellSectionController)element;
-                               // this is the same as .Last but easier and will add in the root if not null
-                               // it generally wont be null but this is just in case
-                               element = controller.PresentedPage ?? element;
-                               break;
+                       switch (element)
+                       {
+                               case Shell shell:
+                                       element = shell.CurrentItem;
+                                       break;
+                               case ShellItem shellItem:
+                                       element = shellItem.CurrentItem;
+                                       break;
+                               case ShellSection shellSection:
+                                       var controller = (IShellSectionController)element;
+                                       // this is the same as .Last but easier and will add in the root if not null
+                                       // it generally wont be null but this is just in case
+                                       element = controller.PresentedPage ?? element;
+                                       break;
                        }
 
                        return element;
@@ -1084,5 +1104,26 @@ namespace Xamarin.Forms
                        if (FlyoutHeaderView != null)
                                PropertyPropagationExtensions.PropagatePropertyChanged(propertyName, this, new[] { FlyoutHeaderView });
                }
+
+               public class NavigationImpl : NavigationProxy
+               {
+                       readonly Shell _shell;
+
+                       NavigationProxy SectionProxy => _shell.CurrentItem.CurrentItem.NavigationProxy;                 
+
+                       public NavigationImpl(Shell shell) => _shell = shell;
+
+                       protected override IReadOnlyList<Page> GetNavigationStack() => SectionProxy.NavigationStack;
+
+                       protected override void OnInsertPageBefore(Page page, Page before) => SectionProxy.InsertPageBefore(page, before);
+
+                       protected override Task<Page> OnPopAsync(bool animated) => SectionProxy.PopAsync(animated);
+
+                       protected override Task OnPopToRootAsync(bool animated) => SectionProxy.PopToRootAsync(animated);
+
+                       protected override Task OnPushAsync(Page page, bool animated) => SectionProxy.PushAsync(page, animated);
+
+                       protected override void OnRemovePage(Page page) => SectionProxy.RemovePage(page);
+               }
        }
 }
index a8d0a7c..955b2fc 100644 (file)
@@ -24,31 +24,19 @@ namespace Xamarin.Forms
 
                #region IShellItemController
 
-               Task IShellItemController.GoToPart(List<string> parts, Dictionary<string, string> queryData)
+               Task IShellItemController.GoToPart(NavigationRequest request, Dictionary<string, string> queryData)
                {
-                       var shellSectionRoute = parts[0];
+                       var shellSection = request.Request.Section;
 
-                       var items = Items;
-                       for (int i = 0; i < items.Count; i++)
-                       {
-                               var shellSection = items[i];
-                               if (Routing.CompareRoutes(shellSection.Route, shellSectionRoute, out var isImplicit))
-                               {
-                                       Shell.ApplyQueryAttributes(shellSection, queryData, parts.Count == 1);
-
-                                       if (CurrentItem != shellSection)
-                                               SetValueFromRenderer(CurrentItemProperty, shellSection);
-
-                                       if (!isImplicit)
-                                               parts.RemoveAt(0);
-                                       if (parts.Count > 0)
-                                       {
-                                               return ((IShellSectionController)shellSection).GoToPart(parts, queryData);
-                                       }
-                                       break;
-                               }
-                       }
-                       return Task.FromResult(true);
+                       if (shellSection == null)
+                               return Task.FromResult(true);
+
+                       Shell.ApplyQueryAttributes(shellSection, queryData, request.Request.Content == null);
+
+                       if (CurrentItem != shellSection)
+                               SetValueFromRenderer(CurrentItemProperty, shellSection);
+
+                       return ((IShellSectionController)shellSection).GoToPart(request, queryData);
                }
 
                bool IShellItemController.ProposeSection(ShellSection shellSection, bool setValue)
@@ -116,10 +104,7 @@ namespace Xamarin.Forms
                        }
                }
 
-#if DEBUG
-               [Obsolete ("Please dont use this in core code... its SUPER hard to debug when this happens", true)]
-#endif
-               public static implicit operator ShellItem(ShellSection shellSection)
+               internal static ShellItem CreateFromShellSection(ShellSection shellSection)
                {
                        var result = new ShellItem();
 
@@ -132,6 +117,14 @@ namespace Xamarin.Forms
                        return result;
                }
 
+#if DEBUG
+               [Obsolete ("Please dont use this in core code... its SUPER hard to debug when this happens", true)]
+#endif
+               public static implicit operator ShellItem(ShellSection shellSection)
+               {
+                       return CreateFromShellSection(shellSection);
+               }
+
                internal static ShellItem GetShellItemFromRouteName(string route)
                {
                        var shellContent = new ShellContent { Route = route, Content = Routing.GetOrCreateContent(route) };
index bd9388f..93e80b2 100644 (file)
@@ -1,7 +1,10 @@
 using System;
+using System.Diagnostics;
 
 namespace Xamarin.Forms
 {
+
+       [DebuggerDisplay("Location = {Location}")]
        public class ShellNavigationState
        {
                public Uri Location { get; set; }
index 0d05786..16ac997 100644 (file)
@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
+using System.Diagnostics;
 using System.Linq;
 using System.Threading.Tasks;
 using Xamarin.Forms.Internals;
@@ -64,30 +65,27 @@ namespace Xamarin.Forms
                        callback(DisplayedPage);
                }
 
-               Task IShellSectionController.GoToPart(List<string> parts, Dictionary<string, string> queryData)
+               Task IShellSectionController.GoToPart(NavigationRequest request, Dictionary<string, string> queryData)
                {
-                       var shellContentRoute = parts[0];
+                       ShellContent shellContent = request.Request.Content;
 
-                       var items = Items;
-                       for (int i = 0; i < items.Count; i++)
+                       if (shellContent == null)
+                               return Task.FromResult(true);
+
+                       
+                       if(request.Request.GlobalRoutes.Count > 0)
                        {
-                               ShellContent shellContent = items[i];
-                               if (Routing.CompareRoutes(shellContent.Route, shellContentRoute, out var isImplicit))
+                               // TODO get rid of this hack and fix so if there's a stack the current page doesn't display
+                               Device.BeginInvokeOnMainThread(async () =>
                                {
-                                       Shell.ApplyQueryAttributes(shellContent, queryData, parts.Count == 1);
+                                       await GoToAsync(request.Request.GlobalRoutes, queryData, false);
+                               });
+                       }
 
-                                       if (CurrentItem != shellContent)
-                                               SetValueFromRenderer(CurrentItemProperty, shellContent);
+                       Shell.ApplyQueryAttributes(shellContent, queryData, request.Request.GlobalRoutes.Count == 0);
 
-                                       if (!isImplicit)
-                                               parts.RemoveAt(0);
-                                       if (parts.Count > 0)
-                                       {
-                                               return GoToAsync(parts, queryData, false);
-                                       }
-                                       break;
-                               }
-                       }
+                       if (CurrentItem != shellContent)
+                               SetValueFromRenderer(CurrentItemProperty, shellContent);
 
                        return Task.FromResult(true);
                }
@@ -190,10 +188,7 @@ namespace Xamarin.Forms
 
                ShellItem ShellItem => Parent as ShellItem;
 
-#if DEBUG
-               [Obsolete("Please dont use this in core code... its SUPER hard to debug when this happens", true)]
-#endif
-               public static implicit operator ShellSection(ShellContent shellContent)
+               internal static ShellSection CreateFromShellContent(ShellContent shellContent)
                {
                        var shellSection = new ShellSection();
 
@@ -207,6 +202,19 @@ namespace Xamarin.Forms
                        return shellSection;
                }
 
+               internal static ShellSection CreateFromTemplatedPage(TemplatedPage page)
+               {
+                       return CreateFromShellContent((ShellContent)page);
+               }
+
+#if DEBUG
+               [Obsolete("Please dont use this in core code... its SUPER hard to debug when this happens", true)]
+#endif
+               public static implicit operator ShellSection(ShellContent shellContent)
+               {
+                       return CreateFromShellContent(shellContent);
+               }
+
 #if DEBUG
                [Obsolete("Please dont use this in core code... its SUPER hard to debug when this happens", true)]
 #endif
diff --git a/Xamarin.Forms.Core/Shell/ShellUriHandler.cs b/Xamarin.Forms.Core/Shell/ShellUriHandler.cs
new file mode 100644 (file)
index 0000000..d9be121
--- /dev/null
@@ -0,0 +1,757 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+
+namespace Xamarin.Forms
+{
+
+       internal class ShellUriHandler
+       {
+               static readonly char[] _pathSeparator = { '/', '\\' };
+
+               static Uri FormatUri(Uri path)
+               {
+                       if (path.IsAbsoluteUri)
+                               return path;
+
+                       return new Uri(FormatUri(path.OriginalString), UriKind.Relative);
+               }
+
+               static string FormatUri(string path)
+               {
+                       return path.Replace("\\", "/");
+               }
+
+               public static Uri ConvertToStandardFormat(Shell shell, Uri request)
+               {
+                       request = FormatUri(request);
+                       string pathAndQuery = null;
+                       if (request.IsAbsoluteUri)
+                               pathAndQuery = $"{request.Host}/{request.PathAndQuery}";
+                       else
+                               pathAndQuery = request.OriginalString;
+
+                       var segments = new List<string>(pathAndQuery.Split(_pathSeparator, StringSplitOptions.RemoveEmptyEntries));
+
+
+                       if (segments[0] != shell.RouteHost)
+                               segments.Insert(0, shell.RouteHost);
+
+                       if (segments[1] != shell.Route)
+                               segments.Insert(1, shell.Route);
+
+                       var path = String.Join("/", segments.ToArray());
+                       string uri = $"{shell.RouteScheme}://{path}";
+
+                       return new Uri(uri);
+               }
+
+               public static NavigationRequest GetNavigationRequest(Shell shell, Uri uri)
+               {
+                       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("\\\\"))
+                               whatDoIDo = NavigationRequest.WhatToDoWithTheStack.ReplaceIt;
+                       else
+                               whatDoIDo = NavigationRequest.WhatToDoWithTheStack.PushToIt;
+
+                       Uri request = ConvertToStandardFormat(shell, uri);
+
+                       var possibleRouteMatches = GenerateRoutePaths(shell, request, uri);
+
+
+                       if (possibleRouteMatches.Count == 0)
+                               throw new ArgumentException($"unable to figure out route for: {uri}", nameof(uri));
+                       else if (possibleRouteMatches.Count > 1)
+                       {
+                               string[] matches = new string[possibleRouteMatches.Count];
+                               int i = 0;
+                               foreach (var match in possibleRouteMatches)
+                               {
+                                       matches[i] = match.PathFull;
+                                       i++;
+                               }
+
+                               string matchesFound = String.Join(",", matches);
+                               throw new ArgumentException($"Ambiguous routes matched for: {uri} matches found: {matchesFound}", nameof(uri));
+
+                       }
+
+                       var theWinningRoute = possibleRouteMatches[0];
+                       RequestDefinition definition =
+                               new RequestDefinition(
+                                       ConvertToStandardFormat(shell, new Uri(theWinningRoute.PathFull, UriKind.RelativeOrAbsolute)),
+                                       new Uri(theWinningRoute.PathNoImplicit, UriKind.RelativeOrAbsolute),
+                                       theWinningRoute.Item,
+                                       theWinningRoute.Section,
+                                       theWinningRoute.Content,
+                                       theWinningRoute.GlobalRouteMatches);
+
+                       NavigationRequest navigationRequest = new NavigationRequest(definition, whatDoIDo, request.Query, request.Fragment);
+
+                       return navigationRequest;
+               }
+
+               internal static List<RouteRequestBuilder> GenerateRoutePaths(Shell shell, Uri request)
+               {
+                       request = FormatUri(request);
+                       return GenerateRoutePaths(shell, request, request);
+               }
+
+               internal static List<RouteRequestBuilder> GenerateRoutePaths(Shell shell, Uri request, Uri originalRequest)
+               {
+                       request = FormatUri(request);
+                       originalRequest = FormatUri(originalRequest);
+
+                       var routeKeys = Routing.GetRouteKeys();
+                       for (int i = 0; i < routeKeys.Length; i++)
+                       {
+                               routeKeys[i] = FormatUri(routeKeys[i]);
+                       }
+
+                       List<RouteRequestBuilder> possibleRoutePaths = new List<RouteRequestBuilder>();
+                       if (!request.IsAbsoluteUri)
+                               request = ConvertToStandardFormat(shell, request);
+
+                       string localPath = request.LocalPath;
+
+                       bool relativeMatch = false;
+                       if (!originalRequest.IsAbsoluteUri && !originalRequest.OriginalString.StartsWith("/") && !originalRequest.OriginalString.StartsWith("\\"))
+                               relativeMatch = true;
+
+                       var segments = localPath.Split(_pathSeparator, 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?
+                                       if (uri.Equals(request))
+                                       {
+                                               var builder = new RouteRequestBuilder(route, route, null, segments);
+                                               return new List<RouteRequestBuilder> { builder };
+                                       }
+                               }
+                       }
+
+                       var depthStart = 0;
+
+                       if (segments[0] == shell.Route)
+                       {
+                               segments = segments.Skip(1).ToArray();
+                               depthStart = 1;
+                       }
+                       else
+                       {
+                               depthStart = 0;
+                       }
+
+                       if(relativeMatch && shell?.CurrentItem != null)
+                       {
+                               // retrieve current location
+                               var currentLocation = NodeLocation.Create(shell);
+
+                               while (currentLocation.Shell != null)
+                               {
+                                       List<RouteRequestBuilder> pureRoutesMatch = new List<RouteRequestBuilder>();
+                                       List<RouteRequestBuilder> pureGlobalRoutesMatch = new List<RouteRequestBuilder>();
+
+                                       SearchPath(currentLocation.LowestChild, null, segments, pureRoutesMatch, 0);
+                                       SearchPath(currentLocation.LowestChild, null, segments, pureGlobalRoutesMatch, 0, ignoreGlobalRoutes: false);
+                                       pureRoutesMatch = GetBestMatches(pureRoutesMatch);
+                                       pureGlobalRoutesMatch = GetBestMatches(pureGlobalRoutesMatch);
+
+                                       if (pureRoutesMatch.Count > 0)
+                                               return pureRoutesMatch;
+
+                                       if (pureGlobalRoutesMatch.Count > 0)
+                                               return pureGlobalRoutesMatch;
+
+                                       currentLocation.Pop();
+                               }
+
+                               string searchPath = String.Join("/", segments);
+
+                               if (routeKeys.Contains(searchPath))
+                               {
+                                       return new List<RouteRequestBuilder> { new RouteRequestBuilder(searchPath, searchPath, null, segments) };
+                               }
+
+                               RouteRequestBuilder builder = null;
+                               foreach (var segment in segments)
+                               {
+                                       if(routeKeys.Contains(segment))
+                                       {
+                                               if (builder == null)
+                                                       builder = new RouteRequestBuilder(segment, segment, null, segments);
+                                               else
+                                                       builder.AddGlobalRoute(segment, segment);
+                                       }
+                               }
+
+                               if(builder != null && builder.IsFullMatch)
+                                       return new List<RouteRequestBuilder> { builder };
+                       }
+                       else
+                       {
+                               possibleRoutePaths.Clear();
+                               SearchPath(shell, null, segments, possibleRoutePaths, depthStart);
+
+                               var bestMatches = GetBestMatches(possibleRoutePaths);
+                               if (bestMatches.Count > 0)
+                                       return bestMatches;
+
+                               bestMatches.Clear();
+                               foreach (var possibleRoutePath in possibleRoutePaths)
+                               {
+                                       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<RouteRequestBuilder> pureGlobalRoutesMatch = new List<RouteRequestBuilder>();
+                                               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;
+                                               }
+
+
+                                               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<RouteRequestBuilder> GetBestMatches(List<RouteRequestBuilder> possibleRoutePaths)
+               {
+                       List<RouteRequestBuilder> bestMatches = new List<RouteRequestBuilder>();
+                       foreach (var match in possibleRoutePaths)
+                       {
+                               if (match.IsFullMatch)
+                                       bestMatches.Add(match);
+                       }
+
+                       return bestMatches;
+               }
+
+               internal class NodeLocation
+               {
+                       public Shell Shell { get; private set; }
+                       public ShellItem Item { get; private set; }
+                       public ShellSection Section { get; private set; }
+                       public ShellContent Content { get; private set; }
+                       public object LowestChild =>
+                               (object)Content ?? (object)Section ?? (object)Item ?? (object)Shell;
+
+
+                       public static NodeLocation Create(Shell shell)
+                       {
+                               NodeLocation location = new NodeLocation();
+                               location.SetNode(
+                                       (object)shell.CurrentItem?.CurrentItem?.CurrentItem ?? 
+                                       (object)shell.CurrentItem?.CurrentItem ?? 
+                                       (object)shell.CurrentItem ?? 
+                                       (object)shell);
+
+                               return location;
+                       }
+
+                       public void SetNode(object node)
+                       {
+                               switch (node)
+                               {
+                                       case Shell shell:
+                                               Shell = shell;
+                                               Item = null;
+                                               Section = null;
+                                               Content = null;
+                                               break;
+                                       case ShellItem item:
+                                               Item = item;
+                                               Section = null;
+                                               Content = null;
+                                               if (Shell == null)
+                                                       Shell = (Shell)Item.Parent;
+                                               break;
+                                       case ShellSection section:
+                                               Section = section;
+
+                                               if (Item == null)
+                                                       Item = Section.Parent as ShellItem;
+
+                                               if (Shell == null)
+                                                       Shell = (Shell)Item.Parent;
+
+                                               Content = null;
+
+                                               break;
+                                       case ShellContent content:
+                                               Content = content;
+                                               if (Section == null)
+                                                       Section = Content.Parent as ShellSection;
+
+                                               if (Item == null)
+                                                       Item = Section.Parent as ShellItem;
+
+                                               if (Shell == null)
+                                                       Shell = (Shell)Item.Parent;
+
+                                               break;
+
+                               }
+                       }
+
+                       public Uri GetUri()
+                       {
+                               List<string> paths = new List<string>();
+                               paths.Add(Shell.RouteHost);
+                               paths.Add(Shell.Route);
+                               if (Item != null && !Routing.IsImplicit(Item))
+                                       paths.Add(Item.Route);
+                               if (Section != null && !Routing.IsImplicit(Section))
+                                       paths.Add(Section.Route);
+                               if (Content != null && !Routing.IsImplicit(Content))
+                                       paths.Add(Content.Route);
+
+                               string uri = String.Join("/", paths);
+                               return new Uri($"{Shell.RouteScheme}://{uri}");
+                       }
+
+                       public void Pop()
+                       {
+                               if (Content != null)
+                                       Content = null;
+                               else if (Section != null)
+                                       Section = null;
+                               else if (Item != null)
+                                       Item = null;
+                               else if (Shell != null)
+                                       Shell = null;
+                       }
+               }
+
+               static void SearchPath(
+                       object node,
+                       RouteRequestBuilder currentMatchedPath,
+                       string[] segments,
+                       List<RouteRequestBuilder> possibleRoutePaths,
+                       int depthToStart,
+                       int myDepth = -1,
+                       NodeLocation currentLocation = null,
+                       bool ignoreGlobalRoutes = true)
+               {
+                       if (node is GlobalRouteItem && ignoreGlobalRoutes)
+                               return;
+
+                       ++myDepth;
+                       currentLocation = currentLocation ?? new NodeLocation();
+                       currentLocation.SetNode(node);
+
+                       IEnumerable items = null;
+                       if (depthToStart > myDepth)
+                       {
+                               items = GetItems(node);
+                               if (items == null)
+                                       return;
+
+                               foreach (var nextNode in items)
+                               {
+                                       SearchPath(nextNode, null, segments, possibleRoutePaths, depthToStart, myDepth, currentLocation, ignoreGlobalRoutes);
+                               }
+                               return;
+                       }
+
+                       string shellSegment = GetRoute(node);
+                       string userSegment = null;
+
+                       if (currentMatchedPath == null)
+                       {
+                               userSegment = segments[0];
+                       }
+                       else
+                       {
+                               userSegment = currentMatchedPath.NextSegment;
+                       }
+
+                       if (userSegment == null)
+                               return;
+
+                       RouteRequestBuilder builder = null;
+                       if (shellSegment == userSegment || Routing.IsImplicit(shellSegment))
+                       {
+                               if (currentMatchedPath == null)
+                                       builder = new RouteRequestBuilder(shellSegment, userSegment, node, segments);
+                               else
+                               {
+                                       builder = new RouteRequestBuilder(currentMatchedPath);
+                                       builder.AddMatch(shellSegment, userSegment, node);
+                               }
+
+                               if (!Routing.IsImplicit(shellSegment) || shellSegment == userSegment)
+                                       possibleRoutePaths.Add(builder);
+                       }
+
+                       items = GetItems(node);
+                       if (items == null)
+                               return;
+
+                       foreach (var nextNode in items)
+                       {
+                               SearchPath(nextNode, builder, segments, possibleRoutePaths, depthToStart, myDepth, currentLocation, ignoreGlobalRoutes);
+                       }
+               }
+
+               static string GetRoute(object node)
+               {
+                       switch (node)
+                       {
+                               case Shell shell:
+                                       return shell.Route;
+                               case ShellItem item:
+                                       return item.Route;
+                               case ShellSection section:
+                                       return section.Route;
+                               case ShellContent content:
+                                       return content.Route;
+                               case GlobalRouteItem routeItem:
+                                       return routeItem.Route;
+
+                       }
+
+                       throw new ArgumentException($"{node}", nameof(node));
+               }
+
+               static IEnumerable GetItems(object node)
+               {
+                       IEnumerable results = null;
+                       switch (node)
+                       {
+                               case Shell shell:
+                                       results = shell.Items;
+                                       break;
+                               case ShellItem item:
+                                       results = item.Items;
+                                       break;
+                               case ShellSection section:
+                                       results = section.Items;
+                                       break;
+                               case ShellContent content:
+                                       results = new object[0];
+                                       break;
+                               case GlobalRouteItem routeITem:
+                                       results = routeITem.Items;
+                                       break;
+                       }
+
+                       if (results == null)
+                               throw new ArgumentException($"{node}", nameof(node));
+
+                       foreach (var result in results)
+                               yield return result;
+
+                       if (node is GlobalRouteItem)
+                               yield break;
+
+                       var keys = Routing.GetRouteKeys();
+                       string route = GetRoute(node);
+                       for (var i = 0; i < keys.Length; i++)
+                       {
+                               var key = FormatUri(keys[i]);
+                               if (key.StartsWith("/") && !(node is Shell))
+                                       continue;
+
+                               var segments = key.Split(_pathSeparator, StringSplitOptions.RemoveEmptyEntries);
+
+                               if (segments[0] == route)
+                               {
+                                       yield return  new GlobalRouteItem(key, key);
+                               }
+                       }
+               }
+
+
+               internal class GlobalRouteItem
+               {
+                       readonly string _path;
+                       public GlobalRouteItem(string path, string sourceRoute)
+                       {
+                               _path = path;
+                               SourceRoute = sourceRoute;
+                       }
+
+                       public IEnumerable Items
+                       {
+                               get
+                               {
+                                       var segments = _path.Split(_pathSeparator, StringSplitOptions.RemoveEmptyEntries).ToList().Skip(1).ToList();
+
+                                       if (segments.Count == 0)
+                                               return new object[0];
+
+                                       var route = Routing.FormatRoute(segments);
+
+                                       return new[] { new GlobalRouteItem(route, SourceRoute) };
+                               }
+                       }
+
+                       public string Route
+                       {
+                               get
+                               {
+                                       var segments = _path.Split(_pathSeparator, StringSplitOptions.RemoveEmptyEntries);
+
+                                       if (segments.Length == 0)
+                                               return string.Empty;
+
+                                       return segments[0];
+                               }
+                       }
+
+                       public bool IsFinished
+                       {
+                               get
+                               {
+                                       var segments = _path.Split(_pathSeparator, StringSplitOptions.RemoveEmptyEntries).ToList().Skip(1).ToList();
+
+                                       if (segments.Count == 0)
+                                               return true;
+
+                                       return false;
+                               }
+                       }
+
+                       public string SourceRoute { get; }
+               }
+       }
+
+       /// <summary>
+       /// This attempts to locate the intended route trying to be navigated to
+       /// </summary>
+       internal class RouteRequestBuilder
+       {
+               readonly List<string> _globalRouteMatches = new List<string>();
+               readonly List<string> _matchedSegments = new List<string>();
+               readonly List<string> _fullSegments = new List<string>();
+               readonly string[] _allSegments = null;
+               readonly static string _uriSeparator = "/";
+
+               public Shell Shell { get; private set; }
+               public ShellItem Item { get; private set; }
+               public ShellSection Section { get; private set; }
+               public ShellContent Content { get; private set; }
+               public object LowestChild =>
+                       (object)Content ?? (object)Section ?? (object)Item ?? (object)Shell;
+
+               public RouteRequestBuilder(string shellSegment, string userSegment, object node, string[] allSegments)
+               {
+                       _allSegments = allSegments;
+                       if (node != null)
+                               AddMatch(shellSegment, userSegment, node);
+                       else
+                               AddGlobalRoute(userSegment, shellSegment);
+               }
+               public RouteRequestBuilder(RouteRequestBuilder builder)
+               {
+                       _allSegments = builder._allSegments;
+                       _matchedSegments.AddRange(builder._matchedSegments);
+                       _fullSegments.AddRange(builder._fullSegments);
+                       _globalRouteMatches.AddRange(builder._globalRouteMatches);
+                       Shell = builder.Shell;
+                       Item = builder.Item;
+                       Section = builder.Section;
+                       Content = builder.Content;
+               }
+
+               public void AddGlobalRoute(string routeName, string segment)
+               {
+                       _globalRouteMatches.Add(routeName);
+                       _fullSegments.Add(segment);
+                       _matchedSegments.Add(segment);
+               }
+
+               public void AddMatch(string shellSegment, string userSegment, object node)
+               {
+                       if (node == null)
+                               throw new ArgumentNullException(nameof(node));
+
+                       switch (node)
+                       {
+                               case ShellUriHandler.GlobalRouteItem globalRoute:
+                                       if(globalRoute.IsFinished)
+                                               _globalRouteMatches.Add(globalRoute.SourceRoute);
+                                       break;
+                               case Shell shell:
+                                       Shell = shell;
+                                       break;
+                               case ShellItem item:
+                                       Item = item;
+                                       break;
+                               case ShellSection section:
+                                       Section = section;
+
+                                       if (Item == null)
+                                       {
+                                               Item = Section.Parent as ShellItem;
+                                               _fullSegments.Add(Item.Route);
+                                       }
+
+                                       break;
+                               case ShellContent content:
+                                       Content = content;
+                                       if (Section == null)
+                                       {
+                                               Section = Content.Parent as ShellSection;
+                                               _fullSegments.Add(Section.Route);
+                                       }
+
+                                       if (Item == null)
+                                       {
+                                               Item = Section.Parent as ShellItem;
+                                               _fullSegments.Insert(0, Item.Route);
+                                       }
+
+                                       break;
+
+                       }
+
+                       // if shellSegment == userSegment it means the implicit route is part of the request
+                       if (!Routing.IsImplicit(shellSegment) || shellSegment == userSegment)
+                               _matchedSegments.Add(shellSegment);
+
+                       _fullSegments.Add(shellSegment);
+               }
+
+               public string NextSegment
+               {
+                       get
+                       {
+                               var nextMatch = _matchedSegments.Count;
+                               if (nextMatch >= _allSegments.Length)
+                                       return null;
+
+                               return _allSegments[nextMatch];
+                       }
+               }
+
+               public string RemainingPath
+               {
+                       get
+                       {
+                               var nextMatch = _matchedSegments.Count;
+                               if (nextMatch >= _allSegments.Length)
+                                       return null;
+
+                               return Routing.FormatRoute(String.Join("/", _allSegments.Skip(nextMatch)));
+                       }
+               }
+               public string[] RemainingSegments
+               {
+                       get
+                       {
+                               var nextMatch = _matchedSegments.Count;
+                               if (nextMatch >= _allSegments.Length)
+                                       return null;
+
+                               return _allSegments.Skip(nextMatch).ToArray();
+                       }
+               }
+
+               string MakeUriString(List<string> segments)
+               {
+                       if (segments[0].StartsWith("/") || segments[0].StartsWith("\\"))
+                               return String.Join(_uriSeparator, segments);
+
+                       return $"//{String.Join(_uriSeparator, segments)}";
+               }
+
+               public string PathNoImplicit => MakeUriString(_matchedSegments);
+               public string PathFull => MakeUriString(_fullSegments);
+
+               public bool IsFullMatch => _matchedSegments.Count == _allSegments.Length;
+               public List<string> GlobalRouteMatches => _globalRouteMatches;
+               public List<string> SegmentsMatched => _matchedSegments;
+
+       }
+
+
+
+       [DebuggerDisplay("RequestDefinition = {Request}, StackRequest = {StackRequest}")]
+       public class NavigationRequest
+       {
+               public enum WhatToDoWithTheStack
+               {
+                       ReplaceIt,
+                       PushToIt
+               }
+
+               public NavigationRequest(RequestDefinition definition, WhatToDoWithTheStack stackRequest, string query, string fragment)
+               {
+                       StackRequest = stackRequest;
+                       Query = query;
+                       Fragment = fragment;
+                       Request = definition;
+               }
+
+               public WhatToDoWithTheStack StackRequest { get; }
+               public string Query { get; }
+               public string Fragment { get; }
+               public RequestDefinition Request { get; }
+       }
+
+
+       [DebuggerDisplay("Full = {FullUri}, Short = {ShortUri}")]
+       public class RequestDefinition
+       {
+               public RequestDefinition(Uri fullUri, Uri shortUri, ShellItem item, ShellSection section, ShellContent content, List<string> 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<string> globalRoutes) :
+                       this(new Uri(fullUri, UriKind.Absolute), new Uri(shortUri, 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; }
+               public List<string> GlobalRoutes { get; }
+       }
+
+
+}
index 78cf49b..9bd3d8b 100644 (file)
@@ -1,5 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.Xamarin.Forms.Sandbox" android:installLocation="auto">
        <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="28" />
-       <application android:label="Forms Sandbox"></application>
+       <application android:label="Forms Sandbox">
+    <uses-library android:name="org.apache.http.legacy" android:required="false" />
+  </application>
 </manifest>
\ No newline at end of file
index 545e4e8..bcae908 100644 (file)
@@ -30,7 +30,6 @@
     <WarningLevel>4</WarningLevel>
     <AndroidLinkMode>None</AndroidLinkMode>
     <AndroidDexTool Condition=" '$(MSBuildAssemblyVersion)' != '15.0'">d8</AndroidDexTool>
-    <AndroidEnableDesugar Condition=" '$(MSBuildAssemblyVersion)' != '15.0'">true</AndroidEnableDesugar>
     <AndroidEnableMultiDex Condition=" '$(MSBuildAssemblyVersion)' == '15.0'">true</AndroidEnableMultiDex>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
@@ -42,8 +41,9 @@
     <WarningLevel>4</WarningLevel>
     <AndroidManagedSymbols>true</AndroidManagedSymbols>
     <AndroidUseSharedRuntime>false</AndroidUseSharedRuntime>
-    <EnableProguard>true</EnableProguard>
     <AndroidLinkMode>Full</AndroidLinkMode>
+    <AndroidDexTool>d8</AndroidDexTool>
+    <AndroidLinkTool>r8</AndroidLinkTool>
   </PropertyGroup>
   <ItemGroup>
     <Reference Include="Mono.Android" />
index c105aa2..c610023 100644 (file)
@@ -2,7 +2,7 @@
 <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
              xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
              x:Class="Xamarin.Forms.Sandbox.MainPage"
-             xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core" 
+             xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
              ios:Page.UseSafeArea="true">
     <ContentPage.Content>
         <StackLayout>