diff --git a/src/Compatibility/Core/src/Android/AppCompat/FlyoutPageContainer.cs b/src/Compatibility/Core/src/Android/AppCompat/FlyoutPageContainer.cs index 3f78667948e4..d1ae9d6c44e0 100644 --- a/src/Compatibility/Core/src/Android/AppCompat/FlyoutPageContainer.cs +++ b/src/Compatibility/Core/src/Android/AppCompat/FlyoutPageContainer.cs @@ -222,7 +222,7 @@ protected override void Dispose(bool disposing) if (disposing) { - if (_currentFragment != null && !FragmentManager.IsDestroyed) + if (_currentFragment != null && !FragmentManager.IsDestroyed(Context)) { FragmentTransaction transaction = FragmentManager.BeginTransactionEx(); transaction.RemoveEx(_currentFragment); diff --git a/src/Compatibility/Core/src/Android/AppCompat/FormsFragmentPagerAdapter.cs b/src/Compatibility/Core/src/Android/AppCompat/FormsFragmentPagerAdapter.cs index 965aaed98ef5..992b18c70a8c 100644 --- a/src/Compatibility/Core/src/Android/AppCompat/FormsFragmentPagerAdapter.cs +++ b/src/Compatibility/Core/src/Android/AppCompat/FormsFragmentPagerAdapter.cs @@ -84,7 +84,7 @@ protected override void Dispose(bool disposing) _page = null; - if (!_fragmentManager.IsDestroyed) + if (!_fragmentManager.IsDestroyed(_page?.Handler?.MauiContext?.Context)) { FragmentTransaction transaction = _fragmentManager.BeginTransactionEx(); diff --git a/src/Controls/samples/Controls.Sample/Pages/Compatibility/TabbedPageGallery.xaml b/src/Controls/samples/Controls.Sample/Pages/Compatibility/TabbedPageGallery.xaml index 0ec45a7e3e32..fb03f1294eca 100644 --- a/src/Controls/samples/Controls.Sample/Pages/Compatibility/TabbedPageGallery.xaml +++ b/src/Controls/samples/Controls.Sample/Pages/Compatibility/TabbedPageGallery.xaml @@ -3,16 +3,9 @@ xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="Maui.Controls.Sample.Pages.TabbedPageGallery" + xmlns:views="clr-namespace:Maui.Controls.Sample.Pages" Title="Tabbed Page"> - - - - - - - - - - - + + + \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample/Pages/Compatibility/TabbedPageGallery.xaml.cs b/src/Controls/samples/Controls.Sample/Pages/Compatibility/TabbedPageGallery.xaml.cs index 61b189504f44..53d7807573e1 100644 --- a/src/Controls/samples/Controls.Sample/Pages/Compatibility/TabbedPageGallery.xaml.cs +++ b/src/Controls/samples/Controls.Sample/Pages/Compatibility/TabbedPageGallery.xaml.cs @@ -14,77 +14,5 @@ public TabbedPageGallery() this.Children.Add(new NavigationGallery()); this.Children.Add(new NavigationPage(new NavigationGallery()) { Title = "With Nav Page" }); } - - void OnTabbedPageAsRoot(object sender, EventArgs e) - { - var topTabs = - new TabbedPage() - { - Children = - { - Handler.MauiContext.Services.GetRequiredService(), - new NavigationPage(new Pages.NavigationGallery()) { Title = "Navigation Gallery" } - } - }; - - this.Handler?.DisconnectHandler(); - Application.Current.MainPage?.Handler?.DisconnectHandler(); - Application.Current.MainPage = topTabs; - } - - void OnSetToBottomTabs(object sender, EventArgs e) - { - var bottomTabs = new TabbedPage() - { - Children = - { - Handler.MauiContext.Services.GetRequiredService(), - new NavigationPage(new Pages.NavigationGallery()) { Title = "Navigation Gallery" } - } - }; - - this.Handler?.DisconnectHandler(); - Application.Current.MainPage?.Handler?.DisconnectHandler(); - - AndroidSpecific.TabbedPage.SetToolbarPlacement(bottomTabs, AndroidSpecific.ToolbarPlacement.Bottom); - Application.Current.MainPage = bottomTabs; - } - - void OnChangeTabIndex(object sender, EventArgs e) - { - CurrentPage = Children[1]; - } - - void OnToggleTabBar(object sender, EventArgs e) - { - if ((this.BarBackground as SolidColorBrush)?.Color == SolidColorBrush.Purple.Color) - this.BarBackground = null; - else - this.BarBackground = SolidColorBrush.Purple; - } - - void OnToggleTabBarTextColor(object sender, EventArgs e) - { - if (this.BarTextColor == Colors.Green) - this.BarTextColor = null; - else - this.BarTextColor = Colors.Green; - } - - void OnToggleTabItemUnSelectedColor(object sender, EventArgs e) - { - if (this.UnselectedTabColor == Colors.Blue) - this.UnselectedTabColor = null; - else - this.UnselectedTabColor = Colors.Blue; - } - - void OnToggleTabItemSelectedColor(object sender, EventArgs e) - { - if (this.SelectedTabColor == Colors.Pink) - this.SelectedTabColor = null; - else - this.SelectedTabColor = Colors.Pink; - } } } \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample/Pages/Compatibility/TabbedPageGalleryMainPage.xaml b/src/Controls/samples/Controls.Sample/Pages/Compatibility/TabbedPageGalleryMainPage.xaml new file mode 100644 index 000000000000..ffc91f8dd240 --- /dev/null +++ b/src/Controls/samples/Controls.Sample/Pages/Compatibility/TabbedPageGalleryMainPage.xaml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample/Pages/Compatibility/TabbedPageGalleryMainPage.xaml.cs b/src/Controls/samples/Controls.Sample/Pages/Compatibility/TabbedPageGalleryMainPage.xaml.cs new file mode 100644 index 000000000000..4ef4abb0b0b4 --- /dev/null +++ b/src/Controls/samples/Controls.Sample/Pages/Compatibility/TabbedPageGalleryMainPage.xaml.cs @@ -0,0 +1,116 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Graphics; +using AndroidSpecific = Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific; + +namespace Maui.Controls.Sample.Pages +{ + public partial class TabbedPageGalleryMainPage + { + public TabbedPageGalleryMainPage() + { + InitializeComponent(); + } + + TabbedPage _tabbedPage; + TabbedPage GetTabbedPage() => _tabbedPage ??= (TabbedPage)Parent; + + void SetNewMainPage(Page page) + { + Application.Current.Windows[0].Page = page; + } + + void OnTabbedPageAsRoot(object sender, EventArgs e) + { + var topTabs = + new TabbedPage() + { + Children = + { + Handler.MauiContext.Services.GetRequiredService(), + new NavigationPage(new Pages.NavigationGallery()) { Title = "Navigation Gallery" } + } + }; + + SetNewMainPage(topTabs); + } + + void OnSetToBottomTabs(object sender, EventArgs e) + { + var bottomTabs = new TabbedPage() + { + Children = + { + Handler.MauiContext.Services.GetRequiredService(), + new NavigationPage(new Pages.NavigationGallery()) { Title = "Navigation Gallery" } + } + }; + + SetNewMainPage(bottomTabs); + AndroidSpecific.TabbedPage.SetToolbarPlacement(bottomTabs, AndroidSpecific.ToolbarPlacement.Bottom); + Application.Current.MainPage = bottomTabs; + } + + void OnChangeTabIndex(object sender, EventArgs e) + { + GetTabbedPage().CurrentPage = GetTabbedPage().Children[1]; + } + + void OnToggleTabBar(object sender, EventArgs e) + { + if ((GetTabbedPage().BarBackground as SolidColorBrush)?.Color == SolidColorBrush.Purple.Color) + GetTabbedPage().BarBackground = null; + else + GetTabbedPage().BarBackground = SolidColorBrush.Purple; + } + + void OnToggleTabBarTextColor(object sender, EventArgs e) + { + if (GetTabbedPage().BarTextColor == Colors.Green) + GetTabbedPage().BarTextColor = null; + else + GetTabbedPage().BarTextColor = Colors.Green; + } + + void OnToggleTabItemUnSelectedColor(object sender, EventArgs e) + { + if (GetTabbedPage().UnselectedTabColor == Colors.Blue) + GetTabbedPage().UnselectedTabColor = null; + else + GetTabbedPage().UnselectedTabColor = Colors.Blue; + } + + void OnToggleTabItemSelectedColor(object sender, EventArgs e) + { + if (GetTabbedPage().SelectedTabColor == Colors.Pink) + GetTabbedPage().SelectedTabColor = null; + else + GetTabbedPage().SelectedTabColor = Colors.Pink; + } + + void OnRemoveTab(object sender, EventArgs e) + { + if (GetTabbedPage().Children.LastOrDefault() is TabbedPageGalleryMainPage mainPage) + { + GetTabbedPage().Children.Remove(mainPage); + } + } + + void OnRemoveAllTabs(object sender, EventArgs e) + { + while (GetTabbedPage().Children.LastOrDefault() is TabbedPageGalleryMainPage mainPage) + { + GetTabbedPage().Children.Remove(mainPage); + } + } + + void OnAddTab(object sender, EventArgs e) + { + GetTabbedPage() + .Children + .Add(new TabbedPageGalleryMainPage() { Title = $"Tab {GetTabbedPage().Children.Count}" }); + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFragmentContainer.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFragmentContainer.cs index 8907188e6015..e766431077d0 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFragmentContainer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellFragmentContainer.cs @@ -23,32 +23,9 @@ public ShellFragmentContainer(ShellContent shellContent, IMauiContext mauiContex public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { _page = ((IShellContentController)ShellContentTab).GetOrCreateContent(); + _page.ToPlatform(_mauiContext, RequireContext(), inflater, ChildFragmentManager); - IMauiContext mauiContext = null; - - // If the page has already been created with a handler then we just let it retain the same - // Handler and MauiContext - // But we want to update the inflater and ChildFragmentManager to match - // the handlers new home - if (_page.Handler?.MauiContext is MauiContext scopedMauiContext) - { - // If this page comes to us from a different activity then don't reuse it - // disconnect the handler so it can recreate against new MauiContext - if (scopedMauiContext.GetActivity() == Context.GetActivity()) - { - scopedMauiContext.AddWeakSpecific(ChildFragmentManager); - scopedMauiContext.AddWeakSpecific(inflater); - mauiContext = scopedMauiContext; - } - else - { - _page.Handler.DisconnectHandler(); - } - } - - mauiContext ??= _mauiContext.MakeScoped(layoutInflater: inflater, fragmentManager: ChildFragmentManager); - - return new ShellPageContainer(RequireContext(), (IPlatformViewHandler)_page.ToHandler(mauiContext), true) + return new ShellPageContainer(RequireContext(), (IPlatformViewHandler)_page.Handler, true) { LayoutParameters = new LP(LP.MatchParent, LP.MatchParent) }; diff --git a/src/Controls/src/Core/HandlerImpl/NavigationPage/NavigationPage.Impl.cs b/src/Controls/src/Core/HandlerImpl/NavigationPage/NavigationPage.Impl.cs index d0c45a7b042d..6b92d1609f61 100644 --- a/src/Controls/src/Core/HandlerImpl/NavigationPage/NavigationPage.Impl.cs +++ b/src/Controls/src/Core/HandlerImpl/NavigationPage/NavigationPage.Impl.cs @@ -181,32 +181,36 @@ async Task SendHandlerUpdateAsync( // Wait for pending navigation tasks to finish await SemaphoreSlim.WaitAsync(); - var currentNavRequestTaskSource = new TaskCompletionSource(); - _allPendingNavigationCompletionSource ??= new TaskCompletionSource(); - - if (CurrentNavigationTask == null) - { - CurrentNavigationTask = _allPendingNavigationCompletionSource.Task; - } - else if (CurrentNavigationTask != _allPendingNavigationCompletionSource.Task) + // If our handler was removed while waiting then don't do anything + if (Handler != null) { - throw new InvalidOperationException("Pending Navigations still processing"); - } + var currentNavRequestTaskSource = new TaskCompletionSource(); + _allPendingNavigationCompletionSource ??= new TaskCompletionSource(); - _currentNavigationCompletionSource = currentNavRequestTaskSource; + if (CurrentNavigationTask == null) + { + CurrentNavigationTask = _allPendingNavigationCompletionSource.Task; + } + else if (CurrentNavigationTask != _allPendingNavigationCompletionSource.Task) + { + throw new InvalidOperationException("Pending Navigations still processing"); + } + + _currentNavigationCompletionSource = currentNavRequestTaskSource; - // We create a new list to send to the handler because the structure backing - // The Navigation stack isn't immutable - var immutableNavigationStack = new List(NavigationStack); - firePostNavigatingEvents?.Invoke(); + // We create a new list to send to the handler because the structure backing + // The Navigation stack isn't immutable + var immutableNavigationStack = new List(NavigationStack); + firePostNavigatingEvents?.Invoke(); - // Create the request for the handler - var request = new NavigationRequest(immutableNavigationStack, animated); - ((IStackNavigation)this).RequestNavigation(request); + // Create the request for the handler + var request = new NavigationRequest(immutableNavigationStack, animated); + ((IStackNavigation)this).RequestNavigation(request); - // Wait for the handler to finish processing the navigation - // This task completes once the handler calls INavigationView.Finished - await currentNavRequestTaskSource.Task; + // Wait for the handler to finish processing the navigation + // This task completes once the handler calls INavigationView.Finished + await currentNavRequestTaskSource.Task; + } } finally { @@ -245,6 +249,13 @@ private protected override void OnHandlerChangedCore() }) .FireAndForget(Handler); } + + // If the handler is disconnected and we're still waiting for updates from the handler + // Just complete any waits + if (Handler == null && _waitingCount > 0) + { + ((IStackNavigation)this).NavigationFinished(this.NavigationStack); + } } // Once we get all platforms over to the new APIs diff --git a/src/Controls/src/Core/Platform/Android/AdapterItemKey.cs b/src/Controls/src/Core/Platform/Android/AdapterItemKey.cs new file mode 100644 index 000000000000..415965a3255d --- /dev/null +++ b/src/Controls/src/Core/Platform/Android/AdapterItemKey.cs @@ -0,0 +1,77 @@ +using System; +using AView = Android.Views.View; + +namespace Microsoft.Maui.Controls.Platform +{ + class AdapterItemKey + { + Page _page; + Action? _markInvalid; + object? _platformView; + bool _disconnected; + + public AdapterItemKey(Page page, Action markInvalid) + { + // We aren't setting the platform view in the ctor because + // the PlatformView might not be valid. It might + // be from a destroyed context or from a page that was moved + // from a different location. + _page = page; + _markInvalid = markInvalid; + _page.HandlerChanging += OnHandlerChanging; + _page.HandlerChanged += OnHandlerChanged; + ItemId = AView.GenerateViewId(); + } + + public bool Disconnected => _disconnected; + public Page Page => _page; + public long ItemId { get; } + public void Disconnect() + { + _disconnected = true; + _markInvalid?.Invoke(this); + + if (_page != null) + { + _page.HandlerChanging -= OnHandlerChanging; + _page.HandlerChanged -= OnHandlerChanged; + } + + _platformView = null; + } + + void OnHandlerChanging(object? sender, HandlerChangingEventArgs e) + { + if (_platformView != null) + Disconnect(); + } + + // This will only ever fire once. This is purely waiting + // for the xplat view to get filled in with a PlatformView. + // Once a handler is set, then this key is locked to that platformview. + // If that handler gets disconnected (OnHandlerChanging) then we have to + // disconnect this key, and once the same page is requested again a new key/handler + // will need to get created. We can't reuse keys for different PlatformViews. + // The ItemKey/PlatformView relationship is immutable + void OnHandlerChanged(object? sender, EventArgs e) + { + if (_disconnected) + { + if (sender is Page page) + page.HandlerChanged -= OnHandlerChanged; + + return; + } + + SetToStableView(); + } + + internal void SetToStableView() + { + _platformView = _page.Handler?.PlatformView; + + if (_platformView != null) + _page.HandlerChanged -= OnHandlerChanged; + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/FragmentContainer.cs b/src/Controls/src/Core/Platform/Android/FragmentContainer.cs index 05cb43448076..a8fce5d7341d 100644 --- a/src/Controls/src/Core/Platform/Android/FragmentContainer.cs +++ b/src/Controls/src/Core/Platform/Android/FragmentContainer.cs @@ -1,41 +1,32 @@ -#nullable disable using System; using Android.Content; using Android.OS; -using Android.Runtime; using Android.Views; using AndroidX.Fragment.App; using Microsoft.Maui.Controls.Platform; -using Microsoft.Maui.Controls.PlatformConfiguration.AndroidSpecific.AppCompat; using AView = Android.Views.View; namespace Microsoft.Maui.Controls.Platform { internal class FragmentContainer : Fragment { - readonly WeakReference _pageRenderer; + AView? _pageContainer; readonly IMauiContext _mauiContext; - Action _onCreateCallback; - AView _pageContainer; - IPlatformViewHandler _viewhandler; - AView PlatformView => _viewhandler?.PlatformView as AView; + Action? _onCreateCallback; + ViewGroup? _parent; + AdapterItemKey _adapterItemKey; - public FragmentContainer(IMauiContext mauiContext) + public FragmentContainer(AdapterItemKey adapterItemKey, IMauiContext mauiContext) { _mauiContext = mauiContext; + _adapterItemKey = adapterItemKey; } - public FragmentContainer(Page page, IMauiContext mauiContext) : this(mauiContext) - { - _pageRenderer = new WeakReference(page); - _mauiContext = mauiContext; - } + public Page Page => _adapterItemKey.Page; - public virtual Page Page => (Page)_pageRenderer?.Target; - - public static FragmentContainer CreateInstance(Page page, IMauiContext mauiContext) + public static FragmentContainer CreateInstance(AdapterItemKey adapterItemKey, IMauiContext mauiContext) { - return new FragmentContainer(page, mauiContext) { Arguments = new Bundle() }; + return new FragmentContainer(adapterItemKey, mauiContext) { Arguments = new Bundle() }; } public void SetOnCreateCallback(Action callback) @@ -43,35 +34,23 @@ public void SetOnCreateCallback(Action callback) _onCreateCallback = callback; } - ViewGroup _parent; - - public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) + public override AView OnCreateView(LayoutInflater inflater, ViewGroup? container, Bundle? savedInstanceState) { _parent = container ?? _parent; - if (Page != null) - { - _pageContainer = Page?.Handler?.PlatformView as AView; - - if (_pageContainer == null) - { - var scopedContext = - _mauiContext.MakeScoped(inflater, ChildFragmentManager); - - _pageContainer = Page.ToPlatform(scopedContext); - _viewhandler = (IPlatformViewHandler)Page.Handler; - } - else - { - _parent = _parent ?? (_pageContainer.Parent as ViewGroup); - } - - _onCreateCallback?.Invoke(_pageContainer); + _pageContainer = Page.ToPlatform(_mauiContext, RequireContext(), inflater, ChildFragmentManager); + _adapterItemKey.SetToStableView(); + _parent = _parent ?? (_pageContainer.Parent as ViewGroup); + _onCreateCallback?.Invoke(_pageContainer); - return _pageContainer; - } + return _pageContainer; + } - return null; + public override void OnDestroy() + { + base.OnDestroy(); + if (Context.IsDestroyed()) + Page?.Handler?.DisconnectHandler(); } public override void OnResume() diff --git a/src/Controls/src/Core/Platform/Android/MultiPageFragmentStateAdapter.cs b/src/Controls/src/Core/Platform/Android/MultiPageFragmentStateAdapter.cs index cad1fb98f66c..346251c95020 100644 --- a/src/Controls/src/Core/Platform/Android/MultiPageFragmentStateAdapter.cs +++ b/src/Controls/src/Core/Platform/Android/MultiPageFragmentStateAdapter.cs @@ -1,5 +1,7 @@ -#nullable disable +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using AndroidX.Fragment.App; using AndroidX.ViewPager2.Adapter; @@ -9,6 +11,7 @@ internal class MultiPageFragmentStateAdapter<[DynamicallyAccessedMembers(Bindabl { MultiPage _page; readonly IMauiContext _context; + List keys = new List(); public MultiPageFragmentStateAdapter( MultiPage page, FragmentManager fragmentManager, IMauiContext context) @@ -24,13 +27,66 @@ public MultiPageFragmentStateAdapter( public override Fragment CreateFragment(int position) { - var fragment = FragmentContainer.CreateInstance(_page.Children[position], _context); + var fragment = FragmentContainer.CreateInstance(GetItemIdByPosition(position), _context); return fragment; } public override long GetItemId(int position) { - return _page.Children[position].GetHashCode(); + return GetItemIdByPosition(position).ItemId; + } + + public override bool ContainsItem(long itemId) + { + return GetItemByItemId(itemId) != null; + } + + AdapterItemKey GetItemIdByPosition(int position) + { + CheckItemKeys(); + var page = _page.Children[position]; + for (var i = 0; i < keys.Count; i++) + { + var item = keys[i]; + if (item.Page == page) + { + return item; + } + } + + var itemKey = new AdapterItemKey(page, (ik) => keys.Remove(ik)); + keys.Add(itemKey); + + return itemKey; + } + + AdapterItemKey? GetItemByItemId(long itemId) + { + CheckItemKeys(); + for (var i = 0; i < keys.Count; i++) + { + var item = keys[i]; + if (item.ItemId == itemId) + { + return item; + } + } + + return null; + } + + void CheckItemKeys() + { + for (var i = keys.Count - 1; i >= 0; i--) + { + var item = keys[i]; + + if (!_page.Children.Contains(item.Page)) + { + // Disconnect will remove the ItemKey from the keys list + item.Disconnect(); + } + } } } -} \ No newline at end of file +} diff --git a/src/Controls/src/Core/Platform/Android/TabbedPageManager.cs b/src/Controls/src/Core/Platform/Android/TabbedPageManager.cs index c77758be10e8..4d7e1568d6c8 100644 --- a/src/Controls/src/Core/Platform/Android/TabbedPageManager.cs +++ b/src/Controls/src/Core/Platform/Android/TabbedPageManager.cs @@ -59,6 +59,7 @@ internal class TabbedPageManager Color _currentBarSelectedItemColor; ColorStateList _currentBarTextColorStateList; bool _tabItemStyleLoaded; + TabLayoutMediator _tabLayoutMediator; NavigationRootManager NavigationRootManager { get; } @@ -183,7 +184,7 @@ void RemoveTabs() .GetNavigationRootManager() .FragmentManager; - if (fragmentManager.IsAlive() && !fragmentManager.IsDestroyed) + if (!fragmentManager.IsDestroyed(_context?.Context)) { SetContentBottomMargin(0); @@ -210,22 +211,26 @@ void OnTabbedPageAppearing(object sender, EventArgs e) SetTabLayout(); } + void RootViewChanged(object sender, EventArgs e) + { + if (sender is NavigationRootManager rootManager) + { + rootManager.RootViewChanged -= RootViewChanged; + SetTabLayout(); + } + } + internal void SetTabLayout() { int id; var rootManager = _context.GetNavigationRootManager(); + _tabItemStyleLoaded = false; if (rootManager.RootView == null) { rootManager.RootViewChanged += RootViewChanged; - void RootViewChanged(object sender, EventArgs e) - { - rootManager.RootViewChanged -= RootViewChanged; - SetTabLayout(); - } - return; } @@ -278,7 +283,7 @@ void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs { BottomNavigationView bottomNavigationView = _bottomNavigationView; - adapter.NotifyDataSetChanged(); + NotifyDataSetChanged(); if (Element.Children.Count == 0) { @@ -286,7 +291,7 @@ void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs } else { - SetupBottomNavigationView(e); + SetupBottomNavigationView(); bottomNavigationView.SetOnItemSelectedListener(_listeners); } @@ -296,16 +301,21 @@ void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs { TabLayout tabs = _tabLayout; - adapter.NotifyDataSetChanged(); + NotifyDataSetChanged(); if (Element.Children.Count == 0) { tabs.RemoveAllTabs(); tabs.SetupWithViewPager(null); + _tabLayoutMediator?.Detach(); + _tabLayoutMediator = null; } else { - new TabLayoutMediator(tabs, _viewPager, _listeners) - .Attach(); + if (_tabLayoutMediator == null) + { + _tabLayoutMediator = new TabLayoutMediator(tabs, _viewPager, _listeners); + _tabLayoutMediator.Attach(); + } UpdateTabIcons(); #pragma warning disable CS0618 // Type or member is obsolete @@ -317,6 +327,33 @@ void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs } } + void NotifyDataSetChanged() + { + if (_viewPager?.Adapter is MultiPageFragmentStateAdapter adapter) + { + var currentIndex = Element.Children.IndexOf(Element.CurrentPage); + + // If the modification to the backing collection has changed the position of the current item + // then we need to update the viewpager so it remains selected + if (_viewPager.CurrentItem != currentIndex && currentIndex < Element.Children.Count && currentIndex >= 0) + _viewPager.SetCurrentItem(Element.Children.IndexOf(Element.CurrentPage), false); + + adapter.NotifyDataSetChanged(); + } + } + + void TabSelected(TabLayout.Tab tab) + { + if (Element == null) + return; + + int selectedIndex = tab.Position; + if (Element.Children.Count > selectedIndex && selectedIndex >= 0) + Element.CurrentPage = Element.Children[selectedIndex]; + + SetIconColorFilter(tab, true); + } + void TeardownPage(Page page) { page.PropertyChanged -= OnPagePropertyChanged; @@ -421,7 +458,7 @@ internal void UpdateSwipePaging() return items; } - void SetupBottomNavigationView(NotifyCollectionChangedEventArgs e) + void SetupBottomNavigationView() { var currentIndex = Element.Children.IndexOf(Element.CurrentPage); var items = CreateTabList(); @@ -794,8 +831,13 @@ public override void OnPageSelected(int position) _previousPage = Element.CurrentPage; _tabbedPageManager._previousPage = Element.CurrentPage; } - Element.CurrentPage = Element.Children[position]; - Element.CurrentPage.SendAppearing(); + + // This only happens if all the pages have been removed + if (Element.Children.Count > 0) + { + Element.CurrentPage = Element.Children[position]; + Element.CurrentPage.SendAppearing(); + } if (IsBottomTabPlacement) _bottomNavigationView.SelectedItemId = position; @@ -835,14 +877,7 @@ void TabLayout.IOnTabSelectedListener.OnTabReselected(TabLayout.Tab tab) void TabLayout.IOnTabSelectedListener.OnTabSelected(TabLayout.Tab tab) { - if (_tabbedPageManager.Element == null) - return; - - int selectedIndex = tab.Position; - if (_tabbedPageManager.Element.Children.Count > selectedIndex && selectedIndex >= 0) - _tabbedPageManager.Element.CurrentPage = _tabbedPageManager.Element.Children[selectedIndex]; - - _tabbedPageManager.SetIconColorFilter(tab, true); + _tabbedPageManager.TabSelected(tab); } void TabLayout.IOnTabSelectedListener.OnTabUnselected(TabLayout.Tab tab) diff --git a/src/Controls/tests/Core.UnitTests/NavigationUnitTest.cs b/src/Controls/tests/Core.UnitTests/NavigationUnitTest.cs index 53e9c4e35c30..c5e1d41a81bd 100644 --- a/src/Controls/tests/Core.UnitTests/NavigationUnitTest.cs +++ b/src/Controls/tests/Core.UnitTests/NavigationUnitTest.cs @@ -21,6 +21,24 @@ public async Task HandlerUpdatesDontFireForLegacy(bool withPage) var handler = new TestNavigationHandler(); (nav as IView).Handler = handler; + Assert.Null(nav.CurrentNavigationTask); + Assert.Null(handler.CurrentNavigationRequest); + } + + [Fact] + public async Task NavigationInLimboCompletesWhenHandlerIsRemoved() + { + TestNavigationPage nav = + new TestNavigationPage(true); + + var task = nav.PushAsync(new ContentPage()); + (nav as IView).Handler = null; + await task.WaitAsync(TimeSpan.FromMilliseconds(100)); + + var handler = new TestNavigationHandler(); + (nav as IView).Handler = handler; + + await nav.PushAsync(new ContentPage()); Assert.Null(nav.CurrentNavigationTask); Assert.Null(handler.CurrentNavigationRequest); diff --git a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Android.cs b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Android.cs index 9474511e87a1..4121ac41f08b 100644 --- a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Android.cs +++ b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Android.cs @@ -50,10 +50,7 @@ Task SetupWindowForTests(IWindow window, Func runTests, IMauiCon } finally { - if (window.Handler != null) - { - window.Handler.DisconnectHandler(); - } + window.Handler?.DisconnectHandler(); fragmentManager .BeginTransaction() diff --git a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs index a9b4b3f28e7c..db2b1359573e 100644 --- a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs +++ b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.cs @@ -169,6 +169,8 @@ await SetupWindowForTests(window, async () => } await OnLoadedAsync(content as VisualElement); + + window.Activated(); #if WINDOWS await Task.Delay(10); #endif @@ -180,6 +182,10 @@ await SetupWindowForTests(window, async () => await action((THandler)cp.Content.Handler); else throw new Exception($"I can't work with {typeof(THandler)}"); + + window.Deactivated(); + window.Destroying(); + }, mauiContext); } finally @@ -336,8 +342,25 @@ protected async Task OnNavigatedToAsync(Page page, TimeSpan? timeOut = null) { await OnLoadedAsync(page, timeOut); + // Navigation Events currently aren't wired up + // correctly on iOS + if (OperatingSystem.IsIOS() && page.Parent is NavigationPage) + { + await Task.Delay(100); + return; + } + if (page.HasNavigatedTo) + { + // TabbedPage fires OnNavigated earlier than it should + if (page.Parent is TabbedPage) + await Task.Delay(10); + + if (page is IPageContainer pc) + await OnNavigatedToAsync(pc.CurrentPage); + return; + } timeOut = timeOut ?? TimeSpan.FromSeconds(2); TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); @@ -345,6 +368,11 @@ protected async Task OnNavigatedToAsync(Page page, TimeSpan? timeOut = null) page.NavigatedTo += NavigatedTo; await taskCompletionSource.Task.WaitAsync(timeOut.Value); + + // TabbedPage fires OnNavigated earlier than it should + if (page.Parent is TabbedPage) + await Task.Delay(10); + void NavigatedTo(object sender, NavigatedToEventArgs e) { taskCompletionSource.SetResult(true); diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Android.cs index 265d8ddb3b87..4bc3a557b9b6 100644 --- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.Android.cs @@ -222,44 +222,6 @@ await CreateHandlerAndAddToWindow(shell, async (handler) => }); } - - [Fact] - public async Task SwappingOutAndroidContextDoesntCrash() - { - SetupBuilder(); - - var shell = await CreateShellAsync(shell => - { - shell.Items.Add(new FlyoutItem() { Route = "FlyoutItem1", Items = { new ContentPage() }, Title = "Flyout Item" }); - shell.Items.Add(new FlyoutItem() { Route = "FlyoutItem2", Items = { new ContentPage() }, Title = "Flyout Item" }); - }); - - var window = new Controls.Window(shell); - var mauiContextStub1 = new ContextStub(MauiContext.GetApplicationServices()); - var activity = mauiContextStub1.GetActivity(); - mauiContextStub1.Context = new ContextThemeWrapper(activity, Resource.Style.Maui_MainTheme_NoActionBar); - - await CreateHandlerAndAddToWindow(window, async (handler) => - { - await OnLoadedAsync(shell.CurrentPage); - await OnNavigatedToAsync(shell.CurrentPage); - await Task.Delay(100); - await shell.GoToAsync("//FlyoutItem2"); - }, mauiContextStub1); - - var mauiContextStub2 = new ContextStub(MauiContext.GetApplicationServices()); - mauiContextStub2.Context = new ContextThemeWrapper(activity, Resource.Style.Maui_MainTheme_NoActionBar); - - await CreateHandlerAndAddToWindow(window, async (handler) => - { - await OnLoadedAsync(shell.CurrentPage); - await OnNavigatedToAsync(shell.CurrentPage); - await Task.Delay(100); - await shell.GoToAsync("//FlyoutItem1"); - await shell.GoToAsync("//FlyoutItem2"); - }, mauiContextStub2); - } - protected AView GetFlyoutPlatformView(ShellRenderer shellRenderer) { var drawerLayout = GetDrawerLayout(shellRenderer); diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs index 4c700c22342f..edf5310af658 100644 --- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs @@ -634,6 +634,48 @@ await CreateHandlerAndAddToWindow(shell, async (handler) => }); } +#if !IOS + [Fact] + public async Task ChangingToNewMauiContextDoesntCrash() + { + SetupBuilder(); + + var shell = new Shell(); + shell.Items.Add(new FlyoutItem() { Route = "FlyoutItem1", Items = { new ContentPage() }, Title = "Flyout Item" }); + shell.Items.Add(new FlyoutItem() { Route = "FlyoutItem2", Items = { new ContentPage() }, Title = "Flyout Item" }); + + + var window = new Controls.Window(shell); + var mauiContextStub1 = new ContextStub(ApplicationServices); +#if ANDROID + var activity = mauiContextStub1.GetActivity(); + mauiContextStub1.Context = new Android.Views.ContextThemeWrapper(activity, Resource.Style.Maui_MainTheme_NoActionBar); +#endif + + await CreateHandlerAndAddToWindow(window, async (handler) => + { + await OnLoadedAsync(shell.CurrentPage); + await OnNavigatedToAsync(shell.CurrentPage); + await Task.Delay(100); + await shell.GoToAsync("//FlyoutItem2"); + }, mauiContextStub1); + + var mauiContextStub2 = new ContextStub(ApplicationServices); + +#if ANDROID + mauiContextStub2.Context = new Android.Views.ContextThemeWrapper(activity, Resource.Style.Maui_MainTheme_NoActionBar); +#endif + await CreateHandlerAndAddToWindow(window, async (handler) => + { + await OnLoadedAsync(shell.CurrentPage); + await OnNavigatedToAsync(shell.CurrentPage); + await Task.Delay(100); + await shell.GoToAsync("//FlyoutItem1"); + await shell.GoToAsync("//FlyoutItem2"); + }, mauiContextStub2); + } +#endif + [Theory] [ClassData(typeof(ShellBasicNavigationTestCases))] public async Task BasicShellNavigationStructurePermutations(ShellItem[] shellItems) diff --git a/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.cs b/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.cs index 04399a637f2b..14f0304729fd 100644 --- a/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/TabbedPage/TabbedPageTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; @@ -28,7 +29,9 @@ void SetupBuilder() { handlers.AddHandler(typeof(VerticalStackLayout), typeof(LayoutHandler)); handlers.AddHandler(typeof(Toolbar), typeof(ToolbarHandler)); + handlers.AddHandler(typeof(Button), typeof(ButtonHandler)); handlers.AddHandler(); + handlers.AddHandler(); #if IOS || MACCATALYST handlers.AddHandler(typeof(TabbedPage), typeof(TabbedRenderer)); @@ -41,19 +44,210 @@ void SetupBuilder() }); } - [Fact] - public async Task PoppingTabbedPageDoesntCrash() +#if !IOS + // iOS currently can't handle recreating a handler if it's disconnecting + // This is left over behavior from Forms and will be fixed by a different PR + [Theory] + [ClassData(typeof(TabbedPagePivots))] + public async Task DisconnectEachPageHandlerAfterNavigation(bool bottomTabs, bool isSmoothScrollEnabled) + { + SetupBuilder(); + + List navPages = new List(); + var pageCount = 5; + for (int i = 0; i < pageCount; i++) + { + navPages.Add(new NavigationPage(new ContentPage() + { + Content = new Label() { Text = $"Page {i}" } + }) + { Title = $"App Page {i}" }); + } + + var tabbedPage = + CreateBasicTabbedPage(bottomTabs, isSmoothScrollEnabled, navPages); + + await CreateHandlerAndAddToWindow(new Window(tabbedPage), async (handler) => + { + for (int i = 0; i < pageCount * 2; i++) + { + var currentPage = tabbedPage.CurrentPage; + var previousPage = currentPage; + + await OnNavigatedToAsync(currentPage); + int pageIndex = tabbedPage.Children.IndexOf(currentPage) + 1; + if (pageIndex >= pageCount) + pageIndex = 0; + + var nextPage = tabbedPage.Children[pageIndex]; + tabbedPage.CurrentPage = nextPage; + await OnNavigatedToAsync(nextPage); + previousPage.Handler.DisconnectHandler(); + } + }); + } +#endif + + [Theory] + [ClassData(typeof(TabbedPagePivots))] + public async Task PoppingTabbedPageDoesntCrash(bool bottomTabs, bool isSmoothScrollEnabled) { SetupBuilder(); var navPage = new NavigationPage(new ContentPage()) { Title = "App Page" }; await CreateHandlerAndAddToWindow(new Window(navPage), async (handler) => { - await navPage.PushAsync(CreateBasicTabbedPage()); + await navPage.PushAsync(CreateBasicTabbedPage(bottomTabs, isSmoothScrollEnabled)); await navPage.PopAsync(); }); } + [Theory("Remove CurrentPage And Then Re-Add Doesnt Crash")] + [ClassData(typeof(TabbedPagePivots))] + public async Task RemoveCurrentPageAndThenReAddDoesntCrash(bool bottomTabs, bool isSmoothScrollEnabled) + { + SetupBuilder(); + + var tabbedPage = CreateBasicTabbedPage(bottomTabs, isSmoothScrollEnabled); + + var firstPage = new NavigationPage(new ContentPage()); + tabbedPage.Children.Insert(0, firstPage); + tabbedPage.CurrentPage = firstPage; + var secondPage = tabbedPage.Children[1]; + + await CreateHandlerAndAddToWindow(new Window(tabbedPage), async (handler) => + { + await OnNavigatedToAsync(firstPage); + tabbedPage.Children.Remove(firstPage); + await OnNavigatedToAsync(secondPage); + + // Validate that the second page becomes the current active page + Assert.Equal(secondPage, tabbedPage.CurrentPage); + + // add the removed page back + tabbedPage.Children.Insert(0, firstPage); + + // Validate that the second page is still the current active page + Assert.Equal(secondPage, tabbedPage.CurrentPage); + + // Validate that we can navigate back to the first page + tabbedPage.CurrentPage = firstPage; + await OnNavigatedToAsync(firstPage); + }); + } + + [Theory] + [ClassData(typeof(TabbedPagePivots))] + public async Task SettingCurrentPageToNotBePositionZeroWorks(bool bottomTabs, bool isSmoothScrollEnabled) + { + SetupBuilder(); + var tabbedPage = CreateBasicTabbedPage(bottomTabs, isSmoothScrollEnabled); + var firstPage = new NavigationPage(new ContentPage()); + tabbedPage.Children.Insert(0, firstPage); + var secondPage = tabbedPage.Children[1]; + tabbedPage.CurrentPage = secondPage; + + await CreateHandlerAndAddToWindow(new Window(tabbedPage), async (handler) => + { + await OnNavigatedToAsync(secondPage); + Assert.Equal(tabbedPage.CurrentPage, secondPage); + }); + } + + [Theory] + [ClassData(typeof(TabbedPagePivots))] + public async Task MovingBetweenMultiplePagesWithNestedNavigationPages(bool bottomTabs, bool isSmoothScrollEnabled) + { + SetupBuilder(); + + var pages = new NavigationPage[5]; + + for (var i = 0; i < pages.Length; i++) + { + string title = $"Tab {i} Root Page"; + var contentPage = new ContentPage() + { + Title = title, + Content = new Button() + { + Text = title + } + }; + + pages[i] = new NavigationPage(contentPage) + { + Title = title + }; + }; + + var tabbedPage = CreateBasicTabbedPage(bottomTabs, isSmoothScrollEnabled, pages); + + await CreateHandlerAndAddToWindow(new Window(tabbedPage), async (handler) => + { + // Navigate to each page and push a page on the stack + // Android does a lot of fragment creating and destroying on us + // So we're mainly validating that android all works alright here + for (var i = 0; i < pages.Length; i++) + { + NavigationPage navigationPage = pages[i]; + tabbedPage.CurrentPage = navigationPage; + await OnNavigatedToAsync(navigationPage.CurrentPage); + await OnLoadedAsync((navigationPage.CurrentPage as ContentPage).Content); + + var nextPage = new ContentPage() + { + Content = new Button() + { + Text = $"Tab {i} Next Page" + } + }; + await navigationPage.PushAsync(nextPage); + await OnNavigatedToAsync(nextPage); + await OnLoadedAsync(nextPage.Content); + } + + // Navigate back through and make sure nothing crashes + // and that we can pop back to our root pages + foreach (var navigationPage in pages) + { + tabbedPage.CurrentPage = navigationPage; + await OnNavigatedToAsync(navigationPage.CurrentPage); + await OnLoadedAsync((navigationPage.CurrentPage as ContentPage).Content); + await Task.Delay(200); + await navigationPage.PopAsync(); + await OnNavigatedToAsync(navigationPage.CurrentPage); + await OnLoadedAsync((navigationPage.CurrentPage as ContentPage).Content); + } + }); + } + +#if !WINDOWS + [Theory] + [ClassData(typeof(TabbedPagePivots))] + public async Task RemovingAllPagesDoesntCrash(bool bottomTabs, bool isSmoothScrollEnabled) + { + SetupBuilder(); + var tabbedPage = CreateBasicTabbedPage(bottomTabs, isSmoothScrollEnabled); + var secondPage = new NavigationPage(new ContentPage()) { Title = "Second Page" }; + tabbedPage.Children.Add(secondPage); + var firstPage = tabbedPage.Children[0]; + + await CreateHandlerAndAddToWindow(new Window(tabbedPage), async (handler) => + { + await OnNavigatedToAsync(firstPage); + + tabbedPage.Children.Remove(firstPage); + tabbedPage.Children.Remove(secondPage); + + await OnUnloadedAsync(secondPage); + tabbedPage.Children.Insert(0, secondPage); + await OnNavigatedToAsync(secondPage); + + Assert.Equal(tabbedPage.CurrentPage, secondPage); + }); + } +#endif + [Theory] #if ANDROID [InlineData(true)] @@ -89,6 +283,26 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async }); } + public class TabbedPagePivots : IEnumerable + { + public IEnumerator GetEnumerator() + { + //bottomtabs, isSmoothScrollEnabled + yield return new object[] { false, true }; +#if ANDROID + yield return new object[] { false, false }; + yield return new object[] { true, false }; + yield return new object[] { true, true }; +#endif + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + TabbedPage CreateBasicTabbedPage(bool bottomTabs = false, bool isSmoothScrollEnabled = true, IEnumerable pages = null) { pages = pages ?? new List() @@ -98,7 +312,8 @@ TabbedPage CreateBasicTabbedPage(bool bottomTabs = false, bool isSmoothScrollEna var tabs = new TabbedPage() { - Title = "Tabbed Page" + Title = "Tabbed Page", + Background = SolidColorBrush.Green }; foreach (var page in pages) diff --git a/src/Controls/tests/DeviceTests/Elements/Window/ChangingToNewMauiContextDoesntCrashTestCases.cs b/src/Controls/tests/DeviceTests/Elements/Window/ChangingToNewMauiContextDoesntCrashTestCases.cs new file mode 100644 index 000000000000..6a37500b7eba --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/Window/ChangingToNewMauiContextDoesntCrashTestCases.cs @@ -0,0 +1,41 @@ +using System.Collections; +using System.Collections.Generic; +using Microsoft.Maui.Controls; + +namespace Microsoft.Maui.DeviceTests +{ + public partial class WindowTests + { + class ChangingToNewMauiContextDoesntCrashTestCases : IEnumerable + { + private readonly List _data = new() + { + new object[] { true, typeof(NavPageWithTabbedPage) }, + new object[] { false, typeof(NavPageWithTabbedPage) }, + new object[] { true, typeof(FlyoutPageWithNavPageAndTabbedPage) }, + new object[] { false, typeof(FlyoutPageWithNavPageAndTabbedPage) }, + }; + + public IEnumerator GetEnumerator() => _data.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + class NavPageWithTabbedPage : NavigationPage + { + public NavPageWithTabbedPage() : base(new TabbedPage() { Children = { new ContentPage() } }) + { + Title = "Detail"; + } + } + + class FlyoutPageWithNavPageAndTabbedPage : FlyoutPage + { + public FlyoutPageWithNavPageAndTabbedPage() : base() + { + Detail = new NavPageWithTabbedPage(); + Flyout = new ContentPage() { Title = "Flyout" }; + } + } + } + } +} diff --git a/src/Controls/tests/DeviceTests/Elements/Window/WindowTests.cs b/src/Controls/tests/DeviceTests/Elements/Window/WindowTests.cs index 0dd7504072d9..1a9c46923048 100644 --- a/src/Controls/tests/DeviceTests/Elements/Window/WindowTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Window/WindowTests.cs @@ -14,6 +14,7 @@ using Microsoft.Maui.DeviceTests.Stubs; using Microsoft.Maui.Devices; using System; +using Microsoft.Maui.Controls.Platform; #if ANDROID || IOS || MACCATALYST using ShellHandler = Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer; @@ -38,12 +39,7 @@ void SetupBuilder() { builder.ConfigureMauiHandlers(handlers => { - handlers.AddHandler(typeof(Controls.Shell), typeof(ShellHandler)); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); + SetupShellHandlers(handlers); #if ANDROID || WINDOWS handlers.AddHandler(typeof(NavigationPage), typeof(NavigationViewHandler)); @@ -55,11 +51,6 @@ void SetupBuilder() handlers.AddHandler(typeof(FlyoutPage), typeof(PhoneFlyoutPageRenderer)); #endif -#if WINDOWS - handlers.AddHandler(); - handlers.AddHandler(); - handlers.AddHandler(); -#endif handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); @@ -67,6 +58,61 @@ void SetupBuilder() }); } +#if !IOS + [Theory] + [ClassData(typeof(ChangingToNewMauiContextDoesntCrashTestCases))] + public async Task ChangingToNewMauiContextDoesntCrash(bool useAppMainPage, Type rootPageType) + { + SetupBuilder(); + IWindow window; + var rootPage = (Page)Activator.CreateInstance(rootPageType); + + if (useAppMainPage) + { + var app = ApplicationServices.GetService() as ApplicationStub; + app.MainPage = rootPage; + window = await InvokeOnMainThreadAsync(() => (app as IApplication).CreateWindow(null)); + + } + else + window = await InvokeOnMainThreadAsync(() => new Window(rootPage)); + + var mauiContextStub1 = new ContextStub(ApplicationServices); +#if ANDROID + var activity = mauiContextStub1.GetActivity(); + mauiContextStub1.Context = new Android.Views.ContextThemeWrapper(activity, Resource.Style.Maui_MainTheme_NoActionBar); +#endif + await CreateHandlerAndAddToWindow(window, async (handler) => + { + if (rootPage is IPageContainer pc) + { + await OnLoadedAsync(pc.CurrentPage); + await OnNavigatedToAsync(pc.CurrentPage); + } + + await Task.Delay(100); + + }, mauiContextStub1); + + var mauiContextStub2 = new ContextStub(ApplicationServices); + +#if ANDROID + mauiContextStub2.Context = new Android.Views.ContextThemeWrapper(activity, Resource.Style.Maui_MainTheme_NoActionBar); +#endif + await CreateHandlerAndAddToWindow(window, async (handler) => + { + if (rootPage is IPageContainer pc) + { + await OnLoadedAsync(pc.CurrentPage); + await OnNavigatedToAsync(pc.CurrentPage); + } + + await Task.Delay(100); + + }, mauiContextStub2); + } +#endif + [Theory] [ClassData(typeof(WindowPageSwapTestCases))] public async Task MainPageSwapTests(WindowPageSwapTestCase swapOrder) diff --git a/src/Controls/tests/DeviceTests/Stubs/ApplicationStub.cs b/src/Controls/tests/DeviceTests/Stubs/ApplicationStub.cs index b6833984072b..8052f88761f2 100644 --- a/src/Controls/tests/DeviceTests/Stubs/ApplicationStub.cs +++ b/src/Controls/tests/DeviceTests/Stubs/ApplicationStub.cs @@ -19,7 +19,8 @@ public ApplicationStub() : base(false) protected override Window CreateWindow(IActivationState activationState) { - return _window; + return _window ?? base.CreateWindow(activationState); + ; } } } diff --git a/src/Controls/tests/DeviceTests/Stubs/WindowHandlerStub.Android.cs b/src/Controls/tests/DeviceTests/Stubs/WindowHandlerStub.Android.cs index e19b99134724..f8c7ffbc8d15 100644 --- a/src/Controls/tests/DeviceTests/Stubs/WindowHandlerStub.Android.cs +++ b/src/Controls/tests/DeviceTests/Stubs/WindowHandlerStub.Android.cs @@ -44,8 +44,7 @@ public static void MapContent(WindowHandlerStub handler, IWindow window) protected override void DisconnectHandler(AActivity platformView) { base.DisconnectHandler(platformView); - var windowManager = MauiContext.GetNavigationRootManager(); - windowManager.Disconnect(); + WindowHandler.DisconnectHandler(MauiContext.GetNavigationRootManager()); } public WindowHandlerStub() diff --git a/src/Core/src/Handlers/NavigationPage/NavigationViewHandler.Android.cs b/src/Core/src/Handlers/NavigationPage/NavigationViewHandler.Android.cs index b4c3ce914733..12c746b59530 100644 --- a/src/Core/src/Handlers/NavigationPage/NavigationViewHandler.Android.cs +++ b/src/Core/src/Handlers/NavigationPage/NavigationViewHandler.Android.cs @@ -20,6 +20,12 @@ protected override View CreatePlatformView() return view; } + + void OnViewChildAdded(object? sender, ViewGroup.ChildViewAddedEventArgs e) + { + _stackNavigationManager?.CheckForFragmentChange(); + } + StackNavigationManager CreateNavigationManager() { _ = MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} should have been set by base class."); @@ -35,6 +41,9 @@ protected override void ConnectHandler(View platformView) platformView.ViewAttachedToWindow += OnViewAttachedToWindow; platformView.ViewDetachedFromWindow += OnViewDetachedFromWindow; + + if (platformView is FragmentContainerView fcw) + fcw.ChildViewAdded += OnViewChildAdded; } void OnViewDetachedFromWindow(object? sender, View.ViewDetachedFromWindowEventArgs e) @@ -61,6 +70,9 @@ private protected override void OnDisconnectHandler(View platformView) platformView.ViewDetachedFromWindow -= OnViewDetachedFromWindow; platformView.LayoutChange -= OnLayoutChanged; + if (platformView is FragmentContainerView fcw) + fcw.ChildViewAdded -= OnViewChildAdded; + _stackNavigationManager?.Disconnect(); base.OnDisconnectHandler(platformView); } diff --git a/src/Core/src/Handlers/Window/WindowHandler.Android.cs b/src/Core/src/Handlers/Window/WindowHandler.Android.cs index 46ae58a040c1..724f56c33005 100644 --- a/src/Core/src/Handlers/Window/WindowHandler.Android.cs +++ b/src/Core/src/Handlers/Window/WindowHandler.Android.cs @@ -8,6 +8,8 @@ namespace Microsoft.Maui.Handlers { public partial class WindowHandler : ElementHandler { + NavigationRootManager? _rootManager; + protected override void ConnectHandler(Activity platformView) { base.ConnectHandler(platformView); @@ -49,7 +51,6 @@ public static void MapRequestDisplayDensity(IWindowHandler handler, IWindow wind request.SetResult(handler.PlatformView.GetDisplayDensity()); } - NavigationRootManager? _rootManager; private protected override void OnConnectHandler(object platformView) { base.OnConnectHandler(platformView); @@ -61,8 +62,11 @@ private protected override void OnConnectHandler(object platformView) private protected override void OnDisconnectHandler(object platformView) { base.OnDisconnectHandler(platformView); + + DisconnectHandler(_rootManager); + if (_rootManager != null) - _rootManager.RootViewChanged += OnRootViewChanged; + _rootManager.RootViewChanged -= OnRootViewChanged; } void OnRootViewChanged(object? sender, EventArgs e) @@ -71,6 +75,13 @@ void OnRootViewChanged(object? sender, EventArgs e) VirtualView.VisualDiagnosticsOverlay.Initialize(); } + // This is here to try and ensure symmetry with disconnect code between test handler + // and the real handler + internal static void DisconnectHandler(NavigationRootManager? navigationRootManager) + { + navigationRootManager?.Disconnect(); + } + internal static View? CreateRootViewFromContent(IWindowHandler handler, IWindow window) { _ = handler.MauiContext ?? throw new InvalidOperationException($"{nameof(MauiContext)} should have been set by base class."); diff --git a/src/Core/src/Platform/Android/ContextExtensions.cs b/src/Core/src/Platform/Android/ContextExtensions.cs index 5a396f287fb9..e1250606fd35 100644 --- a/src/Core/src/Platform/Android/ContextExtensions.cs +++ b/src/Core/src/Platform/Android/ContextExtensions.cs @@ -419,5 +419,36 @@ public static Rect ToCrossPlatformRectInReferenceFrame(this Context context, int return Rect.FromLTRB(0, 0, deviceIndependentRight - deviceIndependentLeft, deviceIndependentBottom - deviceIndependentTop); } + + internal static bool IsDestroyed(this Context? context) + { + if (context == null) + return true; + + if (context.GetActivity() is FragmentActivity fa) + { + if (fa.IsDisposed()) + return true; + + var stateCheck = AndroidX.Lifecycle.Lifecycle.State.Destroyed; + + if (stateCheck != null && + fa.Lifecycle.CurrentState == stateCheck) + { + return true; + } + + if (fa.IsDestroyed) + return true; + } + + return context.IsDisposed(); + } + + internal static bool IsPlatformContextDestroyed(this IElementHandler? handler) + { + var context = handler?.MauiContext?.Context; + return context.IsDestroyed(); + } } } \ No newline at end of file diff --git a/src/Controls/src/Core/Platform/Android/Extensions/FragmentManagerExtensions.cs b/src/Core/src/Platform/Android/FragmentManagerExtensions.cs similarity index 85% rename from src/Controls/src/Core/Platform/Android/Extensions/FragmentManagerExtensions.cs rename to src/Core/src/Platform/Android/FragmentManagerExtensions.cs index 610773428006..0528ae143c50 100644 --- a/src/Controls/src/Core/Platform/Android/Extensions/FragmentManagerExtensions.cs +++ b/src/Core/src/Platform/Android/FragmentManagerExtensions.cs @@ -1,8 +1,8 @@ -#nullable disable - +using System; +using Android.Content; using AndroidX.Fragment.App; -namespace Microsoft.Maui.Controls.Platform +namespace Microsoft.Maui.Platform { // This is a way to centralize all fragment modifications which makes it a lot easier to debug internal static class FragmentManagerExtensions @@ -56,5 +56,19 @@ public static FragmentTransaction BeginTransactionEx(this FragmentManager fragme { return fragmentManager.BeginTransaction(); } + + public static bool IsDestroyed(this FragmentManager? obj, Context? context) + { + if (obj == null || obj.IsDisposed()) + return true; + + if (context == null) + return true; + + if (obj.IsDestroyed) + return true; + + return context.IsDestroyed(); + } } } \ No newline at end of file diff --git a/src/Core/src/Platform/Android/MauiContextExtensions.cs b/src/Core/src/Platform/Android/MauiContextExtensions.cs index a9fc85840674..b591cd4be9f6 100644 --- a/src/Core/src/Platform/Android/MauiContextExtensions.cs +++ b/src/Core/src/Platform/Android/MauiContextExtensions.cs @@ -1,7 +1,9 @@ using System; +using Android.Content; using Android.Views; using AndroidX.AppCompat.App; using AndroidX.Fragment.App; +using Java.Util.Zip; using Microsoft.Extensions.DependencyInjection; using Microsoft.Maui.Devices; @@ -16,7 +18,7 @@ public static LayoutInflater GetLayoutInflater(this IMauiContext mauiContext) { var layoutInflater = mauiContext.Services.GetService(); - if (layoutInflater == null && mauiContext.Context != null) + if (!layoutInflater.IsAlive() && mauiContext.Context != null) { var activity = mauiContext.Context.GetActivity(); @@ -68,6 +70,32 @@ public static IMauiContext MakeScoped(this IMauiContext mauiContext, return scopedContext; } + internal static View ToPlatform( + this IView view, + IMauiContext fragmentMauiContext, + Android.Content.Context context, + LayoutInflater layoutInflater, + FragmentManager childFragmentManager) + { + if (view.Handler?.MauiContext is MauiContext scopedMauiContext) + { + // If this handler becomes to a different activity then we need to + // recreate the view. + // If it's the same activity we just update the layout inflater + // and the fragment manager so that the platform view doesn't recreate + // underneath the users feet + if (scopedMauiContext.GetActivity() == context.GetActivity() && + view.Handler.PlatformView is View platformView) + { + scopedMauiContext.AddWeakSpecific(layoutInflater); + scopedMauiContext.AddWeakSpecific(childFragmentManager); + return platformView; + } + } + + return view.ToPlatform(fragmentMauiContext.MakeScoped(layoutInflater: layoutInflater, fragmentManager: childFragmentManager)); + } + internal static IServiceProvider GetApplicationServices(this IMauiContext mauiContext) { if (mauiContext.Context?.ApplicationContext is MauiApplication ma) diff --git a/src/Core/src/Platform/Android/Navigation/MauiNavHostFragment.cs b/src/Core/src/Platform/Android/Navigation/MauiNavHostFragment.cs new file mode 100644 index 000000000000..29ff73d8e85a --- /dev/null +++ b/src/Core/src/Platform/Android/Navigation/MauiNavHostFragment.cs @@ -0,0 +1,22 @@ +using Android.OS; +using Android.Runtime; +using Android.Views; +using AndroidX.Navigation; +using AndroidX.Navigation.Fragment; + +namespace Microsoft.Maui.Platform +{ + [Register("microsoft.maui.platform.MauiNavHostFragment")] + class MauiNavHostFragment : NavHostFragment + { + public StackNavigationManager? StackNavigationManager { get; set; } + + public MauiNavHostFragment() + { + } + + protected MauiNavHostFragment(nint javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + } +} diff --git a/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs b/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs index 494000fc33b4..e012fac6473b 100644 --- a/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs +++ b/src/Core/src/Platform/Android/Navigation/NavigationRootManager.cs @@ -125,16 +125,16 @@ void SetContentView(IView? view) { if (view == null) { - if (_viewFragment != null) + if (_viewFragment != null && !FragmentManager.IsDestroyed(_mauiContext.Context)) { FragmentManager .BeginTransaction() .Remove(_viewFragment) .SetReorderingAllowed(true) .Commit(); - - _viewFragment = null; } + + _viewFragment = null; } else { diff --git a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs index 8c19b23e99fc..f8df45ab8851 100644 --- a/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs +++ b/src/Core/src/Platform/Android/Navigation/NavigationViewFragment.cs @@ -4,7 +4,6 @@ using Android.Views; using Android.Views.Animations; using AndroidX.Fragment.App; -using AndroidX.Navigation; using AView = Android.Views.View; namespace Microsoft.Maui.Platform @@ -13,14 +12,19 @@ public class NavigationViewFragment : Fragment { AView? _currentView; FragmentContainerView? _fragmentContainerView; + StackNavigationManager? _navigationManager; FragmentContainerView FragmentContainerView => _fragmentContainerView ?? throw new InvalidOperationException($"FragmentContainerView cannot be null here"); - StackNavigationManager? _navigationManager; + MauiNavHostFragment? NavHostFragment => + this.ParentFragment as MauiNavHostFragment; - StackNavigationManager NavigationManager => _navigationManager - ?? throw new InvalidOperationException($"Graph cannot be null here"); + internal StackNavigationManager NavigationManager + { + get => _navigationManager ?? NavHostFragment?.StackNavigationManager ?? throw new InvalidOperationException($"NavigationManager cannot be null here"); + set => _navigationManager = value; + } protected NavigationViewFragment(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) { @@ -32,12 +36,6 @@ public NavigationViewFragment() public override AView OnCreateView(LayoutInflater inflater, ViewGroup? container, Bundle? savedInstanceState) { - var context = - (container?.Context as StackNavigationManager.StackContext) ?? - (container?.Parent as AView)?.Context as StackNavigationManager.StackContext - ?? throw new InvalidOperationException($"StackNavigationManager.StackContext not found"); - - _navigationManager = context.StackNavigationManager; _fragmentContainerView ??= container as FragmentContainerView; // When shuffling around the back stack sometimes we'll need a page to detach and then reattach. @@ -56,14 +54,9 @@ public override AView OnCreateView(LayoutInflater inflater, ViewGroup? container // Even if there's only one page on the stack _currentView = - NavigationManager.CurrentPage.Handler?.PlatformView as AView; - - if (_currentView == null) - { - var scopedContext = NavigationManager.MauiContext.MakeScoped(inflater, ChildFragmentManager); - - _currentView = NavigationManager.CurrentPage.ToPlatform(scopedContext); - } + NavigationManager + .CurrentPage + .ToPlatform(NavigationManager.MauiContext, RequireContext(), inflater, ChildFragmentManager); _currentView.RemoveFromParent(); diff --git a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs index 96dc0228a40d..8f2c0270e270 100644 --- a/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs +++ b/src/Core/src/Platform/Android/Navigation/StackNavigationManager.cs @@ -2,11 +2,11 @@ using System.Collections.Generic; using Android.Content; using Android.OS; -using Android.Views; using AndroidX.Fragment.App; using AndroidX.Navigation; using AndroidX.Navigation.Fragment; using AndroidX.Navigation.UI; +using Java.Interop; using AToolbar = AndroidX.AppCompat.Widget.Toolbar; using AView = Android.Views.View; @@ -19,6 +19,9 @@ public class StackNavigationManager NavGraph? _navGraph; IView? _currentPage; Callbacks? _fragmentLifecycleCallbacks; + FragmentManager? _fragmentManager; + FragmentContainerView? _fragmentContainerView; + internal IView? VirtualView { get; private set; } internal IStackNavigation? NavigationView { get; private set; } internal bool IsNavigating => ActiveRequestedArgs != null; @@ -31,6 +34,9 @@ public class StackNavigationManager internal NavHostFragment NavHost => _navHost ?? throw new InvalidOperationException($"NavHost cannot be null"); + internal NavController NavController => + NavHost.NavController ?? throw new InvalidOperationException($"NavHost cannot be null"); + internal FragmentNavigator FragmentNavigator => _fragmentNavigator ?? throw new InvalidOperationException($"FragmentNavigator cannot be null"); @@ -47,15 +53,7 @@ public IView CurrentPage public StackNavigationManager(IMauiContext mauiContext) { - var currentInflater = mauiContext.GetLayoutInflater(); - var inflater = - new StackLayoutInflater( - currentInflater, - currentInflater.Context, - this); - - MauiContext = - mauiContext.MakeScoped(inflater, context: inflater.Context); + MauiContext = mauiContext; } /* @@ -95,7 +93,7 @@ void ApplyNavigationRequest(NavigationRequest args) ActiveRequestedArgs = args; IReadOnlyList newPageStack = args.NavigationStack; bool animated = args.Animated; - var navController = NavHost.NavController; + var navController = NavController; var previousNavigationStack = NavigationStack; var previousNavigationStackCount = previousNavigationStack.Count; bool initialNavigation = NavigationStack.Count == 0; @@ -142,7 +140,7 @@ void ApplyNavigationRequest(NavigationRequest args) IsAnimated = animated; - var iterator = NavHost.NavController.BackQueue.Iterator(); + var iterator = NavController.BackQueue.Iterator(); var fragmentNavDestinations = new List(); while (iterator.HasNext) @@ -185,7 +183,7 @@ void ApplyNavigationRequest(NavigationRequest args) // We only keep destinations around that are on the backstack // This iterates over the new backstack and removes any destinations // that are no longer apart of the back stack - var iterateNewStack = NavHost.NavController.BackQueue.Iterator(); + var iterateNewStack = NavController.BackQueue.Iterator(); int startId = -1; while (iterateNewStack.HasNext) { @@ -247,7 +245,7 @@ internal void NavigationFinished(IStackNavigation? navigationView) // Navigation Stack on the INavigationView to our platform stack List Initialize(IReadOnlyList pages) { - var navController = NavHost.NavController; + var navController = NavController; // We are subtracting one because the navgraph itself is the first item on the stack int PlatformNavigationStackCount = navController.BackQueue.Size() - 1; @@ -288,20 +286,15 @@ void UpdateNavigationStack(IReadOnlyList newPageStack) public virtual void Disconnect() { - if (_fragmentLifecycleCallbacks != null) - { - if (_navHost?.NavController != null && _navHost.NavController.IsAlive()) - _navHost.NavController.RemoveOnDestinationChangedListener(_fragmentLifecycleCallbacks); - - ChildFragmentManager?.UnregisterFragmentLifecycleCallbacks(_fragmentLifecycleCallbacks); + if (IsNavigating) + NavigationFinished(NavigationView); - _fragmentLifecycleCallbacks.Disconnect(); - _fragmentLifecycleCallbacks = null; - } + _fragmentLifecycleCallbacks?.Disconnect(); + _fragmentLifecycleCallbacks = null; VirtualView = null; NavigationView = null; - _navHost = null; + SetNavHost(null); _fragmentNavigator = null; } @@ -310,32 +303,64 @@ public virtual void Connect(IView navigationView) VirtualView = navigationView; NavigationView = (IStackNavigation)navigationView; - var fragmentManager = MauiContext?.GetFragmentManager(); - _ = fragmentManager ?? throw new InvalidOperationException($"GetFragmentManager returned null"); + _fragmentContainerView = navigationView.Handler?.PlatformView as FragmentContainerView; + + _fragmentManager = MauiContext?.GetFragmentManager(); + + _ = _fragmentManager ?? throw new InvalidOperationException($"GetFragmentManager returned null"); _ = NavigationView ?? throw new InvalidOperationException($"VirtualView cannot be null"); - var navHostFragment = fragmentManager.FindFragmentById(Resource.Id.nav_host); - _navHost = navHostFragment as NavHostFragment; + var navHostFragment = _fragmentManager.FindFragmentById(Resource.Id.nav_host); + SetNavHost(navHostFragment as NavHostFragment); if (_navHost == null) throw new InvalidOperationException($"No NavHostFragment found"); + } + + + internal void CheckForFragmentChange() + { + var fragmentManager = MauiContext.GetFragmentManager(); + var navHostFragment = _fragmentContainerView?.Fragment; + + if ((navHostFragment != null && _navHost != navHostFragment) || (fragmentManager != _fragmentManager)) + { + System.Diagnostics.Debug.WriteLine($"CheckForFragmentChange: {_fragmentContainerView}"); + + _fragmentManager = fragmentManager; + _ = _fragmentManager ?? throw new InvalidOperationException($"GetFragmentManager returned null"); - System.Diagnostics.Debug.WriteLine($"_navHost: {_navHost} {_navHost.GetHashCode()}"); + navHostFragment = navHostFragment ?? _fragmentManager.FindFragmentById(Resource.Id.nav_host); - _fragmentNavigator = - (FragmentNavigator)NavHost - .NavController - .NavigatorProvider - .GetNavigator(Java.Lang.Class.FromType(typeof(FragmentNavigator))); + _fragmentManager = MauiContext.GetFragmentManager(); + _fragmentLifecycleCallbacks?.Disconnect(); + _fragmentLifecycleCallbacks = null; + SetNavHost(navHostFragment as NavHostFragment); + + if (_navHost == null) + throw new InvalidOperationException($"No NavHostFragment found"); + + _fragmentNavigator = + (FragmentNavigator)NavController + .NavigatorProvider + .GetNavigator(Java.Lang.Class.FromType(typeof(FragmentNavigator))); + + NavController.SetGraph(NavGraph, null); + _fragmentLifecycleCallbacks = new Callbacks(this, NavController, ChildFragmentManager); + } } public virtual void RequestNavigation(NavigationRequest e) { + if (MauiContext == null) + return; + + CheckForFragmentChange(); + if (_navGraph == null) { var navGraphNavigator = - (NavGraphNavigator)NavHost - .NavController + (NavGraphNavigator)NavController .NavigatorProvider .GetNavigator(Java.Lang.Class.FromType(typeof(NavGraphNavigator))); @@ -344,9 +369,7 @@ public virtual void RequestNavigation(NavigationRequest e) if (_fragmentLifecycleCallbacks == null) { - _fragmentLifecycleCallbacks = new Callbacks(this); - NavHost.NavController.AddOnDestinationChangedListener(_fragmentLifecycleCallbacks); - ChildFragmentManager?.RegisterFragmentLifecycleCallbacks(_fragmentLifecycleCallbacks, false); + _fragmentLifecycleCallbacks = new Callbacks(this, NavController, ChildFragmentManager); } ApplyNavigationRequest(e); @@ -401,38 +424,38 @@ protected virtual void OnDestinationChanged(NavController navController, NavDest } } - internal class StackLayoutInflater : LayoutInflater + void SetNavHost(NavHostFragment? navHost) { - readonly LayoutInflater _original; + if (_navHost == navHost) + return; - public StackLayoutInflater( - LayoutInflater original, - Context? context, - StackNavigationManager stackNavigationManager) : - base(original, new StackContext(context, stackNavigationManager)) - { - _original = original; - StackNavigationManager = stackNavigationManager; - } + if (_navHost is MauiNavHostFragment oldHost) + oldHost.StackNavigationManager = null; + + if (navHost is MauiNavHostFragment newHost) + newHost.StackNavigationManager = this; - public StackNavigationManager StackNavigationManager { get; } + _navHost = navHost; - public override LayoutInflater? CloneInContext(Context? newContext) + if (_navHost != null) { - return new StackLayoutInflater(_original, newContext, StackNavigationManager); - } - } + _fragmentNavigator = + (FragmentNavigator)NavController + .NavigatorProvider + .GetNavigator(Java.Lang.Class.FromType(typeof(FragmentNavigator))); - internal class StackContext : AndroidX.AppCompat.View.ContextThemeWrapper - { - public StackContext( - Context? context, - StackNavigationManager stackNavigationManager) : base(context, context?.Theme) + foreach (var fragment in _navHost.ChildFragmentManager.Fragments) + { + if (fragment is NavigationViewFragment nvf) + { + nvf.NavigationManager = this; + } + } + } + else { - StackNavigationManager = stackNavigationManager; + _fragmentNavigator = null; } - - public StackNavigationManager StackNavigationManager { get; } } class Callbacks : @@ -440,10 +463,17 @@ class Callbacks : NavController.IOnDestinationChangedListener { StackNavigationManager? _stackNavigationManager; + NavController _navController; + FragmentManager? _childFragmentManager; - public Callbacks(StackNavigationManager navigationLayout) + public Callbacks(StackNavigationManager navigationLayout, NavController navController, FragmentManager? childFragmentManager) { _stackNavigationManager = navigationLayout; + _navController = navController; + _childFragmentManager = childFragmentManager; + + _navController.AddOnDestinationChangedListener(this); + _childFragmentManager?.RegisterFragmentLifecycleCallbacks(this, false); } #region IOnDestinationChangedListener @@ -501,11 +531,58 @@ public override void OnFragmentViewDestroyed( base.OnFragmentViewDestroyed(fm, f); } + + public override void OnFragmentCreated(FragmentManager fm, Fragment f, Bundle? savedInstanceState) + { + if (f is NavigationViewFragment pf && _stackNavigationManager != null) + pf.NavigationManager = _stackNavigationManager; + + base.OnFragmentCreated(fm, f, savedInstanceState); + } + + public override void OnFragmentPreCreated(FragmentManager fm, Fragment f, Bundle? savedInstanceState) + { + if (f is NavigationViewFragment pf && _stackNavigationManager != null) + pf.NavigationManager = _stackNavigationManager; + + base.OnFragmentPreCreated(fm, f, savedInstanceState); + } + + public override void OnFragmentPreAttached(FragmentManager fm, Fragment f, Context context) + { + base.OnFragmentPreAttached(fm, f, context); + } + + public override void OnFragmentStarted(FragmentManager fm, Fragment f) + { + base.OnFragmentStarted(fm, f); + } + + public override void OnFragmentAttached(FragmentManager fm, Fragment f, Context context) + { + base.OnFragmentAttached(fm, f, context); + } + + public override void OnFragmentSaveInstanceState(FragmentManager fm, Fragment f, Bundle outState) + { + base.OnFragmentSaveInstanceState(fm, f, outState); + } + + public override void OnFragmentViewCreated(FragmentManager fm, Fragment f, AView v, Bundle? savedInstanceState) + { + base.OnFragmentViewCreated(fm, f, v, savedInstanceState); + } + #endregion internal void Disconnect() { _stackNavigationManager = null; + + if (_navController != null && _navController.IsAlive()) + _navController.RemoveOnDestinationChangedListener(this); + + _childFragmentManager?.UnregisterFragmentLifecycleCallbacks(this); } } } diff --git a/src/Core/src/Platform/Android/Resources/Layout/fragment_backstack.axml b/src/Core/src/Platform/Android/Resources/Layout/fragment_backstack.axml index a907c459a5ec..a466b566d79b 100644 --- a/src/Core/src/Platform/Android/Resources/Layout/fragment_backstack.axml +++ b/src/Core/src/Platform/Android/Resources/Layout/fragment_backstack.axml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/nav_host" - android:name="androidx.navigation.fragment.NavHostFragment" + android:name="microsoft.maui.platform.MauiNavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" app:defaultNavHost="false" diff --git a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBase.cs b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBase.cs index 09c5b0392d66..5f3be4e06951 100644 --- a/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBase.cs +++ b/src/Core/tests/DeviceTests.Shared/HandlerTests/HandlerTestBase.cs @@ -59,6 +59,15 @@ protected IMauiContext MauiContext } } + protected IServiceProvider ApplicationServices + { + get + { + EnsureHandlerCreated(); + return _servicesProvider; + } + } + protected Task SetValueAsync(IView view, TValue value, Action func) where THandler : IElementHandler, new() {