VirtualizingStackPanelTests.cs 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894
  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. #nullable enable
  18. namespace Avalonia.Controls.UnitTests
  19. {
  20. public class VirtualizingStackPanelTests
  21. {
  22. [Fact]
  23. public void Creates_Initial_Items()
  24. {
  25. using var app = App();
  26. var (target, scroll, itemsControl) = CreateTarget();
  27. Assert.Equal(1000, scroll.Extent.Height);
  28. AssertRealizedItems(target, itemsControl, 0, 10);
  29. }
  30. [Fact]
  31. public void Initializes_Initial_Control_Items()
  32. {
  33. using var app = App();
  34. var items = Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10});
  35. var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: null);
  36. Assert.Equal(1000, scroll.Extent.Height);
  37. AssertRealizedControlItems<Button>(target, itemsControl, 0, 10);
  38. }
  39. [Fact]
  40. public void Creates_Reassigned_Items()
  41. {
  42. using var app = App();
  43. var (target, scroll, itemsControl) = CreateTarget(items: Array.Empty<object>());
  44. Assert.Empty(itemsControl.GetRealizedContainers());
  45. itemsControl.ItemsSource = new[] { "foo", "bar" };
  46. Layout(target);
  47. AssertRealizedItems(target, itemsControl, 0, 2);
  48. }
  49. [Fact]
  50. public void Scrolls_Down_One_Item()
  51. {
  52. using var app = App();
  53. var (target, scroll, itemsControl) = CreateTarget();
  54. scroll.Offset = new Vector(0, 10);
  55. Layout(target);
  56. AssertRealizedItems(target, itemsControl, 1, 10);
  57. }
  58. [Fact]
  59. public void Scrolls_Down_More_Than_A_Page()
  60. {
  61. using var app = App();
  62. var (target, scroll, itemsControl) = CreateTarget();
  63. scroll.Offset = new Vector(0, 200);
  64. Layout(target);
  65. AssertRealizedItems(target, itemsControl, 20, 10);
  66. }
  67. [Fact]
  68. public void Scrolls_Down_To_Index()
  69. {
  70. using var app = App();
  71. var (target, scroll, itemsControl) = CreateTarget();
  72. target.ScrollIntoView(20);
  73. AssertRealizedItems(target, itemsControl, 11, 10);
  74. }
  75. [Fact]
  76. public void Scrolls_Up_To_Index()
  77. {
  78. using var app = App();
  79. var (target, scroll, itemsControl) = CreateTarget();
  80. scroll.ScrollToEnd();
  81. Layout(target);
  82. Assert.Equal(90, target.FirstRealizedIndex);
  83. target.ScrollIntoView(20);
  84. AssertRealizedItems(target, itemsControl, 20, 10);
  85. }
  86. [Fact]
  87. public void Scrolling_Up_To_Index_Does_Not_Create_A_Page_Of_Unrealized_Elements()
  88. {
  89. using var app = App();
  90. var (target, scroll, itemsControl) = CreateTarget();
  91. scroll.ScrollToEnd();
  92. Layout(target);
  93. target.ScrollIntoView(20);
  94. Assert.Equal(11, target.Children.Count);
  95. }
  96. [Fact]
  97. public void Creates_Elements_On_Item_Insert_1()
  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. items.Insert(0, "new");
  104. Assert.Equal(11, target.GetRealizedElements().Count);
  105. var indexes = GetRealizedIndexes(target, itemsControl);
  106. // Blank space inserted in realized elements and subsequent indexes updated.
  107. Assert.Equal(new[] { -1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, indexes);
  108. var elements = target.GetRealizedElements().ToList();
  109. Layout(target);
  110. indexes = GetRealizedIndexes(target, itemsControl);
  111. // After layout an element for the new element is created.
  112. Assert.Equal(Enumerable.Range(0, 10), indexes);
  113. // But apart from the new element and the removed last element, all existing elements
  114. // should be the same.
  115. elements[0] = target.GetRealizedElements().ElementAt(0);
  116. elements.RemoveAt(elements.Count - 1);
  117. Assert.Equal(elements, target.GetRealizedElements());
  118. }
  119. [Fact]
  120. public void Creates_Elements_On_Item_Insert_2()
  121. {
  122. using var app = App();
  123. var (target, _, itemsControl) = CreateTarget();
  124. var items = (IList)itemsControl.ItemsSource!;
  125. Assert.Equal(10, target.GetRealizedElements().Count);
  126. items.Insert(2, "new");
  127. Assert.Equal(11, target.GetRealizedElements().Count);
  128. var indexes = GetRealizedIndexes(target, itemsControl);
  129. // Blank space inserted in realized elements and subsequent indexes updated.
  130. Assert.Equal(new[] { 0, 1, -1, 3, 4, 5, 6, 7, 8, 9, 10 }, indexes);
  131. var elements = target.GetRealizedElements().ToList();
  132. Layout(target);
  133. indexes = GetRealizedIndexes(target, itemsControl);
  134. // After layout an element for the new element is created.
  135. Assert.Equal(Enumerable.Range(0, 10), indexes);
  136. // But apart from the new element and the removed last element, all existing elements
  137. // should be the same.
  138. elements[2] = target.GetRealizedElements().ElementAt(2);
  139. elements.RemoveAt(elements.Count - 1);
  140. Assert.Equal(elements, target.GetRealizedElements());
  141. }
  142. [Fact]
  143. public void Updates_Elements_On_Item_Remove()
  144. {
  145. using var app = App();
  146. var (target, _, itemsControl) = CreateTarget();
  147. var items = (IList)itemsControl.ItemsSource!;
  148. Assert.Equal(10, target.GetRealizedElements().Count);
  149. var toRecycle = target.GetRealizedElements().ElementAt(2);
  150. items.RemoveAt(2);
  151. var indexes = GetRealizedIndexes(target, itemsControl);
  152. // Item removed from realized elements and subsequent row indexes updated.
  153. Assert.Equal(Enumerable.Range(0, 9), indexes);
  154. var elements = target.GetRealizedElements().ToList();
  155. Layout(target);
  156. indexes = GetRealizedIndexes(target, itemsControl);
  157. // After layout an element for the newly visible last row is created and indexes updated.
  158. Assert.Equal(Enumerable.Range(0, 10), indexes);
  159. // And the removed row should now have been recycled as the last row.
  160. elements.Add(toRecycle);
  161. Assert.Equal(elements, target.GetRealizedElements());
  162. }
  163. [Fact]
  164. public void Updates_Elements_On_Item_Replace()
  165. {
  166. using var app = App();
  167. var (target, _, itemsControl) = CreateTarget();
  168. var items = (ObservableCollection<string>)itemsControl.ItemsSource!;
  169. Assert.Equal(10, target.GetRealizedElements().Count);
  170. var toReplace = target.GetRealizedElements().ElementAt(2);
  171. items[2] = "new";
  172. // Container being replaced should have been recycled.
  173. Assert.DoesNotContain(toReplace, target.GetRealizedElements());
  174. Assert.False(toReplace!.IsVisible);
  175. var indexes = GetRealizedIndexes(target, itemsControl);
  176. // Item removed from realized elements at old position and space inserted at new position.
  177. Assert.Equal(new[] { 0, 1, -1, 3, 4, 5, 6, 7, 8, 9 }, indexes);
  178. Layout(target);
  179. indexes = GetRealizedIndexes(target, itemsControl);
  180. // After layout the missing container should have been created.
  181. Assert.Equal(Enumerable.Range(0, 10), indexes);
  182. }
  183. [Fact]
  184. public void Updates_Elements_On_Item_Move()
  185. {
  186. using var app = App();
  187. var (target, _, itemsControl) = CreateTarget();
  188. var items = (ObservableCollection<string>)itemsControl.ItemsSource!;
  189. Assert.Equal(10, target.GetRealizedElements().Count);
  190. var toMove = target.GetRealizedElements().ElementAt(2);
  191. items.Move(2, 6);
  192. // Container being moved should have been recycled.
  193. Assert.DoesNotContain(toMove, target.GetRealizedElements());
  194. Assert.False(toMove!.IsVisible);
  195. var indexes = GetRealizedIndexes(target, itemsControl);
  196. // Item removed from realized elements at old position and space inserted at new position.
  197. Assert.Equal(new[] { 0, 1, 2, 3, 4, 5, -1, 7, 8, 9 }, indexes);
  198. Layout(target);
  199. indexes = GetRealizedIndexes(target, itemsControl);
  200. // After layout the missing container should have been created.
  201. Assert.Equal(Enumerable.Range(0, 10), indexes);
  202. }
  203. [Fact]
  204. public void Removes_Control_Items_From_Panel_On_Item_Remove()
  205. {
  206. using var app = App();
  207. var items = new ObservableCollection<Button>(Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10 }));
  208. var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: null);
  209. Assert.Equal(1000, scroll.Extent.Height);
  210. var removed = items[1];
  211. items.RemoveAt(1);
  212. Assert.Null(removed.Parent);
  213. Assert.Null(removed.VisualParent);
  214. }
  215. [Fact]
  216. public void Does_Not_Recycle_Focused_Element()
  217. {
  218. using var app = App();
  219. var (target, scroll, itemsControl) = CreateTarget();
  220. var focused = target.GetRealizedElements().First()!;
  221. focused.Focusable = true;
  222. focused.Focus();
  223. Assert.True(target.GetRealizedElements().First()!.IsKeyboardFocusWithin);
  224. scroll.Offset = new Vector(0, 200);
  225. Layout(target);
  226. Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin));
  227. }
  228. [Fact]
  229. public void Removing_Item_Of_Focused_Element_Clears_Focus()
  230. {
  231. using var app = App();
  232. var (target, scroll, itemsControl) = CreateTarget();
  233. var focused = target.GetRealizedElements().First()!;
  234. focused.Focusable = true;
  235. focused.Focus();
  236. Assert.True(focused.IsKeyboardFocusWithin);
  237. scroll.Offset = new Vector(0, 200);
  238. Layout(target);
  239. Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin));
  240. Assert.All(target.GetRealizedElements(), x => Assert.NotSame(focused, x));
  241. }
  242. [Fact]
  243. public void Scrolling_Back_To_Focused_Element_Uses_Correct_Element()
  244. {
  245. using var app = App();
  246. var (target, scroll, itemsControl) = CreateTarget();
  247. var focused = target.GetRealizedElements().First()!;
  248. focused.Focusable = true;
  249. focused.Focus();
  250. Assert.True(focused.IsKeyboardFocusWithin);
  251. scroll.Offset = new Vector(0, 200);
  252. Layout(target);
  253. scroll.Offset = new Vector(0, 0);
  254. Layout(target);
  255. Assert.Same(focused, target.GetRealizedElements().First());
  256. }
  257. [Fact]
  258. public void Focusing_Another_Element_Recycles_Original_Focus_Element()
  259. {
  260. using var app = App();
  261. var (target, scroll, itemsControl) = CreateTarget();
  262. var originalFocused = target.GetRealizedElements().First()!;
  263. originalFocused.Focusable = true;
  264. originalFocused.Focus();
  265. scroll.Offset = new Vector(0, 500);
  266. Layout(target);
  267. var newFocused = target.GetRealizedElements().First()!;
  268. newFocused.Focusable = true;
  269. newFocused.Focus();
  270. Assert.False(originalFocused.IsVisible);
  271. }
  272. [Fact]
  273. public void Removing_Range_When_Scrolled_To_End_Updates_Viewport()
  274. {
  275. using var app = App();
  276. var items = new AvaloniaList<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  277. var (target, scroll, itemsControl) = CreateTarget(items: items);
  278. scroll.Offset = new Vector(0, 900);
  279. Layout(target);
  280. AssertRealizedItems(target, itemsControl, 90, 10);
  281. items.RemoveRange(0, 80);
  282. Layout(target);
  283. AssertRealizedItems(target, itemsControl, 10, 10);
  284. Assert.Equal(new Vector(0, 100), scroll.Offset);
  285. }
  286. [Fact]
  287. public void Removing_Range_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport()
  288. {
  289. using var app = App();
  290. var items = new AvaloniaList<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  291. var (target, scroll, itemsControl) = CreateTarget(items: items);
  292. scroll.Offset = new Vector(0, 900);
  293. Layout(target);
  294. AssertRealizedItems(target, itemsControl, 90, 10);
  295. items.RemoveRange(0, 95);
  296. Layout(target);
  297. AssertRealizedItems(target, itemsControl, 0, 5);
  298. Assert.Equal(new Vector(0, 0), scroll.Offset);
  299. }
  300. [Fact]
  301. public void Resetting_Collection_To_Have_Less_Items_When_Scrolled_To_End_Updates_Viewport()
  302. {
  303. using var app = App();
  304. var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  305. var (target, scroll, itemsControl) = CreateTarget(items: items);
  306. scroll.Offset = new Vector(0, 900);
  307. Layout(target);
  308. AssertRealizedItems(target, itemsControl, 90, 10);
  309. items.Reset(Enumerable.Range(0, 20).Select(x => $"Item {x}"));
  310. Layout(target);
  311. AssertRealizedItems(target, itemsControl, 10, 10);
  312. Assert.Equal(new Vector(0, 100), scroll.Offset);
  313. }
  314. [Fact]
  315. public void Resetting_Collection_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport()
  316. {
  317. using var app = App();
  318. var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  319. var (target, scroll, itemsControl) = CreateTarget(items: items);
  320. scroll.Offset = new Vector(0, 900);
  321. Layout(target);
  322. AssertRealizedItems(target, itemsControl, 90, 10);
  323. items.Reset(Enumerable.Range(0, 5).Select(x => $"Item {x}"));
  324. Layout(target);
  325. AssertRealizedItems(target, itemsControl, 0, 5);
  326. Assert.Equal(new Vector(0, 0), scroll.Offset);
  327. }
  328. [Fact]
  329. public void NthChild_Selector_Works()
  330. {
  331. using var app = App();
  332. var style = new Style(x => x.OfType<ContentPresenter>().NthChild(5, 0))
  333. {
  334. Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
  335. };
  336. var (target, _, _) = CreateTarget(styles: new[] { style });
  337. var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
  338. Assert.Equal(10, realized.Count);
  339. for (var i = 0; i < 10; ++i)
  340. {
  341. var container = realized[i];
  342. var index = target.IndexFromContainer(container);
  343. var expectedBackground = (i == 4 || i == 9) ? Brushes.Red : null;
  344. Assert.Equal(i, index);
  345. Assert.Equal(expectedBackground, container.Background);
  346. }
  347. }
  348. [Fact]
  349. public void NthLastChild_Selector_Works()
  350. {
  351. using var app = App();
  352. var style = new Style(x => x.OfType<ContentPresenter>().NthLastChild(5, 0))
  353. {
  354. Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
  355. };
  356. var (target, _, _) = CreateTarget(styles: new[] { style });
  357. var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
  358. Assert.Equal(10, realized.Count);
  359. for (var i = 0; i < 10; ++i)
  360. {
  361. var container = realized[i];
  362. var index = target.IndexFromContainer(container);
  363. var expectedBackground = (i == 0 || i == 5) ? Brushes.Red : null;
  364. Assert.Equal(i, index);
  365. Assert.Equal(expectedBackground, container.Background);
  366. }
  367. }
  368. [Fact]
  369. public void ContainerPrepared_Is_Raised_When_Scrolling()
  370. {
  371. using var app = App();
  372. var (target, scroll, itemsControl) = CreateTarget();
  373. var raised = 0;
  374. itemsControl.ContainerPrepared += (s, e) => ++raised;
  375. scroll.Offset = new Vector(0, 200);
  376. Layout(target);
  377. Assert.Equal(10, raised);
  378. }
  379. [Fact]
  380. public void ContainerClearing_Is_Raised_When_Scrolling()
  381. {
  382. using var app = App();
  383. var (target, scroll, itemsControl) = CreateTarget();
  384. var raised = 0;
  385. itemsControl.ContainerClearing += (s, e) => ++raised;
  386. scroll.Offset = new Vector(0, 200);
  387. Layout(target);
  388. Assert.Equal(10, raised);
  389. }
  390. [Fact]
  391. public void Scrolling_Down_With_Larger_Element_Does_Not_Cause_Jump_And_Arrives_At_End()
  392. {
  393. using var app = App();
  394. var items = Enumerable.Range(0, 1000).Select(x => new ItemWithHeight(x)).ToList();
  395. items[20].Height = 200;
  396. var itemTemplate = new FuncDataTemplate<ItemWithHeight>((x, _) =>
  397. new Canvas
  398. {
  399. Width = 100,
  400. [!Canvas.HeightProperty] = new Binding("Height"),
  401. });
  402. var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: itemTemplate);
  403. var index = target.FirstRealizedIndex;
  404. // Scroll down to the larger element.
  405. while (target.LastRealizedIndex < items.Count - 1)
  406. {
  407. scroll.LineDown();
  408. Layout(target);
  409. Assert.True(
  410. target.FirstRealizedIndex >= index,
  411. $"{target.FirstRealizedIndex} is not greater or equal to {index}");
  412. if (scroll.Offset.Y + scroll.Viewport.Height == scroll.Extent.Height)
  413. Assert.Equal(items.Count - 1, target.LastRealizedIndex);
  414. index = target.FirstRealizedIndex;
  415. }
  416. }
  417. [Fact]
  418. public void Scrolling_Up_To_Larger_Element_Does_Not_Cause_Jump()
  419. {
  420. using var app = App();
  421. var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x)).ToList();
  422. items[20].Height = 200;
  423. var itemTemplate = new FuncDataTemplate<ItemWithHeight>((x, _) =>
  424. new Canvas
  425. {
  426. Width = 100,
  427. [!Canvas.HeightProperty] = new Binding("Height"),
  428. });
  429. var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: itemTemplate);
  430. // Scroll past the larger element.
  431. scroll.Offset = new Vector(0, 600);
  432. Layout(target);
  433. // Precondition checks
  434. Assert.True(target.FirstRealizedIndex > 20);
  435. var index = target.FirstRealizedIndex;
  436. // Scroll up to the top.
  437. while (scroll.Offset.Y > 0)
  438. {
  439. scroll.LineUp();
  440. Layout(target);
  441. Assert.True(target.FirstRealizedIndex <= index, $"{target.FirstRealizedIndex} is not less than {index}");
  442. index = target.FirstRealizedIndex;
  443. }
  444. }
  445. [Fact]
  446. public void Scrolling_Up_To_Smaller_Element_Does_Not_Cause_Jump()
  447. {
  448. using var app = App();
  449. var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x, 30)).ToList();
  450. items[20].Height = 25;
  451. var itemTemplate = new FuncDataTemplate<ItemWithHeight>((x, _) =>
  452. new Canvas
  453. {
  454. Width = 100,
  455. [!Canvas.HeightProperty] = new Binding("Height"),
  456. });
  457. var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: itemTemplate);
  458. // Scroll past the larger element.
  459. scroll.Offset = new Vector(0, 25 * items[0].Height);
  460. Layout(target);
  461. // Precondition checks
  462. Assert.True(target.FirstRealizedIndex > 20);
  463. var index = target.FirstRealizedIndex;
  464. // Scroll up to the top.
  465. while (scroll.Offset.Y > 0)
  466. {
  467. scroll.Offset = scroll.Offset - new Vector(0, 5);
  468. Layout(target);
  469. Assert.True(
  470. target.FirstRealizedIndex <= index,
  471. $"{target.FirstRealizedIndex} is not less than {index}");
  472. Assert.True(
  473. index - target.FirstRealizedIndex <= 1,
  474. $"FirstIndex changed from {index} to {target.FirstRealizedIndex}");
  475. index = target.FirstRealizedIndex;
  476. }
  477. }
  478. [Fact]
  479. public void Does_Not_Throw_When_Estimating_Viewport_With_Ancestor_Margin()
  480. {
  481. // Issue #11272
  482. using var app = App();
  483. var (_, _, itemsControl) = CreateUnrootedTarget<ItemsControl>();
  484. var container = new Decorator { Margin = new Thickness(100) };
  485. var root = new TestRoot(true, container);
  486. root.LayoutManager.ExecuteInitialLayoutPass();
  487. container.Child = itemsControl;
  488. root.LayoutManager.ExecuteLayoutPass();
  489. }
  490. [Fact]
  491. public void Supports_Null_Recycle_Key_When_Scrolling()
  492. {
  493. using var app = App();
  494. var (_, scroll, itemsControl) = CreateUnrootedTarget<NonRecyclingItemsControl>();
  495. var root = CreateRoot(itemsControl);
  496. root.LayoutManager.ExecuteInitialLayoutPass();
  497. var firstItem = itemsControl.ContainerFromIndex(0)!;
  498. scroll.Offset = new(0, 20);
  499. Layout(itemsControl);
  500. Assert.Null(firstItem.Parent);
  501. Assert.Null(firstItem.VisualParent);
  502. Assert.DoesNotContain(firstItem, itemsControl.ItemsPanelRoot!.Children);
  503. }
  504. [Fact]
  505. public void Supports_Null_Recycle_Key_When_Clearing_Items()
  506. {
  507. using var app = App();
  508. var (_, _, itemsControl) = CreateUnrootedTarget<NonRecyclingItemsControl>();
  509. var root = CreateRoot(itemsControl);
  510. root.LayoutManager.ExecuteInitialLayoutPass();
  511. var firstItem = itemsControl.ContainerFromIndex(0)!;
  512. itemsControl.ItemsSource = null;
  513. Layout(itemsControl);
  514. Assert.Null(firstItem.Parent);
  515. Assert.Null(firstItem.VisualParent);
  516. Assert.Empty(itemsControl.ItemsPanelRoot!.Children);
  517. }
  518. [Fact]
  519. public void ScrollIntoView_On_Effectively_Invisible_Panel_Does_Not_Create_Ghost_Elements()
  520. {
  521. var items = new[] { "foo", "bar", "baz" };
  522. var (target, _, itemsControl) = CreateUnrootedTarget<ItemsControl>(items: items);
  523. var container = new Decorator { Margin = new Thickness(100), Child = itemsControl };
  524. var root = new TestRoot(true, container);
  525. root.LayoutManager.ExecuteInitialLayoutPass();
  526. // Clear the items and do a layout to recycle all elements.
  527. itemsControl.ItemsSource = null;
  528. root.LayoutManager.ExecuteLayoutPass();
  529. // Should have no realized elements and 3 unrealized elements.
  530. Assert.Equal(0, target.GetRealizedElements().Count);
  531. Assert.Equal(3, target.Children.Count);
  532. // Make the panel effectively invisible and set items.
  533. container.IsVisible = false;
  534. itemsControl.ItemsSource = items;
  535. // Try to scroll into view while effectively invisible.
  536. target.ScrollIntoView(0);
  537. // Make the panel visible and layout.
  538. container.IsVisible = true;
  539. root.LayoutManager.ExecuteLayoutPass();
  540. // Should have 3 realized elements and no unrealized elements.
  541. Assert.Equal(3, target.GetRealizedElements().Count);
  542. Assert.Equal(3, target.Children.Count);
  543. }
  544. private static IReadOnlyList<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
  545. {
  546. return target.GetRealizedElements()
  547. .Select(x => x is null ? -1 : itemsControl.IndexFromContainer((Control)x))
  548. .ToList();
  549. }
  550. private static void AssertRealizedItems(
  551. VirtualizingStackPanel target,
  552. ItemsControl itemsControl,
  553. int firstIndex,
  554. int count)
  555. {
  556. Assert.All(target.GetRealizedContainers()!, x => Assert.Same(target, x.VisualParent));
  557. Assert.All(target.GetRealizedContainers()!, x => Assert.Same(itemsControl, x.Parent));
  558. var childIndexes = target.GetRealizedContainers()!
  559. .Select(x => itemsControl.IndexFromContainer(x))
  560. .Where(x => x >= 0)
  561. .OrderBy(x => x)
  562. .ToList();
  563. Assert.Equal(Enumerable.Range(firstIndex, count), childIndexes);
  564. }
  565. private static void AssertRealizedControlItems<TContainer>(
  566. VirtualizingStackPanel target,
  567. ItemsControl itemsControl,
  568. int firstIndex,
  569. int count)
  570. {
  571. Assert.All(target.GetRealizedContainers()!, x => Assert.IsType<TContainer>(x));
  572. Assert.All(target.GetRealizedContainers()!, x => Assert.Same(target, x.VisualParent));
  573. Assert.All(target.GetRealizedContainers()!, x => Assert.Same(itemsControl, x.Parent));
  574. var childIndexes = target.GetRealizedContainers()!
  575. .Select(x => itemsControl.IndexFromContainer(x))
  576. .Where(x => x >= 0)
  577. .OrderBy(x => x)
  578. .ToList();
  579. Assert.Equal(Enumerable.Range(firstIndex, count), childIndexes);
  580. }
  581. private static (VirtualizingStackPanel, ScrollViewer, ItemsControl) CreateTarget(
  582. IEnumerable<object>? items = null,
  583. Optional<IDataTemplate?> itemTemplate = default,
  584. IEnumerable<Style>? styles = null)
  585. {
  586. var (target, scroll, itemsControl) = CreateUnrootedTarget<ItemsControl>(items, itemTemplate);
  587. var root = CreateRoot(itemsControl, styles);
  588. root.LayoutManager.ExecuteInitialLayoutPass();
  589. return (target, scroll, itemsControl);
  590. }
  591. private static (VirtualizingStackPanel, ScrollViewer, T) CreateUnrootedTarget<T>(
  592. IEnumerable<object>? items = null,
  593. Optional<IDataTemplate?> itemTemplate = default)
  594. where T : ItemsControl, new()
  595. {
  596. var target = new VirtualizingStackPanel();
  597. items ??= new ObservableCollection<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  598. var presenter = new ItemsPresenter
  599. {
  600. [~ItemsPresenter.ItemsPanelProperty] = new TemplateBinding(ItemsPresenter.ItemsPanelProperty),
  601. };
  602. var scroll = new ScrollViewer
  603. {
  604. Name = "PART_ScrollViewer",
  605. Content = presenter,
  606. Template = ScrollViewerTemplate(),
  607. };
  608. var itemsControl = new T
  609. {
  610. ItemsSource = items,
  611. Template = new FuncControlTemplate<ItemsControl>((_, ns) => scroll.RegisterInNameScope(ns)),
  612. ItemsPanel = new FuncTemplate<Panel?>(() => target),
  613. ItemTemplate = itemTemplate.GetValueOrDefault(DefaultItemTemplate()),
  614. };
  615. return (target, scroll, itemsControl);
  616. }
  617. private static TestRoot CreateRoot(Control? child, IEnumerable<Style>? styles = null)
  618. {
  619. var root = new TestRoot(true, child);
  620. root.ClientSize = new(100, 100);
  621. if (styles is not null)
  622. root.Styles.AddRange(styles);
  623. return root;
  624. }
  625. private static IDataTemplate DefaultItemTemplate()
  626. {
  627. return new FuncDataTemplate<object>((x, _) => new Canvas { Width = 100, Height = 10 });
  628. }
  629. private static void Layout(Control target)
  630. {
  631. var root = (ILayoutRoot?)target.GetVisualRoot();
  632. root?.LayoutManager.ExecuteLayoutPass();
  633. }
  634. private static IControlTemplate ScrollViewerTemplate()
  635. {
  636. return new FuncControlTemplate<ScrollViewer>((x, ns) =>
  637. new ScrollContentPresenter
  638. {
  639. Name = "PART_ContentPresenter",
  640. }.RegisterInNameScope(ns));
  641. }
  642. private static IDisposable App() => UnitTestApplication.Start(TestServices.RealFocus);
  643. private class ItemWithHeight
  644. {
  645. public ItemWithHeight(int index, double height = 10)
  646. {
  647. Caption = $"Item {index}";
  648. Height = height;
  649. }
  650. public string Caption { get; set; }
  651. public double Height { get; set; }
  652. }
  653. private class ResettingCollection : List<string>, INotifyCollectionChanged
  654. {
  655. public ResettingCollection(IEnumerable<string> items)
  656. {
  657. AddRange(items);
  658. }
  659. public void Reset(IEnumerable<string> items)
  660. {
  661. Clear();
  662. AddRange(items);
  663. CollectionChanged?.Invoke(
  664. this,
  665. new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
  666. }
  667. public event NotifyCollectionChangedEventHandler? CollectionChanged;
  668. }
  669. private class NonRecyclingItemsControl : ItemsControl
  670. {
  671. protected override Type StyleKeyOverride => typeof(ItemsControl);
  672. protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
  673. {
  674. recycleKey = null;
  675. return true;
  676. }
  677. }
  678. }
  679. }