From 868c3ad3a827959617fba30362d1103b1ec32302 Mon Sep 17 00:00:00 2001 From: xiaoy312 Date: Tue, 28 Nov 2023 16:52:33 -0500 Subject: [PATCH] fix(listview): header losing datacontext on ios between frame navigation --- .../Given_ListViewBase.cs | 171 ++++++++++++++---- .../Controls/ListViewBase/ListViewBase.iOS.cs | 15 ++ 2 files changed, 148 insertions(+), 38 deletions(-) diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.cs index ee781d013d65..a63ede256195 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListViewBase.cs @@ -41,6 +41,10 @@ using static Private.Infrastructure.TestServices; using Point = Windows.Foundation.Point; +#if HAS_UNO +using static Uno.UI.Extensions.ViewExtensions; +#endif + namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml_Controls { public partial class Given_ListViewBase // resources @@ -4003,13 +4007,6 @@ private async Task When_Items_Have_Duplicates_Common(Selector sut) Assert.AreEqual("String 1", (string)added3.Single()); } - private sealed class AlwaysEqualClass : IEquatable - { - public bool Equals(AlwaysEqualClass obj) => true; - public override bool Equals(object obj) => true; - public override int GetHashCode() => 0; - } - [TestMethod] public async Task When_Items_Are_Equal_But_Different_References_ListView() => await When_Items_Are_Equal_But_Different_References_Common(new ListView()); @@ -4022,7 +4019,44 @@ private sealed class AlwaysEqualClass : IEquatable [TestMethod] public async Task When_Items_Are_Equal_But_Different_References_FlipView() => await When_Items_Are_Equal_But_Different_References_Common(new FlipView()); - public record When_Header_DataContext_Model(string MyText); + private async Task When_Items_Are_Equal_But_Different_References_Common(Selector sut) + { + var obj1 = new AlwaysEqualClass(); + var obj2 = new AlwaysEqualClass(); + var items = new ObservableCollection(new[] + { + obj1, obj2 + }); + sut.ItemsSource = items; + var list = new List(); + sut.SelectionChanged += (_, e) => list.Add(e); + sut.SelectedIndex = 1; + Assert.AreEqual(1, sut.SelectedIndex); + Assert.AreSame(obj2, sut.SelectedItem); + sut.SelectedIndex = 0; + Assert.AreEqual(0, sut.SelectedIndex); + Assert.AreSame(obj1, sut.SelectedItem); + + Assert.AreEqual(2, list.Count); + var removed1 = list[0].RemovedItems; + var removed2 = list[1].RemovedItems; + + var added1 = list[0].AddedItems; + var added2 = list[1].AddedItems; + + if (sut is FlipView) + { + Assert.AreSame(obj1, removed1.Single()); + } + else + { + Assert.AreEqual(0, removed1.Count); + } + Assert.AreSame(obj2, added1.Single()); + + Assert.AreSame(obj2, removed2.Single()); + Assert.AreSame(obj1, added2.Single()); + } [TestMethod] [RunsOnUIThread] @@ -4216,44 +4250,51 @@ public async Task When_HeaderTemplate_DataContext() } #endif - private async Task When_Items_Are_Equal_But_Different_References_Common(Selector sut) +#if __IOS__ + [TestMethod] + [RunsOnUIThread] + public async Task When_HeaderDataContext_Cleared_FromNavigation() { - var obj1 = new AlwaysEqualClass(); - var obj2 = new AlwaysEqualClass(); - var items = new ObservableCollection(new[] - { - obj1, obj2 - }); - sut.ItemsSource = items; - var list = new List(); - sut.SelectionChanged += (_, e) => list.Add(e); - sut.SelectedIndex = 1; - Assert.AreEqual(1, sut.SelectedIndex); - Assert.AreSame(obj2, sut.SelectedItem); - sut.SelectedIndex = 0; - Assert.AreEqual(0, sut.SelectedIndex); - Assert.AreSame(obj1, sut.SelectedItem); + var frame = new Frame(); - Assert.AreEqual(2, list.Count); - var removed1 = list[0].RemovedItems; - var removed2 = list[1].RemovedItems; + WindowHelper.WindowContent = frame; + await WindowHelper.WaitFor(() => frame.IsLoaded); + await WindowHelper.WaitForIdle(); - var added1 = list[0].AddedItems; - var added2 = list[1].AddedItems; + frame.Navigate(typeof(When_HeaderDataContext_Cleared_FromNavigation_Page)); + await WindowHelper.WaitForIdle(); - if (sut is FlipView) - { - Assert.AreSame(obj1, removed1.Single()); - } - else + var page = (When_HeaderDataContext_Cleared_FromNavigation_Page)frame.Content; + var sut = frame.FindFirstDescendant(); + var panel = (NativeListViewBase)sut.InternalItemsPanelRoot; + + page.LvHeaderDcChanged += (s, e) => { /* for debugging */ }; + Assert.IsNotNull(page.DataContext); + + for (var i = 0; i < 3; i++) // may not always trigger, but 3 times is usually more than enough { - Assert.AreEqual(0, removed1.Count); + // scroll header out of viewport and back in + ScrollTo(sut, 100000); + await WindowHelper.WaitForIdle(); + await Task.Delay(1000); + ScrollTo(sut, 0); + await WindowHelper.WaitForIdle(); + await Task.Delay(1000); + + // frame navigate away and back + frame.Navigate(typeof(BackNavigationPage)); + await WindowHelper.WaitForIdle(); + await Task.Delay(1000); + frame.GoBack(); + await WindowHelper.WaitForIdle(); + + // check if data-context is still set + Assert.AreEqual(GetListViewHeader()?.DataContext, page.DataContext); } - Assert.AreSame(obj2, added1.Single()); - Assert.AreSame(obj2, removed2.Single()); - Assert.AreSame(obj1, added2.Single()); + UIElement GetListViewHeader() => (panel.GetSupplementaryView(NativeListViewBase.ListViewHeaderElementKindNS, NSIndexPath.FromRowSection(0, 0)) as ListViewBaseInternalContainer)?.Content; } +#endif } public partial class Given_ListViewBase // data class, data-context, view-model, template-selector @@ -4521,6 +4562,60 @@ public LambdaDataTemplateSelector(Func impl) protected override DataTemplate SelectTemplateCore(object item, DependencyObject container) => SelectTemplateCore(item); protected override DataTemplate SelectTemplateCore(object item) => _impl(item); } + + public record When_Header_DataContext_Model(string MyText); + + private sealed class AlwaysEqualClass : IEquatable + { + public bool Equals(AlwaysEqualClass obj) => true; + public override bool Equals(object obj) => true; + public override int GetHashCode() => 0; + } + +#if HAS_UNO + public partial class When_HeaderDataContext_Cleared_FromNavigation_Page : Page + { + public event TypedEventHandler LvHeaderDcChanged; + + public When_HeaderDataContext_Cleared_FromNavigation_Page() + { + DataContext = "MainVM"; + Content = new Grid + { + RowDefinitions = + { + new() { Height = new GridLength(1, GridUnitType.Auto) }, + new() { Height = new GridLength(1, GridUnitType.Star) }, + }, + Children = + { + new Button { Content = "Next" }.Apply(x => + { + Grid.SetRow(x, 0); + x.Click += (s, e) => Frame.Navigate(typeof(BackNavigationPage)); + }), + new ListView + { + ItemsSource = Enumerable.Range(0, 200).Select(x => $"asd {x}"), + HeaderTemplate = new DataTemplate(() => new StackPanel + { + new TextBlock() { Text = "header" }, + new TextBlock().Apply(x => x.SetBinding(TextBlock.TextProperty, new Binding())), + }.Apply(x => x.DataContextChanged += (s, e) => LvHeaderDcChanged?.Invoke(s, e))), + }.Apply(x => Grid.SetRow(x, 1)), + }, + }; + } + } +#endif + + public partial class BackNavigationPage : Page + { + public BackNavigationPage() + { + Content = new Button().Apply(x => x.Click += (s, e) => Frame.GoBack()); + } + } } public partial class Given_ListViewBase // helpers diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.iOS.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.iOS.cs index 3bc1a2fbbe59..9a3892f7ebe3 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.iOS.cs @@ -232,5 +232,20 @@ private protected override void Refresh() InvalidateMeasure(); } } + + public override void MovedToWindow() + { + base.MovedToWindow(); + + // Uno#13172: The header container can sometimes lose its data-context between/after frame navigation, + // especially so when the header has been scrolled out of viewport (far enough to be recycled) once. + if (Window is { }) + { + if (InternalItemsPanelRoot is NativeListViewBase panel) + { + panel.UpdateHeaderAndFooter(); + } + } + } } }