From f332f3c28ddb6741650131cf0061684811c2fdb2 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Fri, 18 Dec 2020 13:57:03 -0600 Subject: [PATCH 1/4] Add new xplat event to update FlyoutItems from --- .../Issue11214.cs | 104 ++++++++++++++++++ ...rin.Forms.Controls.Issues.Shared.projitems | 3 +- Xamarin.Forms.Core/Shell/IShellController.cs | 1 + Xamarin.Forms.Core/Shell/Shell.cs | 25 ++++- Xamarin.Forms.Core/Shell/ShellItem.cs | 7 +- Xamarin.Forms.Core/Shell/ShellSection.cs | 7 +- .../Renderers/ShellFlyoutRecyclerAdapter.cs | 6 +- .../Shell/ShellRenderer.cs | 39 ++++++- .../Renderers/ShellTableViewController.cs | 8 +- .../Renderers/ShellTableViewSource.cs | 4 +- 10 files changed, 183 insertions(+), 21 deletions(-) create mode 100644 Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue11214.cs 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 d94d2958a7c..b0aeb288e84 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 @@ + Issue10897.xaml @@ -2594,4 +2595,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 ad3a0057280..214574b39bf 100644 --- a/Xamarin.Forms.Core/Shell/Shell.cs +++ b/Xamarin.Forms.Core/Shell/Shell.cs @@ -295,6 +295,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; @@ -884,6 +892,7 @@ void Initialize() { SetCurrentItem(); SendStructureChanged(); + SendFlyoutItemsChanged(); }; async void SetCurrentItem() @@ -1128,8 +1137,18 @@ protected override void OnBindingContextChanged() if (FlyoutFooterView != null) SetInheritedBindingContext(FlyoutFooterView, BindingContext); } + + + internal void SendFlyoutItemsChanged() + { + if (UpdateFlyoutGroupings()) + _flyoutItemsChanged?.Invoke(this, EventArgs.Empty); + } + + List> IShellController.GenerateFlyoutGrouping() => + _currentFlyoutViews; - List> IShellController.GenerateFlyoutGrouping() + 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 @@ -1240,11 +1259,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/ShellItem.cs b/Xamarin.Forms.Core/Shell/ShellItem.cs index 9b0d886d651..5183eacb208 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(); + else + shell.SendFlyoutItemsChanged(); } } diff --git a/Xamarin.Forms.Core/Shell/ShellSection.cs b/Xamarin.Forms.Core/Shell/ShellSection.cs index 84c3d76812c..984debf7907 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(); + else + 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) { From 02b3ec6d527b391729271c9f684274351d135f5f Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Fri, 18 Dec 2020 14:05:11 -0600 Subject: [PATCH 2/4] - always send flyout item changes --- Xamarin.Forms.Core/Shell/ShellItem.cs | 4 ++-- Xamarin.Forms.Core/Shell/ShellSection.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Xamarin.Forms.Core/Shell/ShellItem.cs b/Xamarin.Forms.Core/Shell/ShellItem.cs index 5183eacb208..9268843811b 100644 --- a/Xamarin.Forms.Core/Shell/ShellItem.cs +++ b/Xamarin.Forms.Core/Shell/ShellItem.cs @@ -168,8 +168,8 @@ internal void SendStructureChanged() { if (IsVisibleItem) shell.SendStructureChanged(); - else - shell.SendFlyoutItemsChanged(); + + shell.SendFlyoutItemsChanged(); } } diff --git a/Xamarin.Forms.Core/Shell/ShellSection.cs b/Xamarin.Forms.Core/Shell/ShellSection.cs index 984debf7907..3e07615fa8a 100644 --- a/Xamarin.Forms.Core/Shell/ShellSection.cs +++ b/Xamarin.Forms.Core/Shell/ShellSection.cs @@ -626,8 +626,8 @@ internal void SendStructureChanged() { if (IsVisibleSection) shell.SendStructureChanged(); - else - shell.SendFlyoutItemsChanged(); + + shell.SendFlyoutItemsChanged(); } } From c16e3c7c0382d8336ab0d5e86338b959807bb81a Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Fri, 18 Dec 2020 14:58:08 -0600 Subject: [PATCH 3/4] - fix flyout changed propagation --- Xamarin.Forms.Core/Shell/Shell.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Xamarin.Forms.Core/Shell/Shell.cs b/Xamarin.Forms.Core/Shell/Shell.cs index 214574b39bf..e08487af56f 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 = From ec793c95ef452f7dc2e59d66f78f947e9be19c36 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Sat, 19 Dec 2020 12:42:16 -0600 Subject: [PATCH 4/4] - add additional scenario for Sending Flyout Items Changed Event --- Xamarin.Forms.Core/Shell/Shell.cs | 9 +++++++-- Xamarin.Forms.Core/Shell/ShellGroupItem.cs | 11 +++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Xamarin.Forms.Core/Shell/Shell.cs b/Xamarin.Forms.Core/Shell/Shell.cs index e08487af56f..000ce34aa3a 100644 --- a/Xamarin.Forms.Core/Shell/Shell.cs +++ b/Xamarin.Forms.Core/Shell/Shell.cs @@ -1145,8 +1145,13 @@ internal void SendFlyoutItemsChanged() _flyoutItemsChanged?.Invoke(this, EventArgs.Empty); } - List> IShellController.GenerateFlyoutGrouping() => - _currentFlyoutViews; + List> IShellController.GenerateFlyoutGrouping() + { + if(_currentFlyoutViews == null) + UpdateFlyoutGroupings(); + + return _currentFlyoutViews; + } bool UpdateFlyoutGroupings() { 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 {