Skip to content

Commit

Permalink
fix: Fix IR not reflecting CollectionChanged.Update raised while bein…
Browse files Browse the repository at this point in the history
…g unloaded
  • Loading branch information
dr1rrb committed Jan 19, 2024
1 parent 0dbdf7e commit 2723df5
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI;
using Microsoft/* UWP don't rename */.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
Expand Down Expand Up @@ -310,6 +311,183 @@ public async Task When_UnloadAndReload_Then_StillListenToCollectionChanged()
await TestServices.WindowHelper.WaitForIdle();
sut.Children.Count(elt => elt.ActualOffset.X >= 0).Should().Be(0);
}

[TestMethod]
[RunsOnUIThread]
#if __MACOS__
[Ignore("Currently fails on macOS, part of #9282 epic")]
#elif __ANDROID__ || __SKIA__
[Ignore("Currently fails https://github.com/unoplatform/uno/issues/9080")]
#endif
public async Task When_UnloadAndReload_Then_ReMaterializeItems()
{
var sut = SUT.Create(5000, new Size(250, 500));

await sut.Load();

var topItems = sut.MaterializedItems.ToArray();

sut.Scroller.ChangeView(null, sut.Scroller.ExtentHeight / 2, null, disableAnimation: true);
await TestServices.WindowHelper.WaitForIdle();

var middleItems = sut.MaterializedItems.ToArray();
middleItems.Should().NotContain(topItems);

await sut.Unload();
await sut.Load();

var reloadedItems = sut.MaterializedItems.ToArray();
reloadedItems.Count(item => middleItems.Contains(item)).Should().BeGreaterThan(2);
}

[TestMethod]
[RunsOnUIThread]
#if __MACOS__
[Ignore("Currently fails on macOS, part of #9282 epic")]
#endif
public async Task When_AddItemWhileUnloaded_Then_MaterializeItems()
{
var sut = SUT.Create();

await sut.Load();

sut.Materialized.Should().Be(3);

await sut.Unload();

sut.Source.Add("Additional item");

await sut.Load();

sut.Materialized.Should().Be(4);
}

[TestMethod]
[RunsOnUIThread]
#if __MACOS__
[Ignore("Currently fails on macOS, part of #9282 epic")]
#endif
public async Task When_RemoveItemWhileUnloaded_Then_MaterializeItems()
{
var sut = SUT.Create();

await sut.Load();

sut.Materialized.Should().Be(3);

await sut.Unload();

sut.Source.RemoveAt(1);

await sut.Load();

sut.Materialized.Should().Be(2);
}

[TestMethod]
[RunsOnUIThread]
#if __MACOS__
[Ignore("Currently fails on macOS, part of #9282 epic")]
#endif
public async Task When_EditItemWhileUnloaded_Then_MaterializeItems()
{
var sut = SUT.Create();

await sut.Load();

sut.Materialized.Should().Be(3);

await sut.Unload();

sut.Source[1] = "Item #1 - Edited";

await sut.Load();

sut.Repeater.Children.FirstOrDefault(g => g.DataContext as string == "Item #1 - Edited").Should().NotBeNull();
}

[TestMethod]
[RunsOnUIThread]
#if __MACOS__
[Ignore("Currently fails on macOS, part of #9282 epic")]
#endif
public async Task When_ClearItemsWhileUnloaded_Then_MaterializeItems()
{
var sut = SUT.Create();

await sut.Load();

sut.Materialized.Should().Be(3);

await sut.Unload();

sut.Source.Clear();

await sut.Load();

sut.Materialized.Should().Be(0);
}

private record SUT(Border Root, ScrollViewer Scroller, ItemsRepeater Repeater, ObservableCollection<string> Source)
{
public static SUT Create(int itemsCount = 3, Size? viewport = default)
{
var repeater = default(ItemsRepeater);
var scroller = default(ScrollViewer);
var source = new ObservableCollection<string>(Enumerable.Range(0, itemsCount).Select(i => $"Item #{i}"));
var root = new Border
{
BorderThickness = new Thickness(5),
BorderBrush = new SolidColorBrush(Colors.Purple),
Child = (scroller = new ScrollViewer
{
Content = (repeater = new ItemsRepeater
{
ItemsSource = source,
Layout = new StackLayout(),
ItemTemplate = new DataTemplate(() => new Border
{
Width = 100,
Height = 100,
Background = new SolidColorBrush(Colors.DeepSkyBlue),
Margin = new Thickness(10),
Child = new TextBlock().Apply(tb => tb.SetBinding(TextBlock.TextProperty, new Binding()))
})
})
})
};

if (viewport is not null)
{
root.Height = viewport.Value.Height;
root.Width = viewport.Value.Width;
}

return new(root, scroller, repeater, source);
}

public int Materialized => Repeater.Children.Count(elt => elt.ActualOffset.X >= 0);

public IEnumerable<string> MaterializedItems => Repeater.Children.Where(elt => elt.ActualOffset.X >= 0).Select(elt => elt.DataContext?.ToString());

public async ValueTask Load()
{
Root.Child = Scroller;
if (TestServices.WindowHelper.WindowContent != Root)
{
TestServices.WindowHelper.WindowContent = Root;
}
await TestServices.WindowHelper.WaitForIdle();
Repeater.IsLoaded.Should().BeTrue();
}

public async ValueTask Unload()
{
Root.Child = new TextBlock { Text = "IR unloaded" };
await TestServices.WindowHelper.WaitForIdle();
Repeater.IsLoaded.Should().BeFalse();
}
}
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,15 @@ private void OnUnloaded(object sender, RoutedEventArgs args)
// because ItemsRepeater uses a "singleton" instance of default StackLayout.
_layoutSubscriptionsRevoker.Disposable = null;
_dataSourceSubscriptionsRevoker.Disposable = null;
if (m_itemsSourceView is not null)
{
// We will no longer receive the element changes until next load.
// While add and remove will be detected on next layout pass, a replace would not be reflected in the UI.
// To fix that, we send a fake reset collection changed in order to mark all containers as recyclable.
// Note: We do it on unload rather than on load because we want to avoid multiple layout-pass on next load.
// This is expected to only flag containers as recyclable and should not have any significant perf impact.
OnItemsSourceViewChanged(m_itemsSourceView, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
#endif
}

Expand Down

0 comments on commit 2723df5

Please sign in to comment.