VirtualizingStackPanelTests.cs 19 KB

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