Shell lifecycle (#6527)
authorShane Neuville <shane94@hotmail.com>
Wed, 3 Jul 2019 23:53:45 +0000 (17:53 -0600)
committerSamantha Houts <samhouts@users.noreply.github.com>
Wed, 3 Jul 2019 23:53:45 +0000 (16:53 -0700)
* shell appearing and disappearing events

* life cycle UI tests and fixes

* - fix appearing on shell content to only fire when page gets created
- fix navigated to only fire after shell content is appearing

* OnNavigatedOnlyFiresOnce
fixes #6486

Xamarin.Forms.Core.UnitTests/ShellLifeCycleTests.cs [new file with mode: 0644]
Xamarin.Forms.Core.UnitTests/ShellTests.cs
Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj
Xamarin.Forms.Core/Shell/BaseShellItem.cs
Xamarin.Forms.Core/Shell/Shell.cs
Xamarin.Forms.Core/Shell/ShellContent.cs
Xamarin.Forms.Core/Shell/ShellExtensions.cs [new file with mode: 0644]
Xamarin.Forms.Core/Shell/ShellItem.cs
Xamarin.Forms.Core/Shell/ShellSection.cs
Xamarin.Forms.Platform.Android/Renderers/ShellSectionRenderer.cs

diff --git a/Xamarin.Forms.Core.UnitTests/ShellLifeCycleTests.cs b/Xamarin.Forms.Core.UnitTests/ShellLifeCycleTests.cs
new file mode 100644 (file)
index 0000000..b2667a0
--- /dev/null
@@ -0,0 +1,402 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using NUnit.Framework;
+using Xamarin.Forms.Internals;
+
+namespace Xamarin.Forms.Core.UnitTests
+{
+       [TestFixture]
+       public class ShellLifeCycleTests : ShellTestBase
+       {
+               const string ContentRoute = "content";
+               const string SectionRoute = "section";
+               const string ItemRoute = "item";
+
+               [Test]
+               public void AppearingOnCreate()
+               {
+                       Shell shell = new Shell();
+
+                       FlyoutItem flyoutItem = new FlyoutItem();
+                       Tab tab = new Tab();
+                       ShellContent content = new ShellContent() { Content = new ContentPage() };
+
+                       bool flyoutAppearing = false;
+                       bool tabAppearing = false;
+                       bool contentAppearing = false;
+
+                       flyoutItem.Appearing += (_, __) => flyoutAppearing = true;
+                       tab.Appearing += (_, __) => tabAppearing = true;
+                       content.Appearing += (_, __) => contentAppearing = true;
+
+                       shell.Items.Add(flyoutItem);
+                       flyoutItem.Items.Add(tab);
+                       tab.Items.Add(content);
+
+                       Assert.True(flyoutAppearing, "Flyout appearing");
+                       Assert.True(tabAppearing, "Tab Appearing");
+                       Assert.True(contentAppearing, "Content Appearing");
+               }
+
+
+               [Test]
+               public void AppearingOnCreateFromTemplate()
+               {
+                       Shell shell = new Shell();
+
+                       FlyoutItem flyoutItem = new FlyoutItem();
+                       Tab tab = new Tab();
+                       ContentPage page = new ContentPage();
+                       ShellContent content = new ShellContent() { ContentTemplate = new DataTemplate(() => page) };
+
+                       bool flyoutAppearing = false;
+                       bool tabAppearing = false;
+                       bool contentAppearing = false;
+                       bool pageAppearing = false;
+
+                       flyoutItem.Appearing += (_, __) => flyoutAppearing = true;
+                       tab.Appearing += (_, __) => tabAppearing = true;
+                       content.Appearing += (_, __) => contentAppearing = true;
+                       page.Appearing += (_, __) => pageAppearing = true;
+
+                       shell.Items.Add(flyoutItem);
+                       flyoutItem.Items.Add(tab);
+                       tab.Items.Add(content);
+
+                       Assert.True(flyoutAppearing, "Flyout appearing");
+                       Assert.True(tabAppearing, "Tab Appearing");
+
+                       // Because the is a page template the content appearing events won't fire until the page is created
+                       Assert.IsFalse(contentAppearing, "Content Appearing");
+                       Assert.IsFalse(pageAppearing, "Page Appearing");
+
+                       var createdContent = (content as IShellContentController).GetOrCreateContent();
+
+                       Assert.AreEqual(createdContent, page);
+                       Assert.IsTrue(contentAppearing, "Content Appearing");
+                       Assert.IsTrue(pageAppearing, "Page Appearing");
+               }
+
+
+
+               [Test]
+               public async Task NavigatedFiresAfterContentIsCreatedWhenUsingTemplate()
+               {
+                       Shell shell = new Shell();
+
+                       FlyoutItem flyoutItem = new FlyoutItem();
+                       Tab tab = new Tab();
+                       ContentPage page = null;
+                       ShellContent content = new ShellContent()
+                       {
+                               Route = "destination",
+                               ContentTemplate = new DataTemplate(() =>
+                               {
+                                       page = new ContentPage();
+                                       return page;
+                               })
+                       };
+
+                       flyoutItem.Items.Add(tab);
+                       tab.Items.Add(content);
+
+                       shell.Items.Add(CreateShellItem());
+                       shell.Items.Add(flyoutItem);
+
+                       Assert.AreNotEqual(shell.CurrentItem.CurrentItem.CurrentItem, content);
+                       Assert.IsNull(page);
+
+                       bool navigated = false;
+                       shell.Navigated += (_, __) =>
+                       {
+                               Assert.IsNotNull(page);
+                               Assert.IsNotNull((content as IShellContentController).Page);
+                               navigated = true;
+                       };
+
+                       await shell.GoToAsync("///destination");
+
+                       // content hasn't been created yet
+                       Assert.IsFalse(navigated);
+
+                       var createPage = (content as IShellContentController).GetOrCreateContent();
+                       Assert.AreEqual(createPage, page);
+                       Assert.IsTrue(navigated);
+               }
+
+               [Test]
+               public void AppearingOnShellContentChanged()
+               {
+                       Shell shell = new Shell();
+                       var item = CreateShellItem(shellContentRoute: ContentRoute, shellSectionRoute: SectionRoute, shellItemRoute: ItemRoute);
+                       var section = item.SearchForRoute<ShellSection>(SectionRoute);
+
+                       var content = new ShellContent();
+                       section.Items.Insert(0, content);
+                       section.CurrentItem = content;
+                       shell.Items.Add(item);
+                       ShellLifeCycleState state = new ShellLifeCycleState(shell);
+
+                       state.AllFalse();
+
+                       Assert.AreEqual(content, section.CurrentItem);
+
+                       section.CurrentItem = shell.SearchForRoute<ShellContent>(ContentRoute);
+
+                       Assert.IsTrue(state.ContentAppearing);
+
+                       section.CurrentItem = content;
+
+                       Assert.IsFalse(state.ContentAppearing);
+               }
+
+
+               [Test]
+               public void AppearingOnShellSectionChanged()
+               {
+                       Shell shell = new Shell();
+                       var item = CreateShellItem(shellContentRoute: ContentRoute, shellSectionRoute: SectionRoute, shellItemRoute: ItemRoute);
+                       var section = item.SearchForRoute<ShellSection>(SectionRoute);
+                       var newSection = CreateShellSection();
+                       item.Items.Insert(0, newSection);
+                       item.CurrentItem = newSection;
+                       shell.Items.Add(item);
+                       ShellLifeCycleState state = new ShellLifeCycleState(shell);
+                       state.AllFalse();
+
+
+                       Assert.AreEqual(newSection, item.CurrentItem);
+                       Assert.AreNotEqual(section, item.CurrentItem);
+
+                       item.CurrentItem = section;
+
+                       Assert.IsTrue(state.SectionAppearing);
+                       Assert.IsTrue(state.ContentAppearing);
+
+                       item.CurrentItem = newSection;
+
+                       Assert.IsFalse(state.SectionAppearing);
+                       Assert.IsFalse(state.ContentAppearing);
+               }
+
+               [Test]
+               public void AppearingOnShellItemChanged()
+               {
+                       Shell shell = new Shell();
+                       var item = CreateShellItem(shellContentRoute: ContentRoute, shellSectionRoute: SectionRoute, shellItemRoute: ItemRoute);
+                       var item2 = CreateShellItem();
+                       shell.Items.Add(item2);
+                       shell.Items.Add(item);
+                       ShellLifeCycleState state = new ShellLifeCycleState(shell);
+                       state.AllFalse();
+
+                       Assert.AreEqual(shell.CurrentItem, item2);
+                       Assert.AreNotEqual(shell.CurrentItem, item);
+
+                       shell.CurrentItem = item;
+
+                       Assert.IsTrue(state.ItemAppearing);
+                       Assert.IsTrue(state.SectionAppearing);
+                       Assert.IsTrue(state.ContentAppearing);
+
+                       shell.CurrentItem = item2;
+
+                       Assert.IsFalse(state.ItemAppearing);
+                       Assert.IsFalse(state.SectionAppearing);
+                       Assert.IsFalse(state.ContentAppearing);
+               }
+
+
+               [Test]
+               public async Task ShellPartWithModalPush()
+               {
+                       Shell shell = new Shell();
+                       var item = CreateShellItem(shellContentRoute: ContentRoute, shellSectionRoute: SectionRoute, shellItemRoute: ItemRoute);
+                       ShellLifeCycleState lifeCycleState = new ShellLifeCycleState(item);
+                       shell.Items.Add(item);
+
+                       lifeCycleState.AllTrue();
+
+                       ContentPage page = new ContentPage();
+                       await shell.Navigation.PushModalAsync(page);
+                       lifeCycleState.AllFalse();
+                       await shell.Navigation.PopModalAsync();
+                       lifeCycleState.AllTrue();
+               }
+
+
+               [Test]
+               public async Task ShellPartWithPagePush()
+               {
+                       Shell shell = new Shell();
+                       var item = CreateShellItem(shellContentRoute: ContentRoute, shellSectionRoute: SectionRoute, shellItemRoute: ItemRoute);
+                       ShellLifeCycleState lifeCycleState = new ShellLifeCycleState(item);
+                       shell.Items.Add(item);
+
+                       lifeCycleState.AllTrue();
+
+                       await shell.Navigation.PushAsync(new ContentPage());
+                       //if you're just pushing a page then the section and item are still visible but the content is not
+                       Assert.IsFalse(lifeCycleState.PageAppearing);
+                       Assert.IsFalse(lifeCycleState.ContentAppearing);
+                       Assert.IsTrue(lifeCycleState.SectionAppearing);
+                       Assert.IsTrue(lifeCycleState.ItemAppearing);
+
+                       await shell.Navigation.PushAsync(new ContentPage());
+                       Assert.IsFalse(lifeCycleState.PageAppearing);
+                       Assert.IsFalse(lifeCycleState.ContentAppearing);
+                       Assert.IsTrue(lifeCycleState.SectionAppearing);
+                       Assert.IsTrue(lifeCycleState.ItemAppearing);
+
+                       await shell.Navigation.PopAsync();
+                       Assert.IsFalse(lifeCycleState.PageAppearing);
+                       Assert.IsFalse(lifeCycleState.ContentAppearing);
+                       Assert.IsTrue(lifeCycleState.SectionAppearing);
+                       Assert.IsTrue(lifeCycleState.ItemAppearing);
+                       await shell.Navigation.PopAsync();
+                       lifeCycleState.AllTrue();
+               }
+
+               [Test]
+               public async Task ShellPartWithPopToRoot()
+               {
+                       Shell shell = new Shell();
+                       var item = CreateShellItem(shellContentRoute: ContentRoute, shellSectionRoute: SectionRoute, shellItemRoute: ItemRoute);
+                       ShellLifeCycleState lifeCycleState = new ShellLifeCycleState(item);
+                       shell.Items.Add(item);
+
+                       lifeCycleState.AllTrue();
+
+                       await shell.Navigation.PushAsync(new ContentPage());
+                       await shell.Navigation.PushAsync(new ContentPage());
+                       await shell.Navigation.PushAsync(new ContentPage());
+                       await shell.Navigation.PushAsync(new ContentPage());
+                       Assert.IsFalse(lifeCycleState.PageAppearing);
+                       Assert.IsFalse(lifeCycleState.ContentAppearing);
+                       Assert.IsTrue(lifeCycleState.SectionAppearing);
+                       Assert.IsTrue(lifeCycleState.ItemAppearing);
+
+                       await shell.Navigation.PopToRootAsync();
+                       lifeCycleState.AllTrue();
+               }
+
+
+               [Test]
+               public async Task PagePushModal()
+               {
+                       Shell shell = new Shell();
+                       var item = CreateShellItem(shellContentRoute: ContentRoute, shellSectionRoute: SectionRoute, shellItemRoute: ItemRoute);
+                       ShellLifeCycleState lifeCycleState = new ShellLifeCycleState(item);
+                       shell.Items.Add(item);
+
+                       ContentPage page = new ContentPage();
+
+                       bool appearing = false;
+
+                       page.Appearing += (_, __) => appearing = true;
+                       page.Disappearing += (_, __) => appearing = false;
+
+                       await shell.Navigation.PushModalAsync(page);
+                       Assert.IsTrue(appearing);
+                       lifeCycleState.AllFalse();
+
+                       await shell.Navigation.PopModalAsync();
+                       Assert.IsFalse(appearing);
+                       lifeCycleState.AllTrue();
+               }
+
+               [Test]
+               public async Task PagePush()
+               {
+                       Shell shell = new Shell();
+                       var item = CreateShellItem(shellContentRoute: ContentRoute, shellSectionRoute: SectionRoute, shellItemRoute: ItemRoute);
+                       shell.Items.Add(item);
+
+                       ContentPage page = new ContentPage();
+                       bool appearing = false;
+
+                       page.Appearing += (_, __) => appearing = true;
+                       page.Disappearing += (_, __) => appearing = false;
+
+                       await shell.Navigation.PushAsync(page);
+                       Assert.IsTrue(appearing);
+                       await shell.Navigation.PopAsync();
+                       Assert.IsFalse(appearing);
+               }
+
+
+               [Test]
+               public async Task OnNavigatedOnlyFiresOnce()
+               {
+                       int navigated = 0;
+                       Shell shell = new Shell();
+                       shell.Navigated += (_, __) =>
+                       {
+                               navigated++;
+                       };
+
+                       var item = CreateShellItem(shellContentRoute: ContentRoute, shellSectionRoute: SectionRoute, shellItemRoute: ItemRoute);
+                       shell.Items.Add(item);
+                       Assert.AreEqual(1, navigated);
+               }
+
+
+
+
+               class ShellLifeCycleState
+               {
+                       public bool ItemAppearing;
+                       public bool SectionAppearing;
+                       public bool ContentAppearing;
+                       public bool PageAppearing;
+
+                       public ShellLifeCycleState(Shell shell)
+                       {
+                               var shellContent = shell.SearchForRoute<ShellContent>(ContentRoute);
+                               var contentPage = (shellContent as IShellContentController).GetOrCreateContent();
+
+                               shell.SearchForRoute(ItemRoute).Appearing += (_, __) => ItemAppearing = true;
+                               shell.SearchForRoute(SectionRoute).Appearing += (_, __) => SectionAppearing = true;
+                               shell.SearchForRoute(ContentRoute).Appearing += (_, __) => ContentAppearing = true;
+                               shellContent.Appearing += (_, __) => ContentAppearing = true;
+                               contentPage.Appearing += (_, __) => PageAppearing = true;
+
+                               shell.SearchForRoute(ItemRoute).Disappearing += (_, __) => ItemAppearing = false;
+                               shell.SearchForRoute(SectionRoute).Disappearing += (_, __) => SectionAppearing = false;
+                               shellContent.Disappearing += (_, __) => ContentAppearing = false;
+                               contentPage.Disappearing += (_, __) => PageAppearing = false;
+                       }
+                       public ShellLifeCycleState(BaseShellItem baseShellItem)
+                       {
+                               var shellContent = baseShellItem.SearchForRoute<ShellContent>(ContentRoute);
+                               var contentPage = (shellContent as IShellContentController).GetOrCreateContent();
+
+                               baseShellItem.SearchForRoute(ItemRoute).Appearing += (_, __) => ItemAppearing = true;
+                               baseShellItem.SearchForRoute(SectionRoute).Appearing += (_, __) => SectionAppearing = true;
+                               shellContent.Appearing += (_, __) => ContentAppearing = true;
+                               contentPage.Appearing += (_, __) => PageAppearing = true;
+
+                               baseShellItem.SearchForRoute(ItemRoute).Disappearing += (_, __) => ItemAppearing = false;
+                               baseShellItem.SearchForRoute(SectionRoute).Disappearing += (_, __) => SectionAppearing = false;
+                               shellContent.Disappearing += (_, __) => ContentAppearing = false;
+                               contentPage.Disappearing += (_, __) => PageAppearing = false;
+                       }
+
+                       public void AllFalse()
+                       {
+                               Assert.IsFalse(ItemAppearing);
+                               Assert.IsFalse(SectionAppearing);
+                               Assert.IsFalse(ContentAppearing);
+                               Assert.IsFalse(PageAppearing);
+                       }
+                       public void AllTrue()
+                       {
+                               Assert.IsTrue(ItemAppearing);
+                               Assert.IsTrue(SectionAppearing);
+                               Assert.IsTrue(ContentAppearing);
+                               Assert.IsTrue(PageAppearing);
+                       }
+               }
+       }
+}
index a780453..3c480a2 100644 (file)
@@ -596,5 +596,26 @@ namespace Xamarin.Forms.Core.UnitTests
                        Assert.AreEqual(0, shell.Items[1].Items.Count);
                        Assert.AreEqual(3, shell.Items[0].Items.Count);
                }
