diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue11214.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue11214.cs new file mode 100644 index 00000000000..eb596e27715 --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue11214.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Text; +using Xamarin.Forms.CustomAttributes; +using Xamarin.Forms.Internals; + + +#if UITEST +using Xamarin.UITest; +using NUnit.Framework; +using Xamarin.Forms.Core.UITests; +#endif + +namespace Xamarin.Forms.Controls.Issues +{ + [Preserve(AllMembers = true)] + [Issue(IssueTracker.Github, 11214, "When adding FlyoutItems during Navigating only first one is shown", + PlatformAffected.iOS)] +#if UITEST + [NUnit.Framework.Category(Core.UITests.UITestCategories.Github10000)] + [NUnit.Framework.Category(UITestCategories.Shell)] +#endif + public class Issue11214 : TestShell + { + FlyoutItem _itemexpanderItems; + protected override void Init() + { + _itemexpanderItems = new FlyoutItem() + { + Title = "Expando Magic", + FlyoutDisplayOptions = FlyoutDisplayOptions.AsMultipleItems + }; + + ContentPage contentPage = new ContentPage() + { + Content = new StackLayout() + { + Children = + { + new Label() + { + Text = "Open the Flyout", + AutomationId = "PageLoaded" + } + } + } + }; + + AddFlyoutItem(contentPage, "Top Item"); + + var flyoutItem = AddFlyoutItem("Click Me and You Should see 2 Items show up"); + flyoutItem.Route = "ExpandMe"; + flyoutItem.AutomationId = "ExpandMe"; + Items.Add(_itemexpanderItems); + } + + protected override void OnNavigating(ShellNavigatingEventArgs args) + { + base.OnNavigating(args); + + if(!args.Target.FullLocation.ToString().Contains("ExpandMe")) + { + return; + } + + args.Cancel(); + + if (_itemexpanderItems.Items.Count == 0 || + _itemexpanderItems.Items[0].Items.Count == 0) + { + for (int i = 0; i < 2; i++) + { + _itemexpanderItems.Items.Add(new ShellContent() + { + Title = $"Some Item: {i}", + Content = new ContentPage() + }); + } + } + else + { + _itemexpanderItems.Items.Clear(); + } + } + +#if UITEST + [Test] + public void FlyoutItemChangesPropagateCorrectlyToPlatformForShellElementsNotCurrentlyActive() + { + RunningApp.WaitForElement("PageLoaded"); + TapInFlyout("ExpandMe", makeSureFlyoutStaysOpen: true); + + for (int i = 0; i < 2; i++) + RunningApp.WaitForElement($"Some Item: {i}"); + + TapInFlyout("ExpandMe", makeSureFlyoutStaysOpen: true); + + for (int i = 0; i < 2; i++) + RunningApp.WaitForNoElement($"Some Item: {i}"); + } +#endif + } +} diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems index 0901a704dd0..f2c2bda57ed 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems @@ -10,6 +10,7 @@ + @@ -2607,4 +2608,4 @@ MSBuild:UpdateDesignTimeXaml - + \ No newline at end of file diff --git a/Xamarin.Forms.Core/Shell/IShellController.cs b/Xamarin.Forms.Core/Shell/IShellController.cs index 3c197509963..86344198b05 100644 --- a/Xamarin.Forms.Core/Shell/IShellController.cs +++ b/Xamarin.Forms.Core/Shell/IShellController.cs @@ -19,6 +19,7 @@ public interface IFlyoutBehaviorObserver public interface IShellController : IPageController { event EventHandler StructureChanged; + event EventHandler FlyoutItemsChanged; View FlyoutHeader { get; } diff --git a/Xamarin.Forms.Core/Shell/Shell.cs b/Xamarin.Forms.Core/Shell/Shell.cs index d4b6a93148c..cd3737f33c8 100644 --- a/Xamarin.Forms.Core/Shell/Shell.cs +++ b/Xamarin.Forms.Core/Shell/Shell.cs @@ -65,7 +65,7 @@ static void OnFlyoutItemIsVisibleChanged(BindableObject bindable, object oldValu if (bindable is Element element) element .FindParentOfType() - ?.SendStructureChanged(); + ?.SendFlyoutItemsChanged(); } public static readonly BindableProperty TabBarIsVisibleProperty = @@ -294,6 +294,14 @@ event EventHandler IShellController.StructureChanged event EventHandler _structureChanged; + event EventHandler IShellController.FlyoutItemsChanged + { + add { _flyoutItemsChanged += value; } + remove { _flyoutItemsChanged -= value; } + } + + event EventHandler _flyoutItemsChanged; + View IShellController.FlyoutHeader => FlyoutHeaderView; View IShellController.FlyoutFooter => FlyoutFooterView; @@ -570,6 +578,7 @@ void Initialize() { SetCurrentItem(); SendStructureChanged(); + SendFlyoutItemsChanged(); }; async void SetCurrentItem() @@ -814,8 +823,23 @@ protected override void OnBindingContextChanged() if (FlyoutFooterView != null) SetInheritedBindingContext(FlyoutFooterView, BindingContext); } + + + internal void SendFlyoutItemsChanged() + { + if (UpdateFlyoutGroupings()) + _flyoutItemsChanged?.Invoke(this, EventArgs.Empty); + } List> IShellController.GenerateFlyoutGrouping() + { + if(_currentFlyoutViews == null) + UpdateFlyoutGroupings(); + + return _currentFlyoutViews; + } + + bool UpdateFlyoutGroupings() { // The idea here is to create grouping such that the Flyout would // render correctly if it renderered each item in the groups in order @@ -926,11 +950,11 @@ List> IShellController.GenerateFlyoutGrouping() } if (!hasChanged) - return _currentFlyoutViews; + return false; } _currentFlyoutViews = result; - return result; + return true; bool ShowInFlyoutMenu(BindableObject bo) { diff --git a/Xamarin.Forms.Core/Shell/ShellGroupItem.cs b/Xamarin.Forms.Core/Shell/ShellGroupItem.cs index 7f9a51c732e..9b2c288e16a 100644 --- a/Xamarin.Forms.Core/Shell/ShellGroupItem.cs +++ b/Xamarin.Forms.Core/Shell/ShellGroupItem.cs @@ -1,9 +1,16 @@ -namespace Xamarin.Forms +using System; + +namespace Xamarin.Forms { public class ShellGroupItem : BaseShellItem { public static readonly BindableProperty FlyoutDisplayOptionsProperty = - BindableProperty.Create(nameof(FlyoutDisplayOptions), typeof(FlyoutDisplayOptions), typeof(ShellItem), FlyoutDisplayOptions.AsSingleItem, BindingMode.OneTime); + BindableProperty.Create(nameof(FlyoutDisplayOptions), typeof(FlyoutDisplayOptions), typeof(ShellGroupItem), FlyoutDisplayOptions.AsSingleItem, BindingMode.OneTime, propertyChanged: OnFlyoutDisplayOptionsPropertyChanged); + + static void OnFlyoutDisplayOptionsPropertyChanged(BindableObject bindable, object oldValue, object newValue) + { + ((Element)bindable).FindParentOfType()?.SendFlyoutItemsChanged(); + } public FlyoutDisplayOptions FlyoutDisplayOptions { diff --git a/Xamarin.Forms.Core/Shell/ShellItem.cs b/Xamarin.Forms.Core/Shell/ShellItem.cs index 9b0d886d651..9268843811b 100644 --- a/Xamarin.Forms.Core/Shell/ShellItem.cs +++ b/Xamarin.Forms.Core/Shell/ShellItem.cs @@ -164,9 +164,12 @@ public ShellSection CurrentItem internal void SendStructureChanged() { - if (Parent is Shell shell && IsVisibleItem) + if (Parent is Shell shell) { - shell.SendStructureChanged(); + if (IsVisibleItem) + shell.SendStructureChanged(); + + shell.SendFlyoutItemsChanged(); } } diff --git a/Xamarin.Forms.Core/Shell/ShellSection.cs b/Xamarin.Forms.Core/Shell/ShellSection.cs index 04058d36015..800dbb50344 100644 --- a/Xamarin.Forms.Core/Shell/ShellSection.cs +++ b/Xamarin.Forms.Core/Shell/ShellSection.cs @@ -622,9 +622,12 @@ bool IsNavigationAnimated(BindableObject bo) internal void SendStructureChanged() { - if (Parent?.Parent is Shell shell && IsVisibleSection) + if (Parent?.Parent is Shell shell) { - shell.SendStructureChanged(); + if (IsVisibleSection) + shell.SendStructureChanged(); + + shell.SendFlyoutItemsChanged(); } } diff --git a/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutRecyclerAdapter.cs b/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutRecyclerAdapter.cs index c03838e7af1..884621a80d9 100644 --- a/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutRecyclerAdapter.cs +++ b/Xamarin.Forms.Platform.Android/Renderers/ShellFlyoutRecyclerAdapter.cs @@ -25,7 +25,7 @@ public ShellFlyoutRecyclerAdapter(IShellContext shellContext, Action se { _shellContext = shellContext; - ShellController.StructureChanged += OnShellStructureChanged; + ShellController.FlyoutItemsChanged += OnFlyoutItemsChanged; _listItems = GenerateItemList(); _selectedCallback = selectedCallback; @@ -197,7 +197,7 @@ protected virtual List GenerateItemList() return result; } - protected virtual void OnShellStructureChanged(object sender, EventArgs e) + protected virtual void OnFlyoutItemsChanged(object sender, EventArgs e) { var newListItems = GenerateItemList(); @@ -217,7 +217,7 @@ protected override void Dispose(bool disposing) if (disposing) { - ((IShellController)Shell).StructureChanged -= OnShellStructureChanged; + ((IShellController)Shell).FlyoutItemsChanged -= OnFlyoutItemsChanged; _elementViewHolder?.Dispose(); diff --git a/Xamarin.Forms.Platform.UAP/Shell/ShellRenderer.cs b/Xamarin.Forms.Platform.UAP/Shell/ShellRenderer.cs index a6c48053125..b8aef50a839 100644 --- a/Xamarin.Forms.Platform.UAP/Shell/ShellRenderer.cs +++ b/Xamarin.Forms.Platform.UAP/Shell/ShellRenderer.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; +using System.Linq; using Windows.Foundation.Metadata; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -32,6 +34,7 @@ public class ShellRenderer : Microsoft.UI.Xaml.Controls.NavigationView, IVisualE List> _flyoutGrouping; ShellItemRenderer ItemRenderer { get; } IShellController ShellController => (IShellController)_shell; + ObservableCollection FlyoutItems = new ObservableCollection(); public ShellRenderer() { @@ -43,6 +46,7 @@ public ShellRenderer() Content = ItemRenderer = CreateShellItemRenderer(); MenuItemTemplateSelector = CreateShellFlyoutTemplateSelector(); Style = (Windows.UI.Xaml.Style)Windows.UI.Xaml.Application.Current.Resources["ShellNavigationView"]; + MenuItemsSource = FlyoutItems; } async void OnBackRequested(Microsoft.UI.Xaml.Controls.NavigationView sender, Microsoft.UI.Xaml.Controls.NavigationViewBackRequestedEventArgs args) @@ -317,7 +321,7 @@ protected virtual void OnElementSet(Shell shell) ShellController.AddFlyoutBehaviorObserver(this); ShellController.AddAppearanceObserver(this, shell); ShellController.ItemsCollectionChanged += OnItemsCollectionChanged; - ShellController.StructureChanged += OnStructureChanged; + ShellController.FlyoutItemsChanged += OnFlyoutItemsChanged; UpdateFlyoutBackgroundColor(); _shell.Navigated += OnShellNavigated; @@ -370,7 +374,7 @@ void UpdateToolBar() } } - void OnStructureChanged(object sender, EventArgs e) + void OnFlyoutItemsChanged(object sender, EventArgs e) { UpdateMenuItemSource(); } @@ -386,17 +390,31 @@ void UpdateMenuItemSource() if (_flyoutGrouping != newGrouping) { _flyoutGrouping = newGrouping; - MenuItemsSource = IterateItems(newGrouping); + var newItems = IterateItems(newGrouping).ToList(); + + foreach (var item in newItems) + { + if (!FlyoutItems.Contains(item)) + FlyoutItems.Add(item); + } + + for (var i = FlyoutItems.Count - 1; i >= 0; i--) + { + var item = FlyoutItems[i]; + if (!newItems.Contains(item)) + FlyoutItems.RemoveAt(i); + } } } IEnumerable IterateItems(List> groups) { + int separatorNumber = 0; foreach (var group in groups) { if (group.Count > 0 && group != groups[0]) { - yield return new MenuFlyoutSeparator(); // Creates a separator + yield return new FlyoutItemMenuSeparator(separatorNumber++); // Creates a separator } foreach (var item in group) { @@ -405,6 +423,19 @@ IEnumerable IterateItems(List> groups) } } + class FlyoutItemMenuSeparator : MenuFlyoutSeparator + { + public FlyoutItemMenuSeparator(int separatorNumber) + { + Id = separatorNumber; + } + + public int Id { get; set; } + public override int GetHashCode() => Id.GetHashCode(); + public override bool Equals(object obj) => + obj is FlyoutItemMenuSeparator fim && fim.Id == Id; + } + void SwitchShellItem(ShellItem newItem, bool animate = true) { SelectedItem = newItem; diff --git a/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewController.cs b/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewController.cs index e97d6f23a2f..02aa5835b4d 100644 --- a/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewController.cs +++ b/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewController.cs @@ -33,7 +33,7 @@ public ShellTableViewController(IShellContext context, Action onElement _source = CreateShellTableViewSource(); _source.ScrolledEvent += OnScrolled; - ShellController.StructureChanged += OnStructureChanged; + ShellController.FlyoutItemsChanged += OnFlyoutItemsChanged; _context.Shell.PropertyChanged += OnShellPropertyChanged; } @@ -90,7 +90,7 @@ void OnHeaderFooterSizeChanged(object sender, EventArgs e) LayoutParallax(); } - void OnStructureChanged(object sender, EventArgs e) + void OnFlyoutItemsChanged(object sender, EventArgs e) { _source.ClearCache(); TableView.ReloadData(); @@ -181,8 +181,8 @@ protected override void Dispose(bool disposing) if (disposing) { - if ((_context?.Shell as IShellController) != null) - ((IShellController)_context.Shell).StructureChanged -= OnStructureChanged; + if (ShellController != null) + ShellController.FlyoutItemsChanged -= OnFlyoutItemsChanged; if (_source != null) _source.ScrolledEvent -= OnScrolled; diff --git a/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewSource.cs b/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewSource.cs index 65a45621998..e317687a67f 100644 --- a/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewSource.cs +++ b/Xamarin.Forms.Platform.iOS/Renderers/ShellTableViewSource.cs @@ -13,7 +13,7 @@ public class ShellTableViewSource : UITableViewSource List> _groups; Dictionary _cells; - IShellController ShellController => (IShellController)_context.Shell; + IShellController ShellController => _context.Shell; public ShellTableViewSource(IShellContext context, Action onElementSelected) { @@ -29,7 +29,7 @@ public List> Groups { if (_groups == null) { - _groups = ((IShellController)_context.Shell).GenerateFlyoutGrouping(); + _groups = ShellController.GenerateFlyoutGrouping(); if (_cells != null) {