VirtualizingStackPanelTests.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Collections.ObjectModel;
  5. using System.Collections.Specialized;
  6. using System.Linq;
  7. using Avalonia.Collections;
  8. using Avalonia.Controls.Presenters;
  9. using Avalonia.Controls.Templates;
  10. using Avalonia.Data;
  11. using Avalonia.Layout;
  12. using Avalonia.LogicalTree;
  13. using Avalonia.Media;
  14. using Avalonia.Styling;
  15. using Avalonia.UnitTests;
  16. using Avalonia.VisualTree;
  17. using Xunit;
  18. #nullable enable
  19. namespace Avalonia.Controls.UnitTests
  20. {
  21. public class VirtualizingStackPanelTests
  22. {
  23. [Fact]
  24. public void Creates_Initial_Items()
  25. {
  26. using var app = App();
  27. var (target, scroll, itemsControl) = CreateTarget();
  28. Assert.Equal(1000, scroll.Extent.Height);
  29. AssertRealizedItems(target, itemsControl, 0, 10);
  30. }
  31. [Fact]
  32. public void Initializes_Initial_Control_Items()
  33. {
  34. using var app = App();
  35. var items = Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10});
  36. var (target, scroll, itemsControl) = CreateTarget(items: items, useItemTemplate: false);
  37. Assert.Equal(1000, scroll.Extent.Height);
  38. AssertRealizedControlItems<Button>(target, itemsControl, 0, 10);
  39. }
  40. [Fact]
  41. public void Creates_Reassigned_Items()
  42. {
  43. using var app = App();
  44. var (target, scroll, itemsControl) = CreateTarget(items: Array.Empty<object>());
  45. Assert.Empty(itemsControl.GetRealizedContainers());
  46. itemsControl.Items = new[] { "foo", "bar" };
  47. Layout(target);
  48. AssertRealizedItems(target, itemsControl, 0, 2);
  49. }
  50. [Fact]
  51. public void Scrolls_Down_One_Item()
  52. {
  53. using var app = App();
  54. var (target, scroll, itemsControl) = CreateTarget();
  55. scroll.Offset = new Vector(0, 10);
  56. Layout(target);
  57. AssertRealizedItems(target, itemsControl, 1, 10);
  58. }
  59. [Fact]
  60. public void Scrolls_Down_More_Than_A_Page()
  61. {
  62. using var app = App();
  63. var (target, scroll, itemsControl) = CreateTarget();
  64. scroll.Offset = new Vector(0, 200);
  65. Layout(target);
  66. AssertRealizedItems(target, itemsControl, 20, 10);
  67. }
  68. [Fact]
  69. public void Scrolls_To_Index()
  70. {
  71. using var app = App();
  72. var (target, scroll, itemsControl) = CreateTarget();
  73. target.ScrollIntoView(20);
  74. AssertRealizedItems(target, itemsControl, 11, 10);
  75. }
  76. [Fact]
  77. public void Creates_Elements_On_Item_Insert()
  78. {
  79. using var app = App();
  80. var (target, _, itemsControl) = CreateTarget();
  81. var items = (IList)itemsControl.Items!;
  82. Assert.Equal(10, target.GetRealizedElements().Count);
  83. items.Insert(2, "new");
  84. Assert.Equal(11, target.GetRealizedElements().Count);
  85. var indexes = GetRealizedIndexes(target, itemsControl);
  86. // Blank space inserted in realized elements and subsequent row indexes updated.
  87. Assert.Equal(new[] { 0, 1, -1, 3, 4, 5, 6, 7, 8, 9, 10 }, indexes);
  88. var elements = target.GetRealizedElements().ToList();
  89. Layout(target);
  90. indexes = GetRealizedIndexes(target, itemsControl);
  91. // After layout an element for the new row is created.
  92. Assert.Equal(Enumerable.Range(0, 10), indexes);
  93. // But apart from the new row and the removed last row, all existing elements should be the same.
  94. elements[2] = target.GetRealizedElements().ElementAt(2);
  95. elements.RemoveAt(elements.Count - 1);
  96. Assert.Equal(elements, target.GetRealizedElements());
  97. }
  98. [Fact]
  99. public void Updates_Elements_On_Item_Remove()
  100. {
  101. using var app = App();
  102. var (target, _, itemsControl) = CreateTarget();
  103. var items = (IList)itemsControl.Items!;
  104. Assert.Equal(10, target.GetRealizedElements().Count);
  105. var toRecycle = target.GetRealizedElements().ElementAt(2);
  106. items.RemoveAt(2);
  107. var indexes = GetRealizedIndexes(target, itemsControl);
  108. // Item removed from realized elements and subsequent row indexes updated.
  109. Assert.Equal(Enumerable.Range(0, 9), indexes);
  110. var elements = target.GetRealizedElements().ToList();
  111. Layout(target);
  112. indexes = GetRealizedIndexes(target, itemsControl);
  113. // After layout an element for the newly visible last row is created and indexes updated.
  114. Assert.Equal(Enumerable.Range(0, 10), indexes);
  115. // And the removed row should now have been recycled as the last row.
  116. elements.Add(toRecycle);
  117. Assert.Equal(elements, target.GetRealizedElements());
  118. }
  119. [Fact]
  120. public void Updates_Elements_On_Item_Replace()
  121. {
  122. using var app = App();
  123. var (target, _, itemsControl) = CreateTarget();
  124. var items = (ObservableCollection<string>)itemsControl.Items!;
  125. Assert.Equal(10, target.GetRealizedElements().Count);
  126. var toReplace = target.GetRealizedElements().ElementAt(2);
  127. items[2] = "new";
  128. // Container being replaced should have been recycled.
  129. Assert.DoesNotContain(toReplace, target.GetRealizedElements());
  130. Assert.False(toReplace!.IsVisible);
  131. var indexes = GetRealizedIndexes(target, itemsControl);
  132. // Item removed from realized elements at old position and space inserted at new position.
  133. Assert.Equal(new[] { 0, 1, -1, 3, 4, 5, 6, 7, 8, 9 }, indexes);
  134. Layout(target);
  135. indexes = GetRealizedIndexes(target, itemsControl);
  136. // After layout the missing container should have been created.
  137. Assert.Equal(Enumerable.Range(0, 10), indexes);
  138. }
  139. [Fact]
  140. public void Updates_Elements_On_Item_Move()
  141. {
  142. using var app = App();
  143. var (target, _, itemsControl) = CreateTarget();
  144. var items = (ObservableCollection<string>)itemsControl.Items!;
  145. Assert.Equal(10, target.GetRealizedElements().Count);
  146. var toMove = target.GetRealizedElements().ElementAt(2);
  147. items.Move(2, 6);
  148. // Container being moved should have been recycled.
  149. Assert.DoesNotContain(toMove, target.GetRealizedElements());
  150. Assert.False(toMove!.IsVisible);
  151. var indexes = GetRealizedIndexes(target, itemsControl);
  152. // Item removed from realized elements at old position and space inserted at new position.
  153. Assert.Equal(new[] { 0, 1, 2, 3, 4, 5, -1, 7, 8, 9 }, indexes);
  154. Layout(target);
  155. indexes = GetRealizedIndexes(target, itemsControl);
  156. // After layout the missing container should have been created.
  157. Assert.Equal(Enumerable.Range(0, 10), indexes);
  158. }
  159. [Fact]
  160. public void Removes_Control_Items_From_Panel_On_Item_Remove()
  161. {
  162. using var app = App();
  163. var items = new ObservableCollection<Button>(Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10 }));
  164. var (target, scroll, itemsControl) = CreateTarget(items: items, useItemTemplate: false);
  165. Assert.Equal(1000, scroll.Extent.Height);
  166. var removed = items[1];
  167. items.RemoveAt(1);
  168. Assert.Null(removed.Parent);
  169. Assert.Null(removed.VisualParent);
  170. }
  171. [Fact]
  172. public void Does_Not_Recycle_Focused_Element()
  173. {
  174. using var app = App();
  175. var (target, scroll, itemsControl) = CreateTarget();
  176. target.GetRealizedElements().First()!.Focus();
  177. Assert.True(target.GetRealizedElements().First()!.IsKeyboardFocusWithin);
  178. scroll.Offset = new Vector(0, 200);
  179. Layout(target);
  180. Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin));
  181. }
  182. [Fact]
  183. public void Removing_Item_Of_Focused_Element_Clears_Focus()
  184. {
  185. using var app = App();
  186. var (target, scroll, itemsControl) = CreateTarget();
  187. var focused = target.GetRealizedElements().First()!;
  188. focused.Focus();
  189. Assert.True(focused.IsKeyboardFocusWithin);
  190. scroll.Offset = new Vector(0, 200);
  191. Layout(target);
  192. Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin));
  193. Assert.All(target.GetRealizedElements(), x => Assert.NotSame(focused, x));
  194. }
  195. [Fact]
  196. public void Scrolling_Back_To_Focused_Element_Uses_Correct_Element()
  197. {
  198. using var app = App();
  199. var (target, scroll, itemsControl) = CreateTarget();
  200. var focused = target.GetRealizedElements().First()!;
  201. focused.Focus();
  202. Assert.True(focused.IsKeyboardFocusWithin);
  203. scroll.Offset = new Vector(0, 200);
  204. Layout(target);
  205. scroll.Offset = new Vector(0, 0);
  206. Layout(target);
  207. Assert.Same(focused, target.GetRealizedElements().First());
  208. }
  209. [Fact]
  210. public void Removing_Range_When_Scrolled_To_End_Updates_Viewport()
  211. {
  212. using var app = App();
  213. var items = new AvaloniaList<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  214. var (target, scroll, itemsControl) = CreateTarget(items: items);
  215. scroll.Offset = new Vector(0, 900);
  216. Layout(target);
  217. AssertRealizedItems(target, itemsControl, 90, 10);
  218. items.RemoveRange(0, 80);
  219. Layout(target);
  220. AssertRealizedItems(target, itemsControl, 10, 10);
  221. Assert.Equal(new Vector(0, 100), scroll.Offset);
  222. }
  223. [Fact]
  224. public void Removing_Range_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport()
  225. {
  226. using var app = App();
  227. var items = new AvaloniaList<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  228. var (target, scroll, itemsControl) = CreateTarget(items: items);
  229. scroll.Offset = new Vector(0, 900);
  230. Layout(target);
  231. AssertRealizedItems(target, itemsControl, 90, 10);
  232. items.RemoveRange(0, 95);
  233. Layout(target);
  234. AssertRealizedItems(target, itemsControl, 0, 5);
  235. Assert.Equal(new Vector(0, 0), scroll.Offset);
  236. }
  237. [Fact]
  238. public void Resetting_Collection_To_Have_Less_Items_When_Scrolled_To_End_Updates_Viewport()
  239. {
  240. using var app = App();
  241. var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  242. var (target, scroll, itemsControl) = CreateTarget(items: items);
  243. scroll.Offset = new Vector(0, 900);
  244. Layout(target);
  245. AssertRealizedItems(target, itemsControl, 90, 10);
  246. items.Reset(Enumerable.Range(0, 20).Select(x => $"Item {x}"));
  247. Layout(target);
  248. AssertRealizedItems(target, itemsControl, 10, 10);
  249. Assert.Equal(new Vector(0, 100), scroll.Offset);
  250. }
  251. [Fact]
  252. public void Resetting_Collection_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport()
  253. {
  254. using var app = App();
  255. var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  256. var (target, scroll, itemsControl) = CreateTarget(items: items);
  257. scroll.Offset = new Vector(0, 900);
  258. Layout(target);
  259. AssertRealizedItems(target, itemsControl, 90, 10);
  260. items.Reset(Enumerable.Range(0, 5).Select(x => $"Item {x}"));
  261. Layout(target);
  262. AssertRealizedItems(target, itemsControl, 0, 5);
  263. Assert.Equal(new Vector(0, 0), scroll.Offset);
  264. }
  265. [Fact]
  266. public void NthChild_Selector_Works()
  267. {
  268. using var app = App();
  269. var style = new Style(x => x.OfType<ContentPresenter>().NthChild(5, 0))
  270. {
  271. Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
  272. };
  273. var (target, _, _) = CreateTarget(styles: new[] { style });
  274. var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
  275. Assert.Equal(10, realized.Count);
  276. for (var i = 0; i < 10; ++i)
  277. {
  278. var container = realized[i];
  279. var index = target.IndexFromContainer(container);
  280. var expectedBackground = (i == 4 || i == 9) ? Brushes.Red : null;
  281. Assert.Equal(i, index);
  282. Assert.Equal(expectedBackground, container.Background);
  283. }
  284. }
  285. [Fact]
  286. public void NthLastChild_Selector_Works()
  287. {
  288. using var app = App();
  289. var style = new Style(x => x.OfType<ContentPresenter>().NthLastChild(5, 0))
  290. {
  291. Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
  292. };
  293. var (target, _, _) = CreateTarget(styles: new[] { style });
  294. var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
  295. Assert.Equal(10, realized.Count);
  296. for (var i = 0; i < 10; ++i)
  297. {
  298. var container = realized[i];
  299. var index = target.IndexFromContainer(container);
  300. var expectedBackground = (i == 0 || i == 5) ? Brushes.Red : null;
  301. Assert.Equal(i, index);
  302. Assert.Equal(expectedBackground, container.Background);
  303. }
  304. }
  305. private static IReadOnlyList<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
  306. {
  307. return target.GetRealizedElements()
  308. .Select(x => x is null ? -1 : itemsControl.IndexFromContainer((Control)x))
  309. .ToList();
  310. }
  311. private static void AssertRealizedItems(
  312. VirtualizingStackPanel target,
  313. ItemsControl itemsControl,
  314. int firstIndex,
  315. int count)
  316. {
  317. Assert.All(target.GetRealizedContainers(), x => Assert.Same(target, x.VisualParent));
  318. Assert.All(target.GetRealizedContainers(), x => Assert.Same(itemsControl, x.Parent));
  319. var childIndexes = target.GetRealizedContainers()?
  320. .Select(x => itemsControl.IndexFromContainer(x))
  321. .Where(x => x >= 0)
  322. .OrderBy(x => x)
  323. .ToList();
  324. Assert.Equal(Enumerable.Range(firstIndex, count), childIndexes);
  325. }
  326. private static void AssertRealizedControlItems<TContainer>(
  327. VirtualizingStackPanel target,
  328. ItemsControl itemsControl,
  329. int firstIndex,
  330. int count)
  331. {
  332. Assert.All(target.GetRealizedContainers(), x => Assert.IsType<TContainer>(x));
  333. Assert.All(target.GetRealizedContainers(), x => Assert.Same(target, x.VisualParent));
  334. Assert.All(target.GetRealizedContainers(), x => Assert.Same(itemsControl, x.Parent));
  335. var childIndexes = target.GetRealizedContainers()?
  336. .Select(x => itemsControl.IndexFromContainer(x))
  337. .Where(x => x >= 0)
  338. .OrderBy(x => x)
  339. .ToList();
  340. Assert.Equal(Enumerable.Range(firstIndex, count), childIndexes);
  341. }
  342. private static (VirtualizingStackPanel, ScrollViewer, ItemsControl) CreateTarget(
  343. IEnumerable<object>? items = null,
  344. bool useItemTemplate = true,
  345. IEnumerable<Style>? styles = null)
  346. {
  347. var target = new VirtualizingStackPanel();
  348. items ??= new ObservableCollection<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  349. var presenter = new ItemsPresenter
  350. {
  351. [~ItemsPresenter.ItemsPanelProperty] = new TemplateBinding(ItemsPresenter.ItemsPanelProperty),
  352. };
  353. var scroll = new ScrollViewer
  354. {
  355. Content = presenter,
  356. Template = ScrollViewerTemplate(),
  357. };
  358. var itemsControl = new ItemsControl
  359. {
  360. Items = items,
  361. Template = new FuncControlTemplate<ItemsControl>((_, _) => scroll),
  362. ItemsPanel = new FuncTemplate<Panel>(() => target),
  363. };
  364. if (useItemTemplate)
  365. itemsControl.ItemTemplate = new FuncDataTemplate<object>((x, _) => new Canvas { Width = 100, Height = 10 });
  366. var root = new TestRoot(true, itemsControl);
  367. root.ClientSize = new(100, 100);
  368. if (styles is not null)
  369. root.Styles.AddRange(styles);
  370. root.LayoutManager.ExecuteInitialLayoutPass();
  371. return (target, scroll, itemsControl);
  372. }
  373. private static void Layout(Control target)
  374. {
  375. var root = (ILayoutRoot?)target.GetVisualRoot();
  376. root?.LayoutManager.ExecuteLayoutPass();
  377. }
  378. private static IControlTemplate ScrollViewerTemplate()
  379. {
  380. return new FuncControlTemplate<ScrollViewer>((x, ns) =>
  381. new ScrollContentPresenter
  382. {
  383. Name = "PART_ContentPresenter",
  384. [~ContentPresenter.ContentProperty] = x[~ContentControl.ContentProperty],
  385. [~~ScrollContentPresenter.ExtentProperty] = x[~~ScrollViewer.ExtentProperty],
  386. [~~ScrollContentPresenter.OffsetProperty] = x[~~ScrollViewer.OffsetProperty],
  387. [~~ScrollContentPresenter.ViewportProperty] = x[~~ScrollViewer.ViewportProperty],
  388. [~ScrollContentPresenter.CanHorizontallyScrollProperty] = x[~ScrollViewer.CanHorizontallyScrollProperty],
  389. [~ScrollContentPresenter.CanVerticallyScrollProperty] = x[~ScrollViewer.CanVerticallyScrollProperty],
  390. }.RegisterInNameScope(ns));
  391. }
  392. private static IDisposable App() => UnitTestApplication.Start(TestServices.RealFocus);
  393. private class ResettingCollection : List<string>, INotifyCollectionChanged
  394. {
  395. public ResettingCollection(IEnumerable<string> items)
  396. {
  397. AddRange(items);
  398. }
  399. public void Reset(IEnumerable<string> items)
  400. {
  401. Clear();
  402. AddRange(items);
  403. CollectionChanged?.Invoke(
  404. this,
  405. new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
  406. }
  407. public event NotifyCollectionChangedEventHandler? CollectionChanged;
  408. }
  409. }
  410. }