diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla45125.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla45125.cs new file mode 100644 index 000000000..1d5c573a8 --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Bugzilla45125.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Xamarin.Forms.CustomAttributes; +using Xamarin.Forms.Internals; + +#if UITEST +using Xamarin.UITest; +using NUnit.Framework; +#endif + +namespace Xamarin.Forms.Controls.Issues +{ + [Preserve(AllMembers = true)] + [Issue(IssueTracker.Bugzilla, 45125, "ListView lacks a way to get information about visible elements (such as FirstVisibleItem) to restore visual positions of elements", PlatformAffected.iOS)] + public class Bugzilla45125 : TestContentPage + { + int _TestNumber = 0; + + const string Instructions = "The black area below should show text listing appearing and disappearing events for the ListView beside it. It should update as you scroll the ListView, with each row firing a single Disappearing event and a single Appearing event as it leaves and enters the visible screen, respectively. If this does not happen, this test has failed."; + const string AppearingLabelId = "appearing"; + const string DisappearingLabelId = "disappearing"; + const string TestButtonId = "TestButtonId"; + + static int _Appearing = 0; + static int _Disappearing = 0; + + static Label _status = new Label + { + TextColor = Color.White, + //TODO: NoWrap causes the Label to be missing from the Horizontal StackLayout + //LineBreakMode = LineBreakMode.NoWrap + }; + + static Label _groupsAppearing = new Label + { + TextColor = Color.Green, + AutomationId = AppearingLabelId + }; + + static Label _groupsDisappearing = new Label + { + TextColor = Color.Blue, + AutomationId = DisappearingLabelId + }; + + static ScrollView _scroll = new ScrollView + { + BackgroundColor = Color.Black, + Content = _status, + MinimumWidthRequest = 200 + }; + + [Preserve(AllMembers = true)] + class GroupItem + { + public string DisplayText { get; set; } + } + + [Preserve(AllMembers = true)] + class GroupedData : List + { + public string GroupName { get; set; } + } + + [Preserve(AllMembers = true)] + class MyCell : ViewCell + { + public MyCell() + { + Label newLabel = new Label(); + newLabel.SetBinding(Label.TextProperty, nameof(GroupItem.DisplayText)); + View = newLabel; + } + } + + [Preserve(AllMembers = true)] + class HeaderCell : ViewCell + { + public HeaderCell() + { + Label newLabel = new Label(); + newLabel.SetBinding(Label.TextProperty, nameof(GroupedData.GroupName)); + View = newLabel; + } + } + + protected override void Init() + { + InitTest(ListViewCachingStrategy.RecycleElement, true); + } + + void InitTest(ListViewCachingStrategy cachingStrategy, bool useTemplate) + { + _scroll.SetScrolledPosition(0, 0); + + _status.Text = _groupsAppearing.Text = _groupsDisappearing.Text = ""; + _Appearing = _Disappearing = 0; + + List groups = GetGroups(); + + var listView = new ListView(cachingStrategy) + { + ItemsSource = groups, + ItemTemplate = new DataTemplate(typeof(MyCell)), + HasUnevenRows = true, + + // Must be grouped to repro + IsGroupingEnabled = true + }; + + if (useTemplate) + listView.GroupHeaderTemplate = new DataTemplate(typeof(HeaderCell)); + else + listView.GroupDisplayBinding = new Binding(nameof(GroupedData.GroupName)); + + // Must attach to the ListView's events to repro + listView.ItemAppearing += ListView_ItemAppearing; + listView.ItemDisappearing += ListView_ItemDisappearing; + + var horStack = new StackLayout + { + Orientation = StackOrientation.Horizontal, + Children = { _scroll, listView }, + HeightRequest = 300 + }; + + Button nextButton = new Button { Text = "Next", AutomationId = TestButtonId }; + nextButton.Clicked += NextButton_Clicked; + StackLayout stack = new StackLayout + { + Children = { new Label { Text = Instructions }, _groupsAppearing, _groupsDisappearing, horStack, nextButton } + }; + Content = stack; + + var lastGroup = groups.Last(); + var lastItem = lastGroup.First(); + + var firstGroup = groups.First(); + var firstItem = firstGroup.First(); + + Device.StartTimer(TimeSpan.FromSeconds(1), () => { listView.ScrollTo(lastItem, lastGroup, ScrollToPosition.End, true); return false; }); + Device.StartTimer(TimeSpan.FromSeconds(2), () => { listView.ScrollTo(firstItem, firstItem, ScrollToPosition.MakeVisible, true); return false; }); + + _TestNumber++; + } + + void NextButton_Clicked(object sender, EventArgs e) + { + switch (_TestNumber) + { + default: + InitTest(ListViewCachingStrategy.RecycleElement, useTemplate: true); + break; + case 1: + InitTest(ListViewCachingStrategy.RetainElement, useTemplate: true); + break; + case 2: + InitTest(ListViewCachingStrategy.RetainElement, useTemplate: false); + break; + case 3: + InitTest(ListViewCachingStrategy.RecycleElement, useTemplate: false); + break; + } + } + + List GetGroups() + { + List groups = new List(); + + for (int i = 1; i < 100; i++) + { + var group = new GroupedData { GroupName = $"Group {i}" }; + + group.Add(new GroupItem { DisplayText = $"Item {i}" }); + + groups.Add(group); + } + + return groups; + } + + static string GetItemText(object item) + { + var groupDisplayTextItem = item as TemplatedItemsList, Cell>; + var groupItem = item as GroupedData; + var itemItem = item as GroupItem; + + var text = item.ToString(); + + if (groupDisplayTextItem != null) + text = groupDisplayTextItem.Name; + else if (groupItem != null) + text = groupItem.GroupName; + else if (itemItem != null) + text = itemItem.DisplayText; + + return text; + } + + void ListView_ItemDisappearing(object sender, ItemVisibilityEventArgs e) + { + var text = GetItemText(e.Item); + + if (_status.Text?.Length > 500) + _status.Text = ""; + + _Disappearing++; + + _groupsDisappearing.Text = _Disappearing.ToString(); + + _status.Text += $"\r\n {text} Disappearing"; + _scroll.ScrollToAsync(_status, ScrollToPosition.MakeVisible, false); + } + + void ListView_ItemAppearing(object sender, ItemVisibilityEventArgs e) + { + var text = GetItemText(e.Item); + + if (_status.Text?.Length > 500) + _status.Text = ""; + + _Appearing++; + + _groupsAppearing.Text = _Appearing.ToString(); + + _status.Text += $"\r\n {text} Appearing"; + _scroll.ScrollToAsync(_status, ScrollToPosition.MakeVisible, false); + } + +#if UITEST + [Test] + public void Bugzilla45125Test () + { + RunTest(); + + RunningApp.Tap(q => q.Marked(TestButtonId)); + RunTest(); + + RunningApp.Tap(q => q.Marked(TestButtonId)); + RunTest(); + + RunningApp.Tap(q => q.Marked(TestButtonId)); + RunTest(); + } + + void RunTest() + { + RunningApp.WaitForElement (q => q.Marked (AppearingLabelId)); + RunningApp.WaitForElement (q => q.Marked (DisappearingLabelId)); + + RunningApp.Screenshot ("There should be appearing and disappearing events for the Groups and Items."); + var appearing = int.Parse(RunningApp.Query(q => q.Marked(AppearingLabelId))[0].Text); + var disappearing = int.Parse(RunningApp.Query(q=> q.Marked(DisappearingLabelId))[0].Text); + + Assert.IsTrue(appearing > 0, $"Test {_TestNumber}: No appearing events for groups found."); + Assert.IsTrue(disappearing > 0, $"Test {_TestNumber}: No disappearing events for groups found."); + } +#endif + } +} \ No newline at end of file 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 56c362ff9..8f7cfcc38 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 @@ -331,6 +331,7 @@ + diff --git a/Xamarin.Forms.Core/ListView.cs b/Xamarin.Forms.Core/ListView.cs index ce6bc1cc5..4c2dc60bf 100644 --- a/Xamarin.Forms.Core/ListView.cs +++ b/Xamarin.Forms.Core/ListView.cs @@ -379,6 +379,10 @@ namespace Xamarin.Forms public string GetDisplayTextFromGroup(object cell) { int groupIndex = TemplatedItems.GetGlobalIndexOfGroup(cell); + + if (groupIndex == -1) + return cell.ToString(); + var group = TemplatedItems.GetGroup(groupIndex); string displayBinding = null; diff --git a/Xamarin.Forms.Platform.iOS/Renderers/ListViewRenderer.cs b/Xamarin.Forms.Platform.iOS/Renderers/ListViewRenderer.cs index 0c1911cd2..ee92c54f6 100644 --- a/Xamarin.Forms.Platform.iOS/Renderers/ListViewRenderer.cs +++ b/Xamarin.Forms.Platform.iOS/Renderers/ListViewRenderer.cs @@ -922,21 +922,30 @@ namespace Xamarin.Forms.Platform.iOS public override UIView GetViewForHeader(UITableView tableView, nint section) { - if (List.IsGroupingEnabled && List.GroupHeaderTemplate != null) - { - var cell = TemplatedItemsView.TemplatedItems[(int)section]; - if (cell.HasContextActions) - throw new NotSupportedException("Header cells do not support context actions"); + UIView view = null; + + if (!List.IsGroupingEnabled) + return view; + + var cell = TemplatedItemsView.TemplatedItems[(int)section]; + if (cell.HasContextActions) + throw new NotSupportedException("Header cells do not support context actions"); var renderer = (CellRenderer)Internals.Registrar.Registered.GetHandlerForObject(cell); - var view = new HeaderWrapperView(); + view = new HeaderWrapperView(); view.AddSubview(renderer.GetCell(cell, null, tableView)); - return view; - } + return view; + } - return null; + public override void HeaderViewDisplayingEnded(UITableView tableView, UIView headerView, nint section) + { + if (!List.IsGroupingEnabled) + return; + + var cell = TemplatedItemsView.TemplatedItems[(int)section]; + cell.SendDisappearing(); } public override nint NumberOfSections(UITableView tableView) @@ -1046,18 +1055,6 @@ namespace Xamarin.Forms.Platform.iOS return templatedItems.ShortNames.ToArray(); } - public override string TitleForHeader(UITableView tableView, nint section) - { - if (!List.IsGroupingEnabled) - return null; - - var sl = GetSectionList((int)section); - sl.PropertyChanged -= OnSectionPropertyChanged; - sl.PropertyChanged += OnSectionPropertyChanged; - - return sl.Name; - } - public void Cleanup() { _selectionFromNative = false; @@ -1100,27 +1097,6 @@ namespace Xamarin.Forms.Platform.iOS return cell; } - ITemplatedItemsList GetSectionList(int section) - { - return (ITemplatedItemsList)((IList)TemplatedItemsView.TemplatedItems)[section]; - } - - void OnSectionPropertyChanged(object sender, PropertyChangedEventArgs e) - { - var currentSelected = _uiTableView.IndexPathForSelectedRow; - - var til = (TemplatedItemsList, Cell>)sender; - var groupIndex = ((IList)TemplatedItemsView.TemplatedItems).IndexOf(til); - if (groupIndex == -1) - { - til.PropertyChanged -= OnSectionPropertyChanged; - return; - } - - _uiTableView.ReloadSections(NSIndexSet.FromIndex(groupIndex), ReloadSectionsAnimation); - _uiTableView.SelectRow(currentSelected, false, UITableViewScrollPosition.None); - } - void OnShortNamesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { _uiTableView.ReloadSectionIndexTitles();