+
+
+               [Test]
+               public async Task NavigatedFiresAfterContentIsCreatedWhenUsingTemplate()
+               {
+
+                       var shell = new Shell();
+                       var item1 = CreateShellItem(asImplicit: true, shellContentRoute: "rootlevelcontent1");
+
+                       shell.Items.Add(item1);
+                       Routing.RegisterRoute("cat", typeof(ContentPage));
+                       Routing.RegisterRoute("details", typeof(ContentPage));
+
+                       await shell.GoToAsync("cat");
+                       await shell.GoToAsync("details");
+
+                       Assert.AreEqual("//rootlevelcontent1/cat/details", shell.CurrentState.Location.ToString());
+                       await shell.GoToAsync("//rootlevelcontent1/details");
+                       Assert.AreEqual("//rootlevelcontent1/details", shell.CurrentState.Location.ToString());
+               }
+
        }
 }
index fd38f20..afeea9e 100644 (file)
@@ -75,6 +75,7 @@
     <Compile Include="DependencyResolutionTests.cs" />
     <Compile Include="EffectiveFlowDirectionExtensions.cs" />
     <Compile Include="ShellTestBase.cs" />
+    <Compile Include="ShellLifeCycleTests.cs" />
     <Compile Include="ShellUriHandlerTests.cs" />
     <Compile Include="CheckBoxUnitTests.cs" />
     <Compile Include="VisualTests.cs" />
index 7068dd8..9f08fbd 100644 (file)
@@ -3,12 +3,18 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using System.Runtime.CompilerServices;
 using Xamarin.Forms.Internals;
+using System.ComponentModel;
 
 namespace Xamarin.Forms
 {
        [DebuggerDisplay("Title = {Title}, Route = {Route}")]
        public class BaseShellItem : NavigableElement, IPropertyPropagationController, IVisualController, IFlowDirectionController, ITabStopElement
        {
+               public event EventHandler Appearing;
+               public event EventHandler Disappearing;
+
+               bool _hasAppearing;
+
                #region PropertyKeys
 
                internal static readonly BindablePropertyKey IsCheckedPropertyKey = BindableProperty.CreateReadOnly(nameof(IsChecked), typeof(bool), typeof(BaseShellItem), false);
@@ -106,6 +112,51 @@ namespace Xamarin.Forms
                        set => SetValue(IsTabStopProperty, value);
                }
 
+               internal virtual void SendAppearing()
+               {
+                       if (_hasAppearing)
+                               return;
+
+                       _hasAppearing = true;
+                       OnAppearing();
+                       Appearing?.Invoke(this, EventArgs.Empty);
+               }
+
+               internal virtual void SendDisappearing()
+               {
+                       if (!_hasAppearing)
+                               return;
+
+                       _hasAppearing = false;
+                       OnDisappearing();
+                       Disappearing?.Invoke(this, EventArgs.Empty);
+               }
+
+               protected virtual void OnAppearing()
+               {
+               }
+
+               protected virtual void OnDisappearing()
+               {
+               }
+
+               internal void OnAppearing(Action action)
+               {
+                       if (_hasAppearing)
+                               action();
+                       else
+                       {
+                               EventHandler eventHandler = null;
+                               eventHandler = (_, __) =>
+                               {
+                                       this.Appearing -= eventHandler;
+                                       action();
+                               };
+
+                               this.Appearing += eventHandler;
+                       }
+               }
+
                protected virtual void OnTabStopPropertyChanged(bool oldValue, bool newValue) { }
 
                protected virtual bool TabStopDefaultValueCreator() => true;
index 3cc8fb6..e7bc609 100644 (file)
@@ -390,56 +390,55 @@ namespace Xamarin.Forms
                        var uri = navigationRequest.Request.FullUri;
                        var queryString = navigationRequest.Query;
                        var queryData = ParseQueryString(queryString);
-                       var path = uri.AbsolutePath;
-
-                       path = path.TrimEnd('/');
-
-                       var parts = path.Substring(1).Split('/').ToList();
-
-                       if (parts.Count < 2)
-                               throw new InvalidOperationException("Path must be at least 2 items long in Shell navigation");
-
-                       var shellRoute = parts[0];
-
-                       var expectedShellRoute = Routing.GetRoute(this) ?? string.Empty;
-                       if (expectedShellRoute != shellRoute)
-                               throw new NotImplementedException();
-                       else
-                               parts.RemoveAt(0);
 
                        ApplyQueryAttributes(this, queryData, false);
 
                        var shellItem = navigationRequest.Request.Item;
+                       var shellSection = navigationRequest.Request.Section;
+                       ShellContent shellContent = navigationRequest.Request.Content;
+
                        if (shellItem != null)
                        {
                                ApplyQueryAttributes(shellItem, queryData, navigationRequest.Request.Section == null);
 
                                if (CurrentItem != shellItem)
+                               {
                                        SetValueFromRenderer(CurrentItemProperty, shellItem);
+                               }
+
+                               if (shellSection != null)
+                               {
+                                       Shell.ApplyQueryAttributes(shellSection, queryData, navigationRequest.Request.Content == null);
+                                       if (shellItem.CurrentItem != shellSection)
+                                       {
+                                               shellItem.SetValueFromRenderer(ShellItem.CurrentItemProperty, shellSection);
+                                       }
 
-                               parts.RemoveAt(0);
+                                       if (shellContent != null)
+                                       {
+                                               Shell.ApplyQueryAttributes(shellContent, queryData, navigationRequest.Request.GlobalRoutes.Count == 0);
+                                               if (shellSection.CurrentItem != shellContent)
+                                               {
+                                                       shellSection.SetValueFromRenderer(ShellSection.CurrentItemProperty, shellContent);                                                      
+                                               }
+
+                                               if (navigationRequest.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 shellSection.GoToAsync(navigationRequest, queryData, false);
+                                                       });
+                                               }
 
-                               if (parts.Count > 0)
-                                       await shellItem.GoToPart(navigationRequest, queryData);
+                                       }
+                               }
                        }
                        else
                        {
                                await CurrentItem.CurrentItem.GoToAsync(navigationRequest, queryData, animate);
                        }
 
-                       //if (Routing.CompareWithRegisteredRoutes(shellItemRoute))
-                       //{
-                       //      var shellItem = ShellItem.GetShellItemFromRouteName(shellItemRoute);
-
-                       //      ApplyQueryAttributes(shellItem, queryData, parts.Count == 1);
-
-                       //      if (CurrentItem != shellItem)
-                       //              SetValueFromRenderer(CurrentItemProperty, shellItem);
-
-                       //      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!
@@ -820,12 +819,15 @@ namespace Xamarin.Forms
                                _accumulatedEvent = args;
                        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}");
-                                       */
-                               Navigated?.Invoke(this, args);
-                               //System.Diagnostics.Debug.WriteLine("Navigated: " + args.Current.Location);
+                               var content = CurrentItem?.CurrentItem?.CurrentItem;
+                               if (content != null)
+                               {
+                                       content.OnAppearing(() => Navigated?.Invoke(this, args));
+                               }
+                               else
+                               {
+                                       Navigated?.Invoke(this, args);
+                               }
                        }
                }
 
@@ -838,6 +840,12 @@ namespace Xamarin.Forms
 
                static void OnCurrentItemChanged(BindableObject bindable, object oldValue, object newValue)
                {
+                       if (oldValue is ShellItem oldShellItem)
+                               oldShellItem.SendDisappearing();
+
+                       if (newValue is ShellItem newShellItem)
+                               newShellItem.SendAppearing();
+
                        var shell = (Shell)bindable;
                        UpdateChecked(shell);
 
@@ -1103,6 +1111,25 @@ namespace Xamarin.Forms
                        protected override Task OnPushAsync(Page page, bool animated) => SectionProxy.PushAsync(page, animated);
 
                        protected override void OnRemovePage(Page page) => SectionProxy.RemovePage(page);
+
+                       protected override Task<Page> OnPopModal(bool animated)
+                       {
+                               if (ModalStack.Count > 0)
+                                       ModalStack[ModalStack.Count - 1].SendDisappearing();
+
+                               if (ModalStack.Count == 1)
+                                       _shell.CurrentItem.SendAppearing();
+
+                               return base.OnPopModal(animated);
+                       }
+                       protected override Task OnPushModal(Page modal, bool animated)
+                       {
+                               if (ModalStack.Count == 0)
+                                       _shell.CurrentItem.SendDisappearing();
+
+                               modal.SendAppearing();
+                               return base.OnPushModal(modal, animated);
+                       }
                }
        }
 }
index 547767e..43e6e3f 100644 (file)
@@ -63,7 +63,9 @@ namespace Xamarin.Forms
                        }
 
                        if (result != null && result.Parent != this)
+                       {
                                OnChildAdded(result);
+                       }
 
                        if (result == null)
                                throw new InvalidOperationException($"No Content found for {nameof(ShellContent)}, Title:{Title}, Route {Route}");
@@ -84,9 +86,36 @@ namespace Xamarin.Forms
 
                public ShellContent() => ((INotifyCollectionChanged)MenuItems).CollectionChanged += MenuItemsCollectionChanged;
 
-
+               internal bool IsVisibleContent => Parent is ShellSection shellSection && shellSection.IsVisibleSection;
                internal override ReadOnlyCollection<Element> LogicalChildrenInternal => _logicalChildrenReadOnly ?? (_logicalChildrenReadOnly = new ReadOnlyCollection<Element>(_logicalChildren));
 
+               internal override void SendDisappearing()
+               {
+                       base.SendDisappearing();
+                       ((ContentCache ?? Content) as Page)?.SendDisappearing();
+               }
+
+               internal override void SendAppearing()
+               {
+                       // only fire Appearing when the Content Page exists on the ShellContent
+                       var content = ContentCache ?? Content;
+                       if (content == null)
+                               return;
+
+                       base.SendAppearing();
+                       ((ContentCache ?? Content) as Page)?.SendAppearing();
+               }
+
+               protected override void OnChildAdded(Element child)
+               {
+                       base.OnChildAdded(child);
+                       if (child is Page page && IsVisibleContent)
+                       {
+                               SendAppearing();
+                               page.SendAppearing();
+                       }
+               }
+
                Page ContentCache
                {
                        get => _contentCache;
diff --git a/Xamarin.Forms.Core/Shell/ShellExtensions.cs b/Xamarin.Forms.Core/Shell/ShellExtensions.cs
new file mode 100644 (file)
index 0000000..37aaf93
--- /dev/null
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Xamarin.Forms.Core
+{
+       internal static class ShellExtensions
+       {
+               public static T SearchForRoute<T>(this Shell shell, string route) where T : BaseShellItem =>
+                       (T)SearchForRoute(shell, route);
+
+               public static BaseShellItem SearchForRoute(this Shell shell, string route) =>
+                       SearchForPart(shell, (p) => p.Route == route);
+
+
+               public static T SearchForRoute<T>(this BaseShellItem shell, string route) where T : BaseShellItem =>
+                       (T)SearchForRoute(shell, route);
+
+               public static BaseShellItem SearchForRoute(this BaseShellItem shell, string route) =>
+                       SearchForPart(shell, (p) => p.Route == route);
+
+
+               public static BaseShellItem SearchForPart(this Shell shell, Func<BaseShellItem, bool> searchBy)
+               {
+                       for (var i = 0; i < shell.Items.Count; i++)
+                       {
+                               var result = SearchForPart(shell.Items[i], searchBy);
+                               if (result != null)
+                                       return result;
+                       }
+
+                       return null;
+               }
+
+               public static BaseShellItem SearchForPart(this BaseShellItem part, Func<BaseShellItem, bool> searchBy)
+               {
+                       if (searchBy(part))
+                               return part;
+
+                       BaseShellItem baseShellItem = null;
+                       switch (part)
+                       {
+                               case ShellItem item:
+                                       foreach (var section in item.Items)
+                                       {
+                                               baseShellItem = SearchForPart(section, searchBy);
+                                               if (baseShellItem != null)
+                                                       return baseShellItem;
+                                       }
+                                       break;
+                               case ShellSection section:
+                                       foreach (var content in section.Items)
+                                       {
+                                               baseShellItem = SearchForPart(content, searchBy);
+                                               if (baseShellItem != null)
+                                                       return baseShellItem;
+                                       }
+                                       break;
+                       }
+
+                       return null;
+               }
+       }
+}
index e8f883b..a9d4055 100644 (file)
@@ -41,20 +41,6 @@ namespace Xamarin.Forms
 
                #region IShellItemController
 
-               internal Task GoToPart(NavigationRequest request, Dictionary<string, string> queryData)
-               {
-                       var shellSection = request.Request.Section;
-
-                       if (shellSection == null)
-                               return Task.FromResult(true);
-
-                       Shell.ApplyQueryAttributes(shellSection, queryData, request.Request.Content == null);
-
-                       if (CurrentItem != shellSection)
-                               SetValueFromRenderer(CurrentItemProperty, shellSection);
-
-                       return shellSection.GoToPart(request, queryData);
-               }
 
                bool IShellItemController.ProposeSection(ShellSection shellSection, bool setValue)
                {
@@ -205,7 +191,15 @@ namespace Xamarin.Forms
 
                static void OnCurrentItemChanged(BindableObject bindable, object oldValue, object newValue)
                {
+                       if (oldValue is BaseShellItem oldShellItem)
+                               oldShellItem.SendDisappearing();
+
                        var shellItem = (ShellItem)bindable;
+                       if (shellItem.Parent is Shell parentShell && parentShell.CurrentItem == shellItem)
+                       {
+                               if (newValue is BaseShellItem newShellItem)
+                                       newShellItem.SendAppearing();
+                       }
 
                        if (shellItem.Parent is IShellController shell)
                        {
@@ -231,6 +225,24 @@ namespace Xamarin.Forms
                        }
 
                        SendStructureChanged();
-               }       
+               }
+
+               internal override void SendAppearing()
+               {
+                       base.SendAppearing();
+                       if(CurrentItem != null && Parent is Shell shell && shell.CurrentItem == this)
+                       {
+                               CurrentItem.SendAppearing();
+                       }
+               }
+
+               internal override void SendDisappearing()
+               {
+                       base.SendDisappearing();
+                       if (CurrentItem != null)
+                       {
+                               CurrentItem.SendDisappearing();
+                       }
+               }
        }
 }
index 7eed4b9..f63f203 100644 (file)
@@ -49,7 +49,7 @@ namespace Xamarin.Forms
                        {
                                if (_navStack.Count > 1)
                                        return _navStack[_navStack.Count - 1];
-                               return ((IShellContentController)CurrentItem).Page;
+                               return ((IShellContentController)CurrentItem)?.Page;
                        }
                }
 
@@ -67,30 +67,6 @@ namespace Xamarin.Forms
                        callback(DisplayedPage);
                }
 
-               internal Task GoToPart(NavigationRequest request, Dictionary<string, string> 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);
@@ -294,6 +270,7 @@ namespace Xamarin.Forms
                                        DisplayedPage = currentItem.Page;
                        }
 
+                       PresentedPageAppearing();
                }
 
                protected override void OnChildAdded(Element child)
@@ -381,14 +358,17 @@ namespace Xamarin.Forms
 
                        if (!allow)
                                return null;
-
+                                               
                        var page = _navStack[_navStack.Count - 1];
                        var args = new NavigationRequestedEventArgs(page, animated)
                        {
                                RequestType = NavigationRequestType.Pop
                        };
 
+                       PresentedPageDisappearing();
                        _navStack.Remove(page);
+                       PresentedPageAppearing();
+
                        SendAppearanceChanged();
                        _navigationRequested?.Invoke(this, args);
                        if (args.Task != null)
@@ -433,9 +413,11 @@ namespace Xamarin.Forms
 
                        for (int i = 1; i < oldStack.Count; i++)
                        {
+                               oldStack[i].SendDisappearing();
                                RemovePage(oldStack[i]);
                        }
 
+                       PresentedPageAppearing();
                        SendUpdateCurrentState(ShellNavigationSource.PopToRoot);
                }
 
@@ -454,13 +436,15 @@ namespace Xamarin.Forms
 
                        if (!allow)
                                return Task.FromResult(true);
-
+                                               
                        var args = new NavigationRequestedEventArgs(page, animated)
                        {
                                RequestType = NavigationRequestType.Push
                        };
 
+                       PresentedPageDisappearing();
                        _navStack.Add(page);
+                       PresentedPageAppearing();
                        AddPage(page);
                        SendAppearanceChanged();
                        _navigationRequested?.Invoke(this, args);
@@ -477,6 +461,7 @@ namespace Xamarin.Forms
                        if (!_navStack.Contains(page))
                                return;
 
+                       bool currentPage = (((IShellSectionController)this).PresentedPage) == page;
                        var stack = _navStack.ToList();
                        stack.Remove(page);
                        var allow = ((IShellController)Shell).ProposeNavigation(
@@ -491,8 +476,13 @@ namespace Xamarin.Forms
                        if (!allow)
                                return;
 
+                       if(currentPage)
+                               PresentedPageDisappearing();
                        _navStack.Remove(page);
 
+                       if(currentPage)
+                               PresentedPageAppearing();
+
                        SendAppearanceChanged();
                        RemovePage(page);
                        var args = new NavigationRequestedEventArgs(page, false)
@@ -504,10 +494,36 @@ namespace Xamarin.Forms
                        SendUpdateCurrentState(ShellNavigationSource.Remove);
                }
 
+               internal bool IsVisibleSection => Parent?.Parent is Shell shell && shell.CurrentItem?.CurrentItem == this;
+               void PresentedPageDisappearing()
+               {
+                       if (this is IShellSectionController sectionController)
+                       {                               
+                               CurrentItem?.SendDisappearing();
+                               sectionController.PresentedPage?.SendDisappearing();
+                       }
+               }
+
+               void PresentedPageAppearing()
+               {
+                       if (IsVisibleSection && this is IShellSectionController sectionController)
+                       {
+                               if(_navStack.Count == 1)
+                                       CurrentItem?.SendAppearing();
+
+                               sectionController.PresentedPage?.SendAppearing();
+                       }
+               }
+
                static void OnCurrentItemChanged(BindableObject bindable, object oldValue, object newValue)
                {
                        var shellSection = (ShellSection)bindable;
 
+                       if (oldValue is ShellContent oldShellItem)
+                               oldShellItem.SendDisappearing();
+
+                       shellSection.PresentedPageAppearing();
+
                        if (shellSection.Parent?.Parent is IShellController shell)
                        {
                                shell.UpdateCurrentState(ShellNavigationSource.ShellSectionChanged);
@@ -558,6 +574,19 @@ namespace Xamarin.Forms
                        }
                }
 
+               internal override void SendDisappearing()
+               {
+                       base.SendDisappearing();
+                       PresentedPageDisappearing();
+
+               }
+
+               internal override void SendAppearing()
+               {
+                       base.SendAppearing();
+                       PresentedPageAppearing();
+               }
+
                class NavigationImpl : NavigationProxy
                {
                        readonly ShellSection _owner;
@@ -575,6 +604,29 @@ namespace Xamarin.Forms
                        protected override Task OnPushAsync(Page page, bool animated) => _owner.OnPushAsync(page, animated);
 
                        protected override void OnRemovePage(Page page) => _owner.OnRemovePage(page);
+
+                       protected override Task<Page> OnPopModal(bool animated)
+                       {
+                               if (ModalStack.Count > 0)
+                                       ModalStack[ModalStack.Count - 1].SendDisappearing();
+
+                               if(ModalStack.Count == 1)
+                               {
+                                       _owner.PresentedPageAppearing();
+                               }
+
+                               return base.OnPopModal(animated);
+                       }
+                       protected override Task OnPushModal(Page modal, bool animated)
+                       {
+                               if (ModalStack.Count == 0)
+                               {
+                                       _owner.PresentedPageDisappearing();
+                               }
+
+                               modal.SendAppearing();
+                               return base.OnPushModal(modal, animated);
+                       }
                }
        }
 }
index 44689d2..db9531e 100644 (file)
@@ -46,18 +46,11 @@ namespace Xamarin.Forms.Platform.Android
 
                        if (result)
                        {
-                               var page = ((IShellContentController)shellContent).Page;
-                               if (page == null)
-                                       throw new ArgumentNullException(nameof(page), "Shell Content Page is Null");
-
-                               ShellSection.SetValueFromRenderer(ShellSection.CurrentItemProperty, shellContent);
-
-                               _toolbarTracker.Page = page;
+                               UpdateCurrentItem(shellContent);
                        }
                        else
                        {
                                _selecting = true;
-                               var index = ShellSection.Items.IndexOf(ShellSection.CurrentItem);
 
                                // Android doesn't really appreciate you calling SetCurrentItem inside a OnPageSelected callback.
                                // It wont crash but the way its programmed doesn't really anticipate re-entrancy around that method
@@ -65,12 +58,30 @@ namespace Xamarin.Forms.Platform.Android
 
                                Device.BeginInvokeOnMainThread(() =>
                                {
-                                       _viewPager.SetCurrentItem(index, false);
+                                       if (position < _viewPager.ChildCount && _toolbarTracker != null)
+                                       {
+                                               _viewPager.SetCurrentItem(position, false);
+                                               UpdateCurrentItem(shellContent);
+                                       }
+
                                        _selecting = false;
                                });
                        }
                }
 
+               void UpdateCurrentItem(ShellContent content)
+               {
+                       if (_toolbarTracker == null)
+                               return;
+
+                       var page = ((IShellContentController)content).Page;
+                       if (page == null)
+                               throw new ArgumentNullException(nameof(page), "Shell Content Page is Null");
+
+                       ShellSection.SetValueFromRenderer(ShellSection.CurrentItemProperty, content);
+                       _toolbarTracker.Page = page;
+               }
+
                #endregion IOnPageChangeListener
 
                #region IAppearanceObserver