123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534 |
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Collections.ObjectModel;
- using System.Collections.Specialized;
- using System.Linq;
- using Avalonia.Collections;
- using Avalonia.Controls.Presenters;
- using Avalonia.Controls.Templates;
- using Avalonia.Data;
- using Avalonia.Layout;
- using Avalonia.Media;
- using Avalonia.Styling;
- using Avalonia.UnitTests;
- using Avalonia.VisualTree;
- using Xunit;
- namespace Avalonia.Controls.UnitTests
- {
- public class VirtualizingStackPanelTests
- {
- [Fact]
- public void Creates_Initial_Items()
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget();
- Assert.Equal(1000, scroll.Extent.Height);
- AssertRealizedItems(target, itemsControl, 0, 10);
- }
- [Fact]
- public void Initializes_Initial_Control_Items()
- {
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10});
- var (target, scroll, itemsControl) = CreateTarget(items: items, useItemTemplate: false);
- Assert.Equal(1000, scroll.Extent.Height);
- AssertRealizedControlItems<Button>(target, itemsControl, 0, 10);
- }
- [Fact]
- public void Creates_Reassigned_Items()
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(items: Array.Empty<object>());
- Assert.Empty(itemsControl.GetRealizedContainers());
- itemsControl.ItemsSource = new[] { "foo", "bar" };
- Layout(target);
- AssertRealizedItems(target, itemsControl, 0, 2);
- }
- [Fact]
- public void Scrolls_Down_One_Item()
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget();
- scroll.Offset = new Vector(0, 10);
- Layout(target);
- AssertRealizedItems(target, itemsControl, 1, 10);
- }
- [Fact]
- public void Scrolls_Down_More_Than_A_Page()
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget();
- scroll.Offset = new Vector(0, 200);
- Layout(target);
- AssertRealizedItems(target, itemsControl, 20, 10);
- }
- [Fact]
- public void Scrolls_To_Index()
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget();
- target.ScrollIntoView(20);
- AssertRealizedItems(target, itemsControl, 11, 10);
- }
- [Fact]
- public void Creates_Elements_On_Item_Insert()
- {
- using var app = App();
- var (target, _, itemsControl) = CreateTarget();
- var items = (IList)itemsControl.ItemsSource!;
- Assert.Equal(10, target.GetRealizedElements().Count);
- items.Insert(2, "new");
- Assert.Equal(11, target.GetRealizedElements().Count);
- var indexes = GetRealizedIndexes(target, itemsControl);
- // Blank space inserted in realized elements and subsequent row indexes updated.
- Assert.Equal(new[] { 0, 1, -1, 3, 4, 5, 6, 7, 8, 9, 10 }, indexes);
- var elements = target.GetRealizedElements().ToList();
- Layout(target);
- indexes = GetRealizedIndexes(target, itemsControl);
- // After layout an element for the new row is created.
- Assert.Equal(Enumerable.Range(0, 10), indexes);
- // But apart from the new row and the removed last row, all existing elements should be the same.
- elements[2] = target.GetRealizedElements().ElementAt(2);
- elements.RemoveAt(elements.Count - 1);
- Assert.Equal(elements, target.GetRealizedElements());
- }
- [Fact]
- public void Updates_Elements_On_Item_Remove()
- {
- using var app = App();
- var (target, _, itemsControl) = CreateTarget();
- var items = (IList)itemsControl.ItemsSource!;
- Assert.Equal(10, target.GetRealizedElements().Count);
- var toRecycle = target.GetRealizedElements().ElementAt(2);
- items.RemoveAt(2);
- var indexes = GetRealizedIndexes(target, itemsControl);
- // Item removed from realized elements and subsequent row indexes updated.
- Assert.Equal(Enumerable.Range(0, 9), indexes);
- var elements = target.GetRealizedElements().ToList();
- Layout(target);
- indexes = GetRealizedIndexes(target, itemsControl);
- // After layout an element for the newly visible last row is created and indexes updated.
- Assert.Equal(Enumerable.Range(0, 10), indexes);
- // And the removed row should now have been recycled as the last row.
- elements.Add(toRecycle);
- Assert.Equal(elements, target.GetRealizedElements());
- }
- [Fact]
- public void Updates_Elements_On_Item_Replace()
- {
- using var app = App();
- var (target, _, itemsControl) = CreateTarget();
- var items = (ObservableCollection<string>)itemsControl.ItemsSource!;
- Assert.Equal(10, target.GetRealizedElements().Count);
- var toReplace = target.GetRealizedElements().ElementAt(2);
- items[2] = "new";
- // Container being replaced should have been recycled.
- Assert.DoesNotContain(toReplace, target.GetRealizedElements());
- Assert.False(toReplace!.IsVisible);
- var indexes = GetRealizedIndexes(target, itemsControl);
- // Item removed from realized elements at old position and space inserted at new position.
- Assert.Equal(new[] { 0, 1, -1, 3, 4, 5, 6, 7, 8, 9 }, indexes);
- Layout(target);
- indexes = GetRealizedIndexes(target, itemsControl);
- // After layout the missing container should have been created.
- Assert.Equal(Enumerable.Range(0, 10), indexes);
- }
- [Fact]
- public void Updates_Elements_On_Item_Move()
- {
- using var app = App();
- var (target, _, itemsControl) = CreateTarget();
- var items = (ObservableCollection<string>)itemsControl.ItemsSource!;
- Assert.Equal(10, target.GetRealizedElements().Count);
- var toMove = target.GetRealizedElements().ElementAt(2);
- items.Move(2, 6);
- // Container being moved should have been recycled.
- Assert.DoesNotContain(toMove, target.GetRealizedElements());
- Assert.False(toMove!.IsVisible);
- var indexes = GetRealizedIndexes(target, itemsControl);
- // Item removed from realized elements at old position and space inserted at new position.
- Assert.Equal(new[] { 0, 1, 2, 3, 4, 5, -1, 7, 8, 9 }, indexes);
- Layout(target);
- indexes = GetRealizedIndexes(target, itemsControl);
- // After layout the missing container should have been created.
- Assert.Equal(Enumerable.Range(0, 10), indexes);
- }
- [Fact]
- public void Removes_Control_Items_From_Panel_On_Item_Remove()
- {
- using var app = App();
- var items = new ObservableCollection<Button>(Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10 }));
- var (target, scroll, itemsControl) = CreateTarget(items: items, useItemTemplate: false);
- Assert.Equal(1000, scroll.Extent.Height);
- var removed = items[1];
- items.RemoveAt(1);
- Assert.Null(removed.Parent);
- Assert.Null(removed.VisualParent);
- }
- [Fact]
- public void Does_Not_Recycle_Focused_Element()
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget();
- target.GetRealizedElements().First()!.Focus();
- Assert.True(target.GetRealizedElements().First()!.IsKeyboardFocusWithin);
- scroll.Offset = new Vector(0, 200);
- Layout(target);
- Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin));
- }
- [Fact]
- public void Removing_Item_Of_Focused_Element_Clears_Focus()
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget();
- var focused = target.GetRealizedElements().First()!;
- focused.Focus();
- Assert.True(focused.IsKeyboardFocusWithin);
- scroll.Offset = new Vector(0, 200);
- Layout(target);
- Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin));
- Assert.All(target.GetRealizedElements(), x => Assert.NotSame(focused, x));
- }
- [Fact]
- public void Scrolling_Back_To_Focused_Element_Uses_Correct_Element()
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget();
- var focused = target.GetRealizedElements().First()!;
- focused.Focus();
- Assert.True(focused.IsKeyboardFocusWithin);
- scroll.Offset = new Vector(0, 200);
- Layout(target);
- scroll.Offset = new Vector(0, 0);
- Layout(target);
- Assert.Same(focused, target.GetRealizedElements().First());
- }
- [Fact]
- public void Removing_Range_When_Scrolled_To_End_Updates_Viewport()
- {
- using var app = App();
- var items = new AvaloniaList<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
- var (target, scroll, itemsControl) = CreateTarget(items: items);
- scroll.Offset = new Vector(0, 900);
- Layout(target);
- AssertRealizedItems(target, itemsControl, 90, 10);
- items.RemoveRange(0, 80);
- Layout(target);
- AssertRealizedItems(target, itemsControl, 10, 10);
- Assert.Equal(new Vector(0, 100), scroll.Offset);
- }
- [Fact]
- public void Removing_Range_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport()
- {
- using var app = App();
- var items = new AvaloniaList<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
- var (target, scroll, itemsControl) = CreateTarget(items: items);
- scroll.Offset = new Vector(0, 900);
- Layout(target);
- AssertRealizedItems(target, itemsControl, 90, 10);
- items.RemoveRange(0, 95);
- Layout(target);
- AssertRealizedItems(target, itemsControl, 0, 5);
- Assert.Equal(new Vector(0, 0), scroll.Offset);
- }
- [Fact]
- public void Resetting_Collection_To_Have_Less_Items_When_Scrolled_To_End_Updates_Viewport()
- {
- using var app = App();
- var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
- var (target, scroll, itemsControl) = CreateTarget(items: items);
- scroll.Offset = new Vector(0, 900);
- Layout(target);
- AssertRealizedItems(target, itemsControl, 90, 10);
- items.Reset(Enumerable.Range(0, 20).Select(x => $"Item {x}"));
- Layout(target);
- AssertRealizedItems(target, itemsControl, 10, 10);
- Assert.Equal(new Vector(0, 100), scroll.Offset);
- }
- [Fact]
- public void Resetting_Collection_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport()
- {
- using var app = App();
- var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
- var (target, scroll, itemsControl) = CreateTarget(items: items);
- scroll.Offset = new Vector(0, 900);
- Layout(target);
- AssertRealizedItems(target, itemsControl, 90, 10);
- items.Reset(Enumerable.Range(0, 5).Select(x => $"Item {x}"));
- Layout(target);
- AssertRealizedItems(target, itemsControl, 0, 5);
- Assert.Equal(new Vector(0, 0), scroll.Offset);
- }
- [Fact]
- public void NthChild_Selector_Works()
- {
- using var app = App();
-
- var style = new Style(x => x.OfType<ContentPresenter>().NthChild(5, 0))
- {
- Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
- };
- var (target, _, _) = CreateTarget(styles: new[] { style });
- var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
-
- Assert.Equal(10, realized.Count);
-
- for (var i = 0; i < 10; ++i)
- {
- var container = realized[i];
- var index = target.IndexFromContainer(container);
- var expectedBackground = (i == 4 || i == 9) ? Brushes.Red : null;
- Assert.Equal(i, index);
- Assert.Equal(expectedBackground, container.Background);
- }
- }
- [Fact]
- public void NthLastChild_Selector_Works()
- {
- using var app = App();
- var style = new Style(x => x.OfType<ContentPresenter>().NthLastChild(5, 0))
- {
- Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
- };
- var (target, _, _) = CreateTarget(styles: new[] { style });
- var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
- Assert.Equal(10, realized.Count);
- for (var i = 0; i < 10; ++i)
- {
- var container = realized[i];
- var index = target.IndexFromContainer(container);
- var expectedBackground = (i == 0 || i == 5) ? Brushes.Red : null;
- Assert.Equal(i, index);
- Assert.Equal(expectedBackground, container.Background);
- }
- }
- private static IReadOnlyList<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
- {
- return target.GetRealizedElements()
- .Select(x => x is null ? -1 : itemsControl.IndexFromContainer((Control)x))
- .ToList();
- }
- private static void AssertRealizedItems(
- VirtualizingStackPanel target,
- ItemsControl itemsControl,
- int firstIndex,
- int count)
- {
- Assert.All(target.GetRealizedContainers(), x => Assert.Same(target, x.VisualParent));
- Assert.All(target.GetRealizedContainers(), x => Assert.Same(itemsControl, x.Parent));
- var childIndexes = target.GetRealizedContainers()?
- .Select(x => itemsControl.IndexFromContainer(x))
- .Where(x => x >= 0)
- .OrderBy(x => x)
- .ToList();
- Assert.Equal(Enumerable.Range(firstIndex, count), childIndexes);
- }
- private static void AssertRealizedControlItems<TContainer>(
- VirtualizingStackPanel target,
- ItemsControl itemsControl,
- int firstIndex,
- int count)
- {
- Assert.All(target.GetRealizedContainers(), x => Assert.IsType<TContainer>(x));
- Assert.All(target.GetRealizedContainers(), x => Assert.Same(target, x.VisualParent));
- Assert.All(target.GetRealizedContainers(), x => Assert.Same(itemsControl, x.Parent));
- var childIndexes = target.GetRealizedContainers()?
- .Select(x => itemsControl.IndexFromContainer(x))
- .Where(x => x >= 0)
- .OrderBy(x => x)
- .ToList();
- Assert.Equal(Enumerable.Range(firstIndex, count), childIndexes);
- }
- private static (VirtualizingStackPanel, ScrollViewer, ItemsControl) CreateTarget(
- IEnumerable<object>? items = null,
- bool useItemTemplate = true,
- IEnumerable<Style>? styles = null)
- {
- var target = new VirtualizingStackPanel();
- items ??= new ObservableCollection<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
- var presenter = new ItemsPresenter
- {
- [~ItemsPresenter.ItemsPanelProperty] = new TemplateBinding(ItemsPresenter.ItemsPanelProperty),
- };
- var scroll = new ScrollViewer
- {
- Content = presenter,
- Template = ScrollViewerTemplate(),
- };
- var itemsControl = new ItemsControl
- {
- ItemsSource = items,
- Template = new FuncControlTemplate<ItemsControl>((_, _) => scroll),
- ItemsPanel = new FuncTemplate<Panel>(() => target),
- };
- if (useItemTemplate)
- itemsControl.ItemTemplate = new FuncDataTemplate<object>((x, _) => new Canvas { Width = 100, Height = 10 });
- var root = new TestRoot(true, itemsControl);
- root.ClientSize = new(100, 100);
- if (styles is not null)
- root.Styles.AddRange(styles);
- root.LayoutManager.ExecuteInitialLayoutPass();
- return (target, scroll, itemsControl);
- }
- private static void Layout(Control target)
- {
- var root = (ILayoutRoot?)target.GetVisualRoot();
- root?.LayoutManager.ExecuteLayoutPass();
- }
- private static IControlTemplate ScrollViewerTemplate()
- {
- return new FuncControlTemplate<ScrollViewer>((x, ns) =>
- new ScrollContentPresenter
- {
- Name = "PART_ContentPresenter",
- [~ContentPresenter.ContentProperty] = x[~ContentControl.ContentProperty],
- [~~ScrollContentPresenter.ExtentProperty] = x[~~ScrollViewer.ExtentProperty],
- [~~ScrollContentPresenter.OffsetProperty] = x[~~ScrollViewer.OffsetProperty],
- [~~ScrollContentPresenter.ViewportProperty] = x[~~ScrollViewer.ViewportProperty],
- [~ScrollContentPresenter.CanHorizontallyScrollProperty] = x[~ScrollViewer.CanHorizontallyScrollProperty],
- [~ScrollContentPresenter.CanVerticallyScrollProperty] = x[~ScrollViewer.CanVerticallyScrollProperty],
- }.RegisterInNameScope(ns));
- }
- private static IDisposable App() => UnitTestApplication.Start(TestServices.RealFocus);
- private class ResettingCollection : List<string>, INotifyCollectionChanged
- {
- public ResettingCollection(IEnumerable<string> items)
- {
- AddRange(items);
- }
- public void Reset(IEnumerable<string> items)
- {
- Clear();
- AddRange(items);
- CollectionChanged?.Invoke(
- this,
- new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
- }
- public event NotifyCollectionChangedEventHandler? CollectionChanged;
- }
- }
- }
|