diff --git a/Xamarin.Forms.Core.UnitTests/ShellTestBase.cs b/Xamarin.Forms.Core.UnitTests/ShellTestBase.cs
new file mode 100644
index 00000000000..c8cb0627dc7
--- /dev/null
+++ b/Xamarin.Forms.Core.UnitTests/ShellTestBase.cs
@@ -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;
+ }
+
+ }
+}
diff --git a/Xamarin.Forms.Core.UnitTests/ShellTests.cs b/Xamarin.Forms.Core.UnitTests/ShellTests.cs
index e4ab03cfe1f..84cfcb15e0f 100644
--- a/Xamarin.Forms.Core.UnitTests/ShellTests.cs
+++ b/Xamarin.Forms.Core.UnitTests/ShellTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Xamarin.Forms.Internals;
@@ -6,15 +7,8 @@
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 @@ public void CurrentItemDoesNotChangeOnSecondAdd()
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()
{
@@ -127,6 +101,37 @@ public void SimpleGoTo()
Assert.That(shell.CurrentState.Location.ToString(), Is.EqualTo("app:///s/two/tabfour/content/"));
}
+ [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()
{
@@ -165,6 +170,8 @@ public async Task RelativeGoTo()
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 @@ public async Task RelativeGoTo()
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 @@ public void FlyoutHeaderProjection()
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
index 00000000000..c6fc6435567
--- /dev/null
+++ b/Xamarin.Forms.Core.UnitTests/ShellUriHandlerTests.cs
@@ -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));
+ }
+
+ }
+ }
+ }
+}
diff --git a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj
index 048e5cfd920..f9ee5734530 100644
--- a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj
+++ b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj
@@ -74,6 +74,8 @@
+
+
diff --git a/Xamarin.Forms.Core/Routing.cs b/Xamarin.Forms.Core/Routing.cs
index e0e84bc28b9..6a54744b060 100644
--- a/Xamarin.Forms.Core/Routing.cs
+++ b/Xamarin.Forms.Core/Routing.cs
@@ -11,28 +11,30 @@ public static class Routing
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 @@ static object CreateDefaultRoute(BindableObject bindable)
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 @@ public static string GetRoute(Element obj)
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 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 @@ public static void SetRoute(Element obj, string value)
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
diff --git a/Xamarin.Forms.Core/Shell/BaseShellItem.cs b/Xamarin.Forms.Core/Shell/BaseShellItem.cs
index 57691a4e8bb..8311d83f5e9 100644
--- a/Xamarin.Forms.Core/Shell/BaseShellItem.cs
+++ b/Xamarin.Forms.Core/Shell/BaseShellItem.cs
@@ -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
diff --git a/Xamarin.Forms.Core/Shell/IShellController.cs b/Xamarin.Forms.Core/Shell/IShellController.cs
index 8179d84a4e2..9d62c0578cb 100644
--- a/Xamarin.Forms.Core/Shell/IShellController.cs
+++ b/Xamarin.Forms.Core/Shell/IShellController.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Threading.Tasks;
namespace Xamarin.Forms
{
@@ -35,6 +36,8 @@ public interface IShellController : IPageController
void OnFlyoutItemSelected(Element element);
+ Task OnFlyoutItemSelectedAsync(Element element);
+
bool ProposeNavigation(ShellNavigationSource source, ShellItem item, ShellSection shellSection, ShellContent shellContent, IReadOnlyList stack, bool canCancel);
bool RemoveAppearanceObserver(IAppearanceObserver observer);
diff --git a/Xamarin.Forms.Core/Shell/IShellItemController.cs b/Xamarin.Forms.Core/Shell/IShellItemController.cs
index 6713cc37965..0ca88393485 100644
--- a/Xamarin.Forms.Core/Shell/IShellItemController.cs
+++ b/Xamarin.Forms.Core/Shell/IShellItemController.cs
@@ -6,7 +6,7 @@ namespace Xamarin.Forms
{
public interface IShellItemController : IElementController
{
- Task GoToPart(List parts, Dictionary queryData);
+ Task GoToPart(NavigationRequest navigationRequest, Dictionary queryData);
bool ProposeSection(ShellSection shellSection, bool setValue = true);
}
diff --git a/Xamarin.Forms.Core/Shell/IShellSectionController.cs b/Xamarin.Forms.Core/Shell/IShellSectionController.cs
index 9889e17cb3c..6e5104097f6 100644
--- a/Xamarin.Forms.Core/Shell/IShellSectionController.cs
+++ b/Xamarin.Forms.Core/Shell/IShellSectionController.cs
@@ -15,7 +15,7 @@ public interface IShellSectionController : IElementController
void AddDisplayedPageObserver(object observer, Action callback);
- Task GoToPart(List parts, Dictionary queryData);
+ Task GoToPart(NavigationRequest request, Dictionary queryData);
bool RemoveContentInsetObserver(IShellContentInsetObserver observer);
diff --git a/Xamarin.Forms.Core/Shell/Shell.cs b/Xamarin.Forms.Core/Shell/Shell.cs
index 52c128d1e90..f24daaf7e9b 100644
--- a/Xamarin.Forms.Core/Shell/Shell.cs
+++ b/Xamarin.Forms.Core/Shell/Shell.cs
@@ -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 @@ static void OnFlyoutBehaviorChanged(BindableObject bindable, object oldValue, ob
{
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);
@@ -265,35 +267,41 @@ ShellNavigationState IShellController.GetNavigationState(ShellItem shellItem, Sh
=> GetNavigationState(shellItem, shellSection, shellContent, includeStack ? shellSection.Stack.ToList() : null);
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 @@ void IShellController.UpdateCurrentState(ShellNavigationSource source)
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, @"(?.+?)(\?(?.+?))?(#(?.+))?$").Groups;
- var url = parseUri["u"].Value;
- var query = parseUri["q"].Value;
- var fragment = parseUri["f"].Value;
+ List BuildAllTheRoutes()
+ {
+ List routes = new List();
+ // todo make better maybe
- Element item = CurrentItem;
- var list = new List();
- 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()));
+ }
+ }
+ }
+
+ return routes;
}
public async Task GoToAsync(ShellNavigationState state, bool animate = true)
@@ -388,9 +392,9 @@ public async Task GoToAsync(ShellNavigationState state, bool animate = true)
_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 @@ public async Task GoToAsync(ShellNavigationState state, bool animate = true)
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 @@ internal static void ApplyQueryAttributes(Element element, IDictionary(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 @@ ShellNavigationState GetNavigationState(ShellItem shellItem, ShellSection shellS
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 @@ ShellNavigationState GetNavigationState(ShellItem shellItem, ShellSection shellS
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 @@ ShellNavigationState GetNavigationState(ShellItem shellItem, ShellSection shellS
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 @@ public Shell() : this(true)
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 @@ internal Shell(bool checkFlag)
[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 @@ public ImageSource FlyoutIcon
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 @@ public DataTemplate GroupHeaderTemplate {
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 @@ public string Route {
public string RouteScheme { get; set; } = "app";
- View FlyoutHeaderView {
+ View FlyoutHeaderView
+ {
get => _flyoutHeaderView;
- set {
+ set
+ {
if (_flyoutHeaderView == value)
return;
@@ -816,7 +834,8 @@ protected virtual void OnNavigated(ShellNavigatedEventArgs args)
{
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 @@ bool ProposeNavigation(ShellNavigationSource source, ShellNavigationState propos
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 @@ void IPropertyPropagationController.PropagatePropertyChanged(string propertyName
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 GetNavigationStack() => SectionProxy.NavigationStack;
+
+ protected override void OnInsertPageBefore(Page page, Page before) => SectionProxy.InsertPageBefore(page, before);
+
+ protected override Task 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);
+ }
}
}
diff --git a/Xamarin.Forms.Core/Shell/ShellItem.cs b/Xamarin.Forms.Core/Shell/ShellItem.cs
index a8d0a7cc7b1..955b2fc99df 100644
--- a/Xamarin.Forms.Core/Shell/ShellItem.cs
+++ b/Xamarin.Forms.Core/Shell/ShellItem.cs
@@ -24,31 +24,19 @@ public class ShellItem : ShellGroupItem, IShellItemController, IElementConfigura
#region IShellItemController
- Task IShellItemController.GoToPart(List parts, Dictionary queryData)
+ Task IShellItemController.GoToPart(NavigationRequest request, Dictionary 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 @@ internal void SendStructureChanged()
}
}
-#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 @@ public static implicit operator ShellItem(ShellSection shellSection)
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) };
diff --git a/Xamarin.Forms.Core/Shell/ShellNavigationState.cs b/Xamarin.Forms.Core/Shell/ShellNavigationState.cs
index bd9388fd03e..93e80b2e5ff 100644
--- a/Xamarin.Forms.Core/Shell/ShellNavigationState.cs
+++ b/Xamarin.Forms.Core/Shell/ShellNavigationState.cs
@@ -1,7 +1,10 @@
using System;
+using System.Diagnostics;
namespace Xamarin.Forms
{
+
+ [DebuggerDisplay("Location = {Location}")]
public class ShellNavigationState
{
public Uri Location { get; set; }
diff --git a/Xamarin.Forms.Core/Shell/ShellSection.cs b/Xamarin.Forms.Core/Shell/ShellSection.cs
index 0d05786f00a..16ac997613f 100644
--- a/Xamarin.Forms.Core/Shell/ShellSection.cs
+++ b/Xamarin.Forms.Core/Shell/ShellSection.cs
@@ -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 @@ void IShellSectionController.AddDisplayedPageObserver(object observer, Action parts, Dictionary queryData)
+ Task IShellSectionController.GoToPart(NavigationRequest request, Dictionary 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 @@ Page DisplayedPage
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 @@ public static implicit operator ShellSection(ShellContent shellContent)
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
index 00000000000..d9be1213f07
--- /dev/null
+++ b/Xamarin.Forms.Core/Shell/ShellUriHandler.cs
@@ -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(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 GenerateRoutePaths(Shell shell, Uri request)
+ {
+ request = FormatUri(request);
+ return GenerateRoutePaths(shell, request, request);
+ }
+
+ internal static List 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 possibleRoutePaths = new List();
+ 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 { 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 pureRoutesMatch = new List();
+ List pureGlobalRoutesMatch = new List();
+
+ SearchPath(currentLocation.LowestChild, null, segments, pureRoutesMatch, 0);
+ SearchPath(currentLocation.LowestChild, null, segments, pureGlobalRoutesMatch, 0, ignoreGlobalRoutes: false);
+ pureRoutesMatch = GetBestMatches(pureRoutesMatch);
+ pureGlobalRoutesMatch = GetBestMatches(pureGlobalRoutesMatch);
+
+ 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 { 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 { 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 pureGlobalRoutesMatch = new List();
+ while (nodeLocation.Shell != null && pureGlobalRoutesMatch.Count == 0)
+ {
+ SearchPath(nodeLocation.LowestChild, null, possibleRoutePath.RemainingSegments, pureGlobalRoutesMatch, 0, ignoreGlobalRoutes: false);
+ nodeLocation.Pop();
+ }
+
+ // nothing found or too many things found
+ if (pureGlobalRoutesMatch.Count != 1)
+ {
+ break;
+ }
+
+
+ for (var i = 0; i < pureGlobalRoutesMatch[0].GlobalRouteMatches.Count; i++)
+ {
+ var match = pureGlobalRoutesMatch[0];
+ possibleRoutePath.AddGlobalRoute(match.GlobalRouteMatches[i], match.SegmentsMatched[i]);
+ }
+ }
+ }
+ }
+
+ possibleRoutePaths = GetBestMatches(possibleRoutePaths);
+ return possibleRoutePaths;
+ }
+
+ internal static List GetBestMatches(List possibleRoutePaths)
+ {
+ List bestMatches = new List();
+ 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 paths = new List();
+ 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 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; }
+ }
+ }
+
+ ///
+ /// This attempts to locate the intended route trying to be navigated to
+ ///
+ internal class RouteRequestBuilder
+ {
+ readonly List _globalRouteMatches = new List();
+ readonly List _matchedSegments = new List();
+ readonly List _fullSegments = new List();
+ 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 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 GlobalRouteMatches => _globalRouteMatches;
+ public List 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 globalRoutes)
+ {
+ FullUri = fullUri;
+ ShortUri = shortUri;
+ Item = item;
+ Section = section;
+ Content = content;
+ GlobalRoutes = globalRoutes;
+ }
+
+ public RequestDefinition(string fullUri, string shortUri, ShellItem item, ShellSection section, ShellContent content, List globalRoutes) :
+ this(new Uri(fullUri, UriKind.Absolute), new Uri(shortUri, UriKind.Absolute), item, section, content, globalRoutes)
+ {
+ }
+
+ public Uri FullUri { get; }
+ public Uri ShortUri { get; }
+ public ShellItem Item { get; }
+ public ShellSection Section { get; }
+ public ShellContent Content { get; }
+ public List GlobalRoutes { get; }
+ }
+
+
+}
diff --git a/Xamarin.Forms.Sandbox.Android/Properties/AndroidManifest.xml b/Xamarin.Forms.Sandbox.Android/Properties/AndroidManifest.xml
index 78cf49b173f..9bd3d8bc926 100644
--- a/Xamarin.Forms.Sandbox.Android/Properties/AndroidManifest.xml
+++ b/Xamarin.Forms.Sandbox.Android/Properties/AndroidManifest.xml
@@ -1,5 +1,7 @@
-
+
+
+
\ No newline at end of file
diff --git a/Xamarin.Forms.Sandbox.Android/Xamarin.Forms.Sandbox.Android.csproj b/Xamarin.Forms.Sandbox.Android/Xamarin.Forms.Sandbox.Android.csproj
index 545e4e86442..bcae908dd3a 100644
--- a/Xamarin.Forms.Sandbox.Android/Xamarin.Forms.Sandbox.Android.csproj
+++ b/Xamarin.Forms.Sandbox.Android/Xamarin.Forms.Sandbox.Android.csproj
@@ -30,7 +30,6 @@
4
None
d8
- true
true
@@ -42,8 +41,9 @@
4
true
false
- true
Full
+ d8
+ r8
diff --git a/Xamarin.Forms.Sandbox/MainPage.xaml b/Xamarin.Forms.Sandbox/MainPage.xaml
index c105aa29d58..c610023dd42 100644
--- a/Xamarin.Forms.Sandbox/MainPage.xaml
+++ b/Xamarin.Forms.Sandbox/MainPage.xaml
@@ -2,7 +2,7 @@