From 1edd6c655f7266406ff261cf4f91d3b3d6256db4 Mon Sep 17 00:00:00 2001 From: ShaneN Date: Sat, 15 Jun 2019 11:51:31 -0600 Subject: [PATCH] mvvm start --- .../ShellNavigationTests.cs | 44 +++ .../ShellUriHandlerTests.cs | 46 ++- .../Xamarin.Forms.Core.UnitTests.csproj | 1 + .../Shell/RouteRequestBuilder.cs | 157 ++++++++ Xamarin.Forms.Core/Shell/Shell.cs | 211 +++++++---- Xamarin.Forms.Core/Shell/ShellContent.cs | 17 +- Xamarin.Forms.Core/Shell/ShellItem.cs | 20 +- .../Shell/ShellNavigationService.cs | 348 +++++++++++++++++ Xamarin.Forms.Core/Shell/ShellSection.cs | 72 ++-- Xamarin.Forms.Core/Shell/ShellState.cs | 111 ++++++ Xamarin.Forms.Core/Shell/ShellUriHandler.cs | 351 ++++++------------ 11 files changed, 989 insertions(+), 389 deletions(-) create mode 100644 Xamarin.Forms.Core.UnitTests/ShellNavigationTests.cs create mode 100644 Xamarin.Forms.Core/Shell/RouteRequestBuilder.cs create mode 100644 Xamarin.Forms.Core/Shell/ShellNavigationService.cs create mode 100644 Xamarin.Forms.Core/Shell/ShellState.cs diff --git a/Xamarin.Forms.Core.UnitTests/ShellNavigationTests.cs b/Xamarin.Forms.Core.UnitTests/ShellNavigationTests.cs new file mode 100644 index 00000000000..6720f1c2941 --- /dev/null +++ b/Xamarin.Forms.Core.UnitTests/ShellNavigationTests.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using NUnit.Framework; +using Xamarin.Forms.Internals; + +namespace Xamarin.Forms.Core.UnitTests +{ + [TestFixture] + public class ShellNavigationTests : ShellTestBase + { + [Test] + public async Task NavigationWithQueryStringWhenPageMatchesBindingContext() + { + var shell = new Shell(); + + var one = new ShellItem { Route = "one" }; + var two = new ShellItem { Route = "two" }; + + var tabone = MakeSimpleShellSection("tabone", "content"); + var tabfour = MakeSimpleShellSection("tabfour", "content", null); + + one.Items.Add(tabone); + two.Items.Add(tabfour); + + shell.Items.Add(one); + shell.Items.Add(two); + + ShellTestPage pagetoTest = new ShellTestPage(); + await shell.GoToAsync(new ShellNavigationState($"//two/tabfour/content?{nameof(ShellTestPage.SomeQueryParameter)}=1234&two.rabbit=monkey")); + two.CurrentItem.CurrentItem.ContentTemplate = new DataTemplate(() => + { + pagetoTest = new ShellTestPage(); + pagetoTest.BindingContext = pagetoTest; + return pagetoTest; + }); + + + var page = (two.CurrentItem.CurrentItem as IShellContentController).GetOrCreateContent(); + Assert.AreEqual("1234", (page as ShellTestPage).SomeQueryParameter); + + } + } +} diff --git a/Xamarin.Forms.Core.UnitTests/ShellUriHandlerTests.cs b/Xamarin.Forms.Core.UnitTests/ShellUriHandlerTests.cs index 4129c7124df..3010ae2fc93 100644 --- a/Xamarin.Forms.Core.UnitTests/ShellUriHandlerTests.cs +++ b/Xamarin.Forms.Core.UnitTests/ShellUriHandlerTests.cs @@ -87,7 +87,7 @@ public async Task GlobalRegisterAbsoluteMatching() Routing.RegisterRoute("/seg1/seg2/seg3", typeof(object)); var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("/seg1/seg2/seg3")); - Assert.AreEqual("app://shell/IMPL_shell/seg1/seg2/seg3", request.Request.FullUri.ToString()); + Assert.AreEqual("app://shell/IMPL_shell/seg1/seg2/seg3", request.CurrentRoute.FullUriWithImplicit.ToString()); } [Test] @@ -120,8 +120,8 @@ public async Task ShellRelativeGlobalRegistration() await shell.GoToAsync("//item1/section1/rootlevelcontent1"); var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("section1/edit"), true); - Assert.AreEqual(1, request.Request.GlobalRoutes.Count); - Assert.AreEqual("item1/section1/edit", request.Request.GlobalRoutes.First()); + Assert.AreEqual(4, request.CurrentRoute.PathParts.Count); + Assert.AreEqual("item1/section1/edit", request.CurrentRoute.PathParts[3].Path); } [Test] @@ -137,9 +137,9 @@ public async Task ShellSectionWithRelativeEditUpOneLevelMultiple() 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()); + Assert.AreEqual(5, request.CurrentRoute.PathParts.Count); + Assert.AreEqual("section1/add", request.CurrentRoute.PathParts[3].Path); + Assert.AreEqual("section1/edit", request.CurrentRoute.PathParts[4].Path); } [Test] @@ -154,8 +154,8 @@ public async Task ShellSectionWithGlobalRouteAbsolute() var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("//rootlevelcontent1/edit")); - Assert.AreEqual(1, request.Request.GlobalRoutes.Count); - Assert.AreEqual("edit", request.Request.GlobalRoutes.First()); + Assert.AreEqual(4, request.CurrentRoute.PathParts.Count); + Assert.AreEqual("edit", request.CurrentRoute.PathParts[3].Path); } [Test] @@ -171,8 +171,8 @@ public async Task ShellSectionWithGlobalRouteRelative() 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()); + Assert.AreEqual(4, request.CurrentRoute.PathParts.Count); + Assert.AreEqual("edit", request.CurrentRoute.PathParts[3].Path); } @@ -189,7 +189,7 @@ public async Task ShellSectionWithRelativeEditUpOneLevel() await shell.GoToAsync("//rootlevelcontent1"); var request = ShellUriHandler.GetNavigationRequest(shell, CreateUri("edit"), true); - Assert.AreEqual("section1/edit", request.Request.GlobalRoutes.First()); + Assert.AreEqual("section1/edit", request.CurrentRoute.PathParts[3].Path); } [Test] @@ -205,6 +205,7 @@ public async Task ShellSectionWithRelativeEdit() await shell.GoToAsync("//rootlevelcontent1"); var location = shell.CurrentState.FullLocation; + await shell.GoToAsync("edit", false, true); Assert.AreEqual(editShellContent, shell.CurrentItem.CurrentItem.CurrentItem); @@ -275,6 +276,25 @@ public async Task ShellItemAndContentOnly() Assert.IsTrue(builders.Contains("//item2/rootlevelcontent")); } + [Test] + public async Task PoppingRemovesRouteFromStack() + { + var shell = new Shell(); + var item1 = CreateShellItem(asImplicit: true, shellSectionRoute: "domestic", shellContentRoute: "cats", shellItemRoute: "animals"); + + shell.Items.Add(item1); + + Routing.RegisterRoute("catdetails", typeof(ContentPage)); + await shell.GoToAsync($"catdetails"); + await shell.Navigation.PopAsync(); + await shell.GoToAsync($"catdetails"); + + Assert.AreEqual( + "//animals/domestic/cats/catdetails", + shell.CurrentState.FullLocation.ToString() + ); + } + [Test] public async Task AbsoluteNavigationToRelativeWithGlobal() @@ -350,12 +370,12 @@ public async Task ConvertToStandardFormat() foreach(var uri in TestUris) { - Assert.AreEqual(new Uri("app://shell/IMPL_shell/path"), ShellUriHandler.ConvertToStandardFormat(shell, uri), $"{uri}"); + Assert.AreEqual(new Uri("app://shell/IMPL_shell/path"), ShellUriHandler.ConvertToStandardFormat(uri), $"{uri}"); if(!uri.IsAbsoluteUri) { var reverse = new Uri(uri.OriginalString.Replace("/", "\\"), UriKind.Relative); - Assert.AreEqual(new Uri("app://shell/IMPL_shell/path"), ShellUriHandler.ConvertToStandardFormat(shell, reverse)); + Assert.AreEqual(new Uri("app://shell/IMPL_shell/path"), ShellUriHandler.ConvertToStandardFormat(reverse)); } } diff --git a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj index fd38f20a0b8..3bb01acac1d 100644 --- a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj +++ b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj @@ -74,6 +74,7 @@ + diff --git a/Xamarin.Forms.Core/Shell/RouteRequestBuilder.cs b/Xamarin.Forms.Core/Shell/RouteRequestBuilder.cs new file mode 100644 index 00000000000..16f500aa761 --- /dev/null +++ b/Xamarin.Forms.Core/Shell/RouteRequestBuilder.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Xamarin.Forms +{ + /// + /// 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(_uriSeparator, _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(_uriSeparator, StringComparison.Ordinal) || segments[0].StartsWith("\\", StringComparison.Ordinal)) + 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; + } +} diff --git a/Xamarin.Forms.Core/Shell/Shell.cs b/Xamarin.Forms.Core/Shell/Shell.cs index 595704ad03e..2a6868b9405 100644 --- a/Xamarin.Forms.Core/Shell/Shell.cs +++ b/Xamarin.Forms.Core/Shell/Shell.cs @@ -343,38 +343,17 @@ void IShellController.UpdateCurrentState(ShellNavigationSource source) public static Shell Current => Application.Current?.MainPage as Shell; - List BuildAllTheRoutes() + public Task GoToAsync(ShellNavigationState state, bool animate = true) { - List routes = new List(); - // todo make better maybe - - for (var i = 0; i < Items.Count; i++) - { - var item = Items[i]; - - for (var j = 0; j < item.Items.Count; j++) - { - var section = item.Items[j]; - - for (var k = 0; k < section.Items.Count; k++) - { - var content = section.Items[k]; - - string longUri = $"{RouteScheme}://{RouteHost}/{Routing.GetRoute(this)}/{Routing.GetRoute(item)}/{Routing.GetRoute(section)}/{Routing.GetRoute(content)}"; - - longUri = longUri.TrimEnd('/'); - - routes.Add(new RequestDefinition(longUri, item, section, content, new List())); - } - } - } - - return routes; + return GoToAsync(state, animate, false); } - public Task GoToAsync(ShellNavigationState state, bool animate = true) + internal ShellRouteState RouteState { - return GoToAsync(state, animate, false); + get + { + return new ShellRouteState(this); + } } internal async Task GoToAsync(ShellNavigationState state, bool animate, bool enableRelativeShellRoutes) @@ -386,70 +365,100 @@ internal async Task GoToAsync(ShellNavigationState state, bool animate, bool ena _accumulateNavigatedEvents = true; - var navigationRequest = ShellUriHandler.GetNavigationRequest(this, state.FullLocation, enableRelativeShellRoutes); - var uri = navigationRequest.Request.FullUri; - var queryString = navigationRequest.Query; - var queryData = ParseQueryString(queryString); - var path = uri.AbsolutePath; + try + { - path = path.TrimEnd('/'); + ShellRouteState navigationRequest = null; - var parts = path.Substring(1).Split('/').ToList(); + if (!enableRelativeShellRoutes) + navigationRequest = await ShellUriParser.ParseAsync(new ShellUriParserArgs(this, state.FullLocation)); + else // this path is really for ui testing only + navigationRequest = ShellUriHandler.GetNavigationRequest(this, state.FullLocation, enableRelativeShellRoutes); - if (parts.Count < 2) - throw new InvalidOperationException("Path must be at least 2 items long in Shell navigation"); + navigationRequest = await ShellNavigationRequest.NavigatingToAsync(new ShellNavigationArgs(this, navigationRequest)); - var shellRoute = parts[0]; + if (navigationRequest == null || navigationRequest == this.RouteState) + { + return; + } - var expectedShellRoute = Routing.GetRoute(this) ?? string.Empty; - if (expectedShellRoute != shellRoute) - throw new NotImplementedException(); - else - parts.RemoveAt(0); + var currentRoute = navigationRequest.CurrentRoute; + var pathParts = currentRoute.PathParts; + ApplyQueryAttributes(this, currentRoute.NavigationParameters, false); - ApplyQueryAttributes(this, queryData, false); + // Right now this is a rigid structure but later down the road this will have more variations + // for example it might only be a ShellContent + ShellItem shellItem = (ShellItem)pathParts[0].ShellPart; + ShellSection shellSection = (ShellSection)pathParts[1].ShellPart; + ShellContent shellContent = (ShellContent)pathParts[2].ShellPart; + bool shellSectionChanged = false; + bool shellItemChanged = false; + bool shellContentChanged = false; - var shellItem = navigationRequest.Request.Item; - if (shellItem != null) - { - ApplyQueryAttributes(shellItem, queryData, navigationRequest.Request.Section == null); + ShellApplyParameters.ApplyParameters(new ShellLifecycleArgs(this, null, currentRoute)); + ShellApplyParameters.ApplyParameters(new ShellLifecycleArgs(shellItem, pathParts[0], currentRoute)); + ShellApplyParameters.ApplyParameters(new ShellLifecycleArgs(shellSection, pathParts[1], currentRoute)); + ShellApplyParameters.ApplyParameters(new ShellLifecycleArgs(shellContent, pathParts[2], currentRoute)); if (CurrentItem != shellItem) + { SetValueFromRenderer(CurrentItemProperty, shellItem); + shellItemChanged = true; + } - parts.RemoveAt(0); - - if (parts.Count > 0) - await shellItem.GoToPart(navigationRequest, queryData); - } - else - { - await CurrentItem.CurrentItem.GoToAsync(navigationRequest, queryData, animate); - } + if (shellItem.CurrentItem != shellSection) + { + shellItem.SetValueFromRenderer(ShellItem.CurrentItemProperty, shellSection); + shellSectionChanged = true; + } - //if (Routing.CompareWithRegisteredRoutes(shellItemRoute)) - //{ - // var shellItem = ShellItem.GetShellItemFromRouteName(shellItemRoute); + if (shellSection.CurrentItem != shellContent) + { + shellSection.SetValueFromRenderer(ShellSection.CurrentItemProperty, shellContent); + shellContentChanged = true; + } - // ApplyQueryAttributes(shellItem, queryData, parts.Count == 1); + if (shellItemChanged) + await ShellPartAppearing.AppearingAsync(new ShellLifecycleArgs(shellItem, pathParts[0], currentRoute)); - // if (CurrentItem != shellItem) - // SetValueFromRenderer(CurrentItemProperty, shellItem); + if (shellSectionChanged) + await ShellPartAppearing.AppearingAsync(new ShellLifecycleArgs(shellSection, pathParts[1], currentRoute)); - // if (parts.Count > 0) - // await ((IShellItemController)shellItem).GoToPart(parts, queryData); - //} + if (shellContentChanged) + await ShellPartAppearing.AppearingAsync(new ShellLifecycleArgs(shellContent, pathParts[2], currentRoute)); - _accumulateNavigatedEvents = false; + if (shellSectionChanged) + { + if (pathParts.Count > 3) + { + // TODO get rid of this hack and fix so if there's a stack the current page doesn't display + Device.BeginInvokeOnMainThread(async () => + { + await shellSection.GoToAsync(navigationRequest, false); + }); + } + } + else + { + await shellSection.GoToAsync(navigationRequest, animate); + } + } + finally + { + _accumulateNavigatedEvents = false; + } // this can be null in the event that no navigation actually took place! if (_accumulatedEvent != null) OnNavigated(_accumulatedEvent); + + return; } - internal static void ApplyQueryAttributes(Element element, IDictionary query, bool isLastItem) + // TODO cleanup duplication between here and GetNavigationParameters + internal static void ApplyQueryAttributes(Element element, IDictionary navigationParameters, bool isLastItem) { - if (query.Count == 0) + if (navigationParameters == null || navigationParameters.Count == 0) return; string prefix = ""; @@ -462,7 +471,44 @@ internal static void ApplyQueryAttributes(Element element, IDictionary GetNavigationParameters(Element element, string queryString, bool isLastItem) + { + var query = ParseQueryString(queryString); + + if (query.Count == 0) + return new Dictionary(); + + string prefix = ""; + if (!isLastItem) + { + var route = Routing.GetRoute(element); + if (string.IsNullOrEmpty(route) || route.StartsWith(Routing.ImplicitPrefix, StringComparison.Ordinal)) + return new Dictionary(); + prefix = route + "."; + } + + //if the lastItem is implicitly wrapped, get the actual ShellContent + else if (isLastItem) { if (element is ShellItem shellitem && shellitem.Items.FirstOrDefault() is ShellSection section) element = section; @@ -488,9 +534,11 @@ internal static void ApplyQueryAttributes(Element element, IDictionary(); } ShellNavigationState GetNavigationState(ShellItem shellItem, ShellSection shellSection, ShellContent shellContent, IReadOnlyList sectionStack) @@ -572,7 +620,7 @@ public Shell() { Navigation = new NavigationImpl(this); ((INotifyCollectionChanged)Items).CollectionChanged += (s, e) => SendStructureChanged(); - Route = Routing.GenerateImplicitRoute("shell"); + Route = ShellUriHandler.Route; } public event EventHandler Navigated; @@ -649,9 +697,9 @@ internal string Route set => Routing.SetRoute(this, value); } - internal string RouteHost { get; set; } = "shell"; + internal string RouteHost => ShellUriHandler.RouteHost; - internal string RouteScheme { get; set; } = "app"; + internal string RouteScheme => ShellUriHandler.RouteScheme; View FlyoutHeaderView { @@ -724,7 +772,7 @@ void IncrementGroup() } else { - if(!(shellSection.Parent is TabBar)) + if (!(shellSection.Parent is TabBar)) currentGroup.Add(shellSection); // If we have only a single child we will also show the items menu items @@ -811,6 +859,7 @@ protected virtual void OnNavigated(ShellNavigatedEventArgs args) } ShellNavigationState _lastNavigating; + protected virtual void OnNavigating(ShellNavigatingEventArgs args) { Navigating?.Invoke(this, args); @@ -1065,6 +1114,16 @@ void IPropertyPropagationController.PropagatePropertyChanged(string propertyName PropertyPropagationExtensions.PropagatePropertyChanged(propertyName, this, new[] { FlyoutHeaderView }); } + #region Navigation Interfaces + + IShellUriParser ShellUriParser => DependencyService.Get(); + IShellApplyParameters ShellApplyParameters => DependencyService.Get(); + IShellPartAppearing ShellPartAppearing => DependencyService.Get(); + IShellPartAppeared ShellPartAppeared => DependencyService.Get(); + IShellNavigationRequest ShellNavigationRequest => DependencyService.Get(); + + #endregion + class NavigationImpl : NavigationProxy { readonly Shell _shell; diff --git a/Xamarin.Forms.Core/Shell/ShellContent.cs b/Xamarin.Forms.Core/Shell/ShellContent.cs index 0ca9d55e86a..8c48487ea8a 100644 --- a/Xamarin.Forms.Core/Shell/ShellContent.cs +++ b/Xamarin.Forms.Core/Shell/ShellContent.cs @@ -47,17 +47,7 @@ Page IShellContentController.GetOrCreateContent() var template = ContentTemplate; var content = Content; - Page result = null; - if (template == null) - { - if (content is Page page) - result = page; - } - else - { - result = ContentCache ?? (Page)template.CreateContent(content, this); - ContentCache = result; - } + Page result = ContentCache ?? ShellContentCreator.Create(new ShellContentCreateArgs(this)); if (result != null && result.Parent != this) OnChildAdded(result); @@ -66,6 +56,7 @@ Page IShellContentController.GetOrCreateContent() throw new InvalidOperationException($"No Content found for {nameof(ShellContent)}, Title:{Title}, Route {Route}"); if (_delayedQueryParams != null && result != null) { + // TODO rework this to all be internal to Shell Navigation Service ApplyQueryAttributes(result, _delayedQueryParams); _delayedQueryParams = null; } @@ -73,6 +64,10 @@ Page IShellContentController.GetOrCreateContent() return result; } + #region Navigation Interfaces + IShellContentCreator ShellContentCreator => DependencyService.Get(); + #endregion + void IShellContentController.RecyclePage(Page page) { } diff --git a/Xamarin.Forms.Core/Shell/ShellItem.cs b/Xamarin.Forms.Core/Shell/ShellItem.cs index 0f557d0fe6a..1efa2a788b7 100644 --- a/Xamarin.Forms.Core/Shell/ShellItem.cs +++ b/Xamarin.Forms.Core/Shell/ShellItem.cs @@ -41,20 +41,20 @@ public class ShellItem : ShellGroupItem, IShellItemController, IElementConfigura #region IShellItemController - internal Task GoToPart(NavigationRequest request, Dictionary queryData) - { - var shellSection = request.Request.Section; + //internal Task GoToPart(NavigationRequest request, Dictionary queryData) + //{ + // var shellSection = request.Request.Section; - if (shellSection == null) - return Task.FromResult(true); + // if (shellSection == null) + // return Task.FromResult(true); - Shell.ApplyQueryAttributes(shellSection, queryData, request.Request.Content == null); + // Shell.ApplyQueryAttributes(shellSection, queryData, request.Request.Content == null); - if (CurrentItem != shellSection) - SetValueFromRenderer(CurrentItemProperty, shellSection); + // if (CurrentItem != shellSection) + // SetValueFromRenderer(CurrentItemProperty, shellSection); - return shellSection.GoToPart(request, queryData); - } + // return shellSection.GoToPart(request, queryData); + //} bool IShellItemController.ProposeSection(ShellSection shellSection, bool setValue) { diff --git a/Xamarin.Forms.Core/Shell/ShellNavigationService.cs b/Xamarin.Forms.Core/Shell/ShellNavigationService.cs new file mode 100644 index 00000000000..1c2ea1e60e4 --- /dev/null +++ b/Xamarin.Forms.Core/Shell/ShellNavigationService.cs @@ -0,0 +1,348 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Xamarin.Forms; +using Xamarin.Forms.Internals; + +[assembly: Dependency(typeof(ShellNavigationService))] +namespace Xamarin.Forms +{ + public class ShellNavigationService : + IShellUriParser, + IShellContentCreator, + IShellApplyParameters, + IShellPartAppearing, + IShellPartAppeared, + IShellNavigationRequest + { + public ShellNavigationService() + { + } + + public virtual Task AppearedAsync(ShellLifecycleArgs args) + { + return Task.Delay(0); + } + + public virtual Task AppearingAsync(ShellLifecycleArgs args) + { + return Task.Delay(0); + } + + public virtual void ApplyParameters(ShellLifecycleArgs args) + { + Shell.ApplyQueryAttributes(args.Element, args.PathPart?.NavigationParameters ?? args.RoutePath?.NavigationParameters, args.IsLast); + } + + public virtual Page Create(ShellContentCreateArgs args) + { + var shellContent = args.Content; + var template = shellContent.ContentTemplate; + var content = shellContent.Content; + + Page result = null; + if (template == null) + { + if (content is Page page) + result = page; + else + result = (Page)Routing.GetOrCreateContent(args.Content.Route); + } + else + { + result = (Page)template.CreateContent(content, shellContent); + } + + return result; + + } + public virtual Task NavigatingToAsync(ShellNavigationArgs args) + { + return Task.FromResult(args.FutureState); + } + + public virtual Task ParseAsync(ShellUriParserArgs args) + { + var navigationRequest = ShellUriHandler.GetNavigationRequest(args.Shell, args.Uri, false); + return Task.FromResult(navigationRequest); + } + } + + public class PathPart + { + readonly string _path; + + public PathPart(Element baseShellItem, Dictionary navigationParameters) + { + ShellPart = baseShellItem; + + if (baseShellItem is BaseShellItem shellPart) + _path = shellPart.Route; + else + _path = Routing.GetRoute(baseShellItem); + + NavigationParameters = navigationParameters; + } + + public string Path => _path ?? Routing.GetRoute(ShellPart); + public Dictionary NavigationParameters { get; } + + public Element ShellPart { get; } + + //// This describes how you will transition to and away from this Path Part + //ITransitionPlan Transition { get; } + + //// how am I presented? Modally? as a page? + //PresentationHint Presentation { get; } + } + + // Do better at immutable stuff + public class RoutePath + { + public RoutePath(IList pathParts, Dictionary navigationParameters) + { + PathParts = new ReadOnlyCollection(pathParts); + NavigationParameters = navigationParameters; + + StringBuilder builder = new StringBuilder(); + + for (var i = 0; i < pathParts.Count; i++) + { + var path = pathParts[i]; + builder.Append(path.Path); + builder.Append("/"); + } + + FullUriWithImplicit = ShellUriHandler.ConvertToStandardFormat(new Uri(builder.ToString(), UriKind.Relative)); + } + + internal Uri FullUriWithImplicit { get; } + public Dictionary NavigationParameters { get; } + public IReadOnlyList PathParts { get; } + } + + public class ShellRouteState + { + internal ShellRouteState() : this(new RoutePath(new List(), new Dictionary())) + { + + } + + internal ShellRouteState(Shell shell) + { + // TODO Shane wire up navigation parameters property on each base shell item + List pathParts = new List(); + if (shell.CurrentItem != null) + pathParts.Add(new PathPart(shell.CurrentItem, null)); + + if (shell.CurrentItem?.CurrentItem != null) + pathParts.Add(new PathPart(shell.CurrentItem.CurrentItem, null)); + + if (shell.CurrentItem?.CurrentItem?.CurrentItem != null) + { + var shellSection = shell.CurrentItem.CurrentItem.CurrentItem; + pathParts.Add(new PathPart(shellSection, null)); + + foreach(var item in shellSection.Navigation.NavigationStack) + { + if (item == null) + continue; + + if(item.Parent is ShellContent content) + pathParts.Add(new PathPart(content, null)); + else + pathParts.Add(new PathPart(item, null)); + } + } + + CurrentRoute = new RoutePath(pathParts, new Dictionary()); + Routes = new[] { CurrentRoute }; + } + + + public ShellRouteState(RoutePath routePath) + { + CurrentRoute = routePath; + Routes = new[] { CurrentRoute }; + } + + ShellRouteState(RoutePath[] routePaths, RoutePath currentRoute) + { + Routes = routePaths; + CurrentRoute = currentRoute; + } + + public RoutePath[] Routes { get; } + public RoutePath CurrentRoute { get; } + + public ShellRouteState Add(PathPart pathPart) + { + List newPathPArts = new List(CurrentRoute.PathParts); + newPathPArts.Add(pathPart); + + RoutePath[] newRoutes = new RoutePath[Routes.Length]; + Array.Copy(Routes, newRoutes, Routes.Length); + + RoutePath newCurrentRoute = null; + for (var i = 0; i < newRoutes.Length; i++) + { + var route = newRoutes[i]; + if (route == CurrentRoute) + { + newCurrentRoute = new RoutePath(newPathPArts, route.NavigationParameters); + newRoutes[i] = newCurrentRoute; + } + } + + return new ShellRouteState(newRoutes, newCurrentRoute); + } + public ShellRouteState Add(IList pathParts) + { + List newPathPArts = new List(CurrentRoute.PathParts); + newPathPArts.AddRange(pathParts); + + RoutePath[] newRoutes = new RoutePath[Routes.Length]; + Array.Copy(Routes, newRoutes, Routes.Length); + + RoutePath newCurrentRoute = null; + for (var i = 0; i < newRoutes.Length; i++) + { + var route = newRoutes[i]; + if (route == CurrentRoute) + { + newCurrentRoute = new RoutePath(newPathPArts, route.NavigationParameters); + newRoutes[i] = newCurrentRoute; + } + } + + return new ShellRouteState(newRoutes, newCurrentRoute); + } + } + + public interface IShellUriParser + { + // based on the current state and this uri what should the new state look like? + // the uri could completely demolish the current state and just return a new setup completely + Task ParseAsync(ShellUriParserArgs args); + } + + public class ShellUriParserArgs : EventArgs + { + public ShellUriParserArgs(Shell shell, Uri uri) + { + Shell = shell; + Uri = uri; + } + + public Shell Shell { get; } + public Uri Uri { get; } + } + + public interface IShellNavigationRequest + { + // this will return the state change. If you want to cancel navigation then just return null or current state + Task NavigatingToAsync(ShellNavigationArgs args); + } + + public interface IShellContentCreator + { + Page Create(ShellContentCreateArgs content); + } + + + public interface IShellApplyParameters + { + // this is where we will apply query parameters to the shell content + // this may be called multiple times. For example when the bindingcontext changes it will be called again + void ApplyParameters(ShellLifecycleArgs args); + } + + public interface IShellPartAppearing + { + // this will get called for each piece that appears shellitem, shellsection, shellcontent + Task AppearingAsync(ShellLifecycleArgs args); + } + + public interface IShellPartAppeared + { + // this will get called for each piece that appeared shellitem, shellsection, shellcontent + Task AppearedAsync(ShellLifecycleArgs args); + } + + public interface IShellPartDisappeared + { + // this will get called for each piece that disappeared shellitem, shellsection, shellcontent + Task DisappearedAsync(ShellLifecycleArgs args); + } + + public class ShellNavigationArgs : EventArgs + { + public ShellNavigationArgs(Shell shell, ShellRouteState futureState) + { + Shell = shell; + FutureState = futureState; + } + + public Shell Shell { get; } + public ShellRouteState FutureState { get; } + } + + public class ShellContentCreateArgs : EventArgs + { + public ShellContentCreateArgs(ShellContent content) + { + Content = content; + } + + public ShellContent Content { get; } + } + public class ShellLifecycleArgs : EventArgs + { + public ShellLifecycleArgs(Element element, PathPart pathPart, RoutePath routePath) + { + Element = element; + PathPart = pathPart; + RoutePath = routePath; + } + + public Element Element { get; } + public PathPart PathPart { get; } + public RoutePath RoutePath { get; } + + public bool IsLast + { + get + { + if (RoutePath.PathParts[RoutePath.PathParts.Count - 1] == PathPart) + return true; + + return false; + } + } + } + + + + + //// possible any interface + //public enum PresentationHint + //{ + // Page, + // Modal, + // Dialog + //} + + //interface ITransitionPlan + //{ + // ITransition TransitionTo { get; } + // ITransition TransitionFrom { get; } + //} + + //interface ITransition + //{ + + //} + +} diff --git a/Xamarin.Forms.Core/Shell/ShellSection.cs b/Xamarin.Forms.Core/Shell/ShellSection.cs index 7eed4b94e7f..c87ae54933e 100644 --- a/Xamarin.Forms.Core/Shell/ShellSection.cs +++ b/Xamarin.Forms.Core/Shell/ShellSection.cs @@ -67,30 +67,6 @@ void IShellSectionController.AddDisplayedPageObserver(object observer, Action queryData) - { - ShellContent shellContent = request.Request.Content; - - if (shellContent == null) - return Task.FromResult(true); - - if (request.Request.GlobalRoutes.Count > 0) - { - // TODO get rid of this hack and fix so if there's a stack the current page doesn't display - Device.BeginInvokeOnMainThread(async () => - { - await GoToAsync(request, queryData, false); - }); - } - - Shell.ApplyQueryAttributes(shellContent, queryData, request.Request.GlobalRoutes.Count == 0); - - if (CurrentItem != shellContent) - SetValueFromRenderer(CurrentItemProperty, shellContent); - - return Task.FromResult(true); - } - bool IShellSectionController.RemoveContentInsetObserver(IShellContentInsetObserver observer) { return _observers.Remove(observer); @@ -227,49 +203,58 @@ public static implicit operator ShellSection(TemplatedPage page) return (ShellSection)(ShellContent)page; } - internal async Task GoToAsync(NavigationRequest request, IDictionary queryData, bool animate) + internal async Task GoToAsync(ShellRouteState navigationRequest, bool animate) { - List routes = request.Request.GlobalRoutes; - if (routes == null || routes.Count == 0) + var currentRoute = navigationRequest.CurrentRoute; + var pathParts = currentRoute.PathParts; + if (pathParts == null || pathParts.Count <= 3) { await Navigation.PopToRootAsync(animate); return; } - for (int i = 0; i < routes.Count; i++) + int pageCount = 0; + for (int i = 3; i < pathParts.Count; i++, pageCount++) { - bool isLast = i == routes.Count - 1; - var route = routes[i]; - var navPage = _navStack.Count > i + 1 ? _navStack[i + 1] : null; + bool isLast = i == pathParts.Count - 1; + var route = pathParts[i]; + var navPage = _navStack.Count > pageCount + 1 ? _navStack[pageCount + 1] : null; if (navPage != null) { - if (Routing.GetRoute(navPage) == route) + if (Routing.GetRoute(navPage) == route.Path) { - Shell.ApplyQueryAttributes(navPage, queryData, isLast); + ShellApplyParameters.ApplyParameters(new ShellLifecycleArgs(navPage, route, currentRoute)); + //Shell.ApplyQueryAttributes(navPage, route.NavigationParameters, isLast); continue; } - - if (request.StackRequest == NavigationRequest.WhatToDoWithTheStack.ReplaceIt) + else { - while (_navStack.Count > i + 1) + while (_navStack.Count > pageCount + 1) { await OnPopAsync(false); } } } - var content = Routing.GetOrCreateContent(route) as Page; + Page content = null; + + if (route.ShellPart is IShellContentController shellContent) + content = shellContent.GetOrCreateContent(); + else + content = Routing.GetOrCreateContent(route.Path) as Page; + if (content == null) break; - Shell.ApplyQueryAttributes(content, queryData, isLast); - await OnPushAsync(content, i == routes.Count - 1 && animate); + ShellApplyParameters.ApplyParameters(new ShellLifecycleArgs(content, route, currentRoute)); + //Shell.ApplyQueryAttributes(content, route.NavigationParameters, isLast); + await OnPushAsync(content, i == pathParts.Count - 1 && animate); } - + SendAppearanceChanged(); } - + internal void SendStructureChanged() { if (Parent?.Parent is Shell shell) @@ -558,6 +543,11 @@ void SendUpdateCurrentState(ShellNavigationSource source) } } + #region Navigation Interfaces + IShellApplyParameters ShellApplyParameters => DependencyService.Get(); + IShellContentCreator ShellContentCreator => DependencyService.Get(); + #endregion + class NavigationImpl : NavigationProxy { readonly ShellSection _owner; diff --git a/Xamarin.Forms.Core/Shell/ShellState.cs b/Xamarin.Forms.Core/Shell/ShellState.cs new file mode 100644 index 00000000000..19db8ae18fc --- /dev/null +++ b/Xamarin.Forms.Core/Shell/ShellState.cs @@ -0,0 +1,111 @@ +//using System.Collections.ObjectModel; + +//namespace Xamarin.Forms +//{ +// public class ShellState +// { +// internal string Route => _state.Route; +// readonly Shell _state; +// public ShellState(Shell state) +// { +// _state = state; +// ObservableCollection shellItems = new ObservableCollection(); +// foreach (var item in state.Items) +// shellItems.Add(new ShellItemState(item)); + +// Items = new ReadOnlyCollection(shellItems); +// } +// public ReadOnlyCollection Items { get; } + +// public ShellRouteState RouteState { get; } + +// public ShellItemState CurrentItem +// { +// get +// { +// foreach (var item in Items) +// if (item.Item == _state.CurrentItem) +// return item; + +// return null; +// } +// } + +// // TODO DELETE ME +// public static implicit operator ShellState(Shell shell) +// { +// return new ShellState(shell); +// } +// } +// public class ShellItemState +// { +// internal ShellItem Item { get; } +// public ReadOnlyCollection Items { get; } + +// public ShellItemState(ShellItem item) +// { +// Item = item; +// ObservableCollection shellItems = new ObservableCollection(); +// foreach (var section in Item.Items) +// shellItems.Add(new ShellSectionState(section)); + +// Items = new ReadOnlyCollection(shellItems); +// } +// public ShellSectionState CurrentItem +// { +// get +// { +// foreach (var item in Items) +// if (item.Section == Item.CurrentItem) +// return item; + +// return null; +// } +// } + +// public string Route => Item.Route; +// } + +// public class ShellSectionState +// { +// internal ShellSection Section { get; } +// public ReadOnlyCollection Items { get; } + +// public ShellSectionState(ShellSection section) +// { +// Section = section; +// ObservableCollection shellItems = new ObservableCollection(); +// foreach (var content in Section.Items) +// shellItems.Add(new ShellContentState(content)); + +// Items = new ReadOnlyCollection(shellItems); +// } +// public ShellContentState CurrentItem +// { +// get +// { +// foreach (var item in Items) +// if (item.Content == Section.CurrentItem) +// return item; + +// return null; +// } +// } +// public string Route => Section.Route; +// } + +// public class ShellContentState +// { +// internal ShellContent Content { get; } +// public ShellContentState(ShellContent content) +// { +// Content = content; +// } +// public string Route => Content.Route; +// } + +// public interface IShellStateCreator +// { +// ShellState CreateShellState(Shell shell); +// } +//} diff --git a/Xamarin.Forms.Core/Shell/ShellUriHandler.cs b/Xamarin.Forms.Core/Shell/ShellUriHandler.cs index 285a3894537..b03061a275a 100644 --- a/Xamarin.Forms.Core/Shell/ShellUriHandler.cs +++ b/Xamarin.Forms.Core/Shell/ShellUriHandler.cs @@ -1,17 +1,21 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; namespace Xamarin.Forms { - internal class ShellUriHandler { static readonly char[] _pathSeparators = { '/', '\\' }; const string _pathSeparator = "/"; + static internal string Route = Routing.GenerateImplicitRoute("shell"); + static internal string RouteHost => "shell"; + static internal string RouteScheme => "app"; + internal static Uri FormatUri(Uri path) { if (path.IsAbsoluteUri) @@ -41,7 +45,7 @@ internal static Uri CreateUri(string path) return new Uri(path, UriKind.Relative); } - public static Uri ConvertToStandardFormat(Shell shell, Uri request) + public static Uri ConvertToStandardFormat(Uri request) { request = FormatUri(request); string pathAndQuery = null; @@ -53,34 +57,23 @@ public static Uri ConvertToStandardFormat(Shell shell, Uri request) var segments = new List(pathAndQuery.Split(_pathSeparators, StringSplitOptions.RemoveEmptyEntries)); - if (segments[0] != shell.RouteHost) - segments.Insert(0, shell.RouteHost); + if (segments.Count > 0 && segments[0] != RouteHost) + segments.Insert(0, RouteHost); - if (segments[1] != shell.Route) - segments.Insert(1, shell.Route); + if (segments.Count > 1 && segments[1] != Route) + segments.Insert(1, Route); var path = String.Join(_pathSeparator, segments.ToArray()); - string uri = $"{shell.RouteScheme}://{path}"; + string uri = $"{RouteScheme}://{path}"; return new Uri(uri); } - internal static NavigationRequest GetNavigationRequest(Shell shell, Uri uri, bool enableRelativeShellRoutes = false) + internal static ShellRouteState GetNavigationRequest(Shell shell, Uri uri, bool enableRelativeShellRoutes = false) { uri = FormatUri(uri); - // figure out the intent of the Uri - NavigationRequest.WhatToDoWithTheStack whatDoIDo = NavigationRequest.WhatToDoWithTheStack.PushToIt; - if (uri.IsAbsoluteUri) - whatDoIDo = NavigationRequest.WhatToDoWithTheStack.ReplaceIt; - else if (uri.OriginalString.StartsWith("//", StringComparison.Ordinal) || uri.OriginalString.StartsWith("\\\\", StringComparison.Ordinal)) - whatDoIDo = NavigationRequest.WhatToDoWithTheStack.ReplaceIt; - else - whatDoIDo = NavigationRequest.WhatToDoWithTheStack.PushToIt; - - Uri request = ConvertToStandardFormat(shell, uri); - - var possibleRouteMatches = GenerateRoutePaths(shell, request, uri, enableRelativeShellRoutes); - + Uri request = ConvertToStandardFormat(uri); + List possibleRouteMatches = GenerateRoutePaths(shell, request, uri, enableRelativeShellRoutes); if (possibleRouteMatches.Count == 0) throw new ArgumentException($"unable to figure out route for: {uri}", nameof(uri)); @@ -100,17 +93,50 @@ internal static NavigationRequest GetNavigationRequest(Shell shell, Uri uri, boo } var theWinningRoute = possibleRouteMatches[0]; - RequestDefinition definition = - new RequestDefinition( - ConvertToStandardFormat(shell, CreateUri(theWinningRoute.PathFull)), - theWinningRoute.Item, - theWinningRoute.Section, - theWinningRoute.Content, - theWinningRoute.GlobalRouteMatches); + List pathParts = new List(); + ShellRouteState returnValue = shell.RouteState ?? new ShellRouteState(); + + if (theWinningRoute.Item == null) + { + for (var i = 0; i < theWinningRoute.GlobalRouteMatches.Count; i++) + { + var item = new ShellContent() + { + Route = theWinningRoute.GlobalRouteMatches[i] + }; - NavigationRequest navigationRequest = new NavigationRequest(definition, whatDoIDo, request.Query, request.Fragment); + pathParts.Add(new PathPart(item, Shell.GetNavigationParameters(item, request.Query, theWinningRoute.GlobalRouteMatches.Count == (i + 1)))); + } - return navigationRequest; + returnValue = returnValue.Add(pathParts); + } + else + { + var shellItem = theWinningRoute.Item; + ShellSection shellSection = theWinningRoute.Section; + + if (shellSection == null) + shellSection = shellItem.Items[0]; + + ShellContent shellContent = theWinningRoute.Content; + if (shellContent == null) + shellContent = shellSection.Items[0]; + + pathParts.Add(new PathPart(shellItem, Shell.GetNavigationParameters(shellItem, request.Query, false))); + pathParts.Add(new PathPart(shellSection, Shell.GetNavigationParameters(shellSection, request.Query, false))); + + if(shellContent != null) + pathParts.Add(new PathPart(shellContent, Shell.GetNavigationParameters(shellContent, request.Query, theWinningRoute.GlobalRouteMatches.Count == 0))); + + for (var i = 0; i < theWinningRoute.GlobalRouteMatches.Count; i++) + { + var item = new ShellContent() { Route = theWinningRoute.GlobalRouteMatches[i] }; + pathParts.Add(new PathPart(item, Shell.GetNavigationParameters(item, request.Query, theWinningRoute.GlobalRouteMatches.Count == (i + 1)))); + } + returnValue = new ShellRouteState(new RoutePath(pathParts, Shell.GetNavigationParameters(shell, request.Query, false))); + } + + return returnValue; } internal static List GenerateRoutePaths(Shell shell, Uri request) @@ -137,7 +163,7 @@ internal static List GenerateRoutePaths(Shell shell, Uri re List possibleRoutePaths = new List(); if (!request.IsAbsoluteUri) - request = ConvertToStandardFormat(shell, request); + request = ConvertToStandardFormat(request); string localPath = request.LocalPath; @@ -153,10 +179,10 @@ internal static List GenerateRoutePaths(Shell shell, Uri re for (int i = 0; i < routeKeys.Length; i++) { var route = routeKeys[i]; - var uri = ConvertToStandardFormat(shell, CreateUri(route)); + var uri = ConvertToStandardFormat(CreateUri(route)); if (uri.Equals(request)) { - throw new Exception($"Global routes currently cannot be the only page on the stack, so absolute routing to global routes is not supported. For now, just navigate to: {originalRequest.OriginalString.Replace("//","")}"); + throw new Exception($"Global routes currently cannot be the only page on the stack, so absolute routing to global routes is not supported. For now, just navigate to: {originalRequest.OriginalString.Replace("//", "")}"); //var builder = new RouteRequestBuilder(route, route, null, segments); //return new List { builder }; } @@ -175,7 +201,7 @@ internal static List GenerateRoutePaths(Shell shell, Uri re depthStart = 0; } - if(relativeMatch && shell?.CurrentItem != null) + if (relativeMatch && shell?.CurrentItem != null) { // retrieve current location var currentLocation = NodeLocation.Create(shell); @@ -227,7 +253,7 @@ internal static List GenerateRoutePaths(Shell shell, Uri re RouteRequestBuilder builder = null; foreach (var segment in segments) { - if(routeKeys.Contains(segment)) + if (routeKeys.Contains(segment)) { if (builder == null) builder = new RouteRequestBuilder(segment, segment, null, segments); @@ -236,7 +262,7 @@ internal static List GenerateRoutePaths(Shell shell, Uri re } } - if(builder != null && builder.IsFullMatch) + if (builder != null && builder.IsFullMatch) return new List { builder }; } else @@ -321,9 +347,9 @@ 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.CurrentItem?.CurrentItem?.CurrentItem ?? + (object)shell.CurrentItem?.CurrentItem ?? + (object)shell.CurrentItem ?? (object)shell); return location; @@ -537,7 +563,7 @@ static IEnumerable GetItems(object node) if (segments[0] == route) { - yield return new GlobalRouteItem(key, key); + yield return new GlobalRouteItem(key, key); } } } @@ -597,204 +623,53 @@ public bool IsFinished } } - /// - /// 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(_uriSeparator, _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(_uriSeparator, StringComparison.Ordinal) || segments[0].StartsWith("\\", StringComparison.Ordinal)) - 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}")] - internal 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}")] - internal class RequestDefinition - { - public RequestDefinition(Uri fullUri, ShellItem item, ShellSection section, ShellContent content, List globalRoutes) - { - FullUri = fullUri; - Item = item; - Section = section; - Content = content; - GlobalRoutes = globalRoutes; - } - - public RequestDefinition(string fullUri, ShellItem item, ShellSection section, ShellContent content, List globalRoutes) : - this(new Uri(fullUri, UriKind.Absolute), item, section, content, globalRoutes) - { - } - - public Uri FullUri { get; } - public ShellItem Item { get; } - public ShellSection Section { get; } - public ShellContent Content { get; } - public List GlobalRoutes { get; } - } + //[DebuggerDisplay("RequestDefinition = {Request}, StackRequest = {StackRequest}")] + //internal 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}")] + //internal class RequestDefinition + //{ + // public RequestDefinition(Uri fullUri, ShellItem item, ShellSection section, ShellContent content, List globalRoutes) + // { + // FullUri = fullUri; + // Item = item; + // Section = section; + // Content = content; + // GlobalRoutes = globalRoutes; + // } + + // public RequestDefinition(string fullUri, ShellItem item, ShellSection section, ShellContent content, List globalRoutes) : + // this(new Uri(fullUri, UriKind.Absolute), item, section, content, globalRoutes) + // { + // } + + // public Uri FullUri { get; } + // public ShellItem Item { get; } + // public ShellSection Section { get; } + // public ShellContent Content { get; } + // public List GlobalRoutes { get; } + //} }