VirtualizingStackPanelTests.cs 101 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549
  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.Diagnostics;
  7. using System.Linq;
  8. using Avalonia.Collections;
  9. using Avalonia.Controls.Presenters;
  10. using Avalonia.Controls.Primitives;
  11. using Avalonia.Controls.Templates;
  12. using Avalonia.Data;
  13. using Avalonia.Input;
  14. using Avalonia.Layout;
  15. using Avalonia.Media;
  16. using Avalonia.Styling;
  17. using Avalonia.UnitTests;
  18. using Avalonia.VisualTree;
  19. using Xunit;
  20. #nullable enable
  21. namespace Avalonia.Controls.UnitTests
  22. {
  23. public class VirtualizingStackPanelTests : ScopedTestBase
  24. {
  25. private static FuncDataTemplate<ItemWithHeight> CanvasWithHeightTemplate = new((_, _) =>
  26. new CanvasCountingMeasureArrangeCalls
  27. {
  28. Width = 100,
  29. [!Layoutable.HeightProperty] = new Binding("Height"),
  30. });
  31. private static FuncDataTemplate<ItemWithWidth> CanvasWithWidthTemplate = new((_, _) =>
  32. new CanvasCountingMeasureArrangeCalls
  33. {
  34. Height = 100,
  35. [!Layoutable.WidthProperty] = new Binding("Width"),
  36. });
  37. [Theory]
  38. [InlineData(0d , 10)]
  39. [InlineData(0.5d, 20)]
  40. public void Creates_Initial_Items(double bufferFactor, int expectedCount)
  41. {
  42. using var app = App();
  43. var (target, scroll, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
  44. Assert.Equal(1000, scroll.Extent.Height);
  45. AssertRealizedItems(target, itemsControl, 0, expectedCount);
  46. }
  47. [Theory]
  48. [InlineData(0d, 10)]
  49. [InlineData(0.5d, 20)] // Buffer factor of 0.5. Since at start there is no room, the 10 additional items are just appended
  50. public void Initializes_Initial_Control_Items(double bufferFactor, int expectedCount)
  51. {
  52. using var app = App();
  53. var items = Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10 });
  54. var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: null, bufferFactor:bufferFactor);
  55. Assert.Equal(1000, scroll.Extent.Height);
  56. AssertRealizedControlItems<Button>(target, itemsControl, 0, expectedCount);
  57. }
  58. [Theory]
  59. [InlineData(0d, 2)]
  60. [InlineData(0.5d, 2)]
  61. public void Creates_Reassigned_Items(double bufferFactor, int expectedCount)
  62. {
  63. using var app = App();
  64. var (target, scroll, itemsControl) = CreateTarget(items: Array.Empty<object>(), bufferFactor: bufferFactor);
  65. Assert.Empty(itemsControl.GetRealizedContainers());
  66. itemsControl.ItemsSource = new[] { "foo", "bar" };
  67. Layout(target);
  68. AssertRealizedItems(target, itemsControl, 0, expectedCount);
  69. }
  70. [Theory]
  71. [InlineData(0d, 1, 10)]
  72. [InlineData(0.5d, 0, 20)]
  73. public void Scrolls_Down_One_Item(double bufferFactor, int expectedFirstIndex, int expectedCount)
  74. {
  75. using var app = App();
  76. var (target, scroll, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
  77. scroll.Offset = new Vector(0, 10);
  78. Layout(target);
  79. AssertRealizedItems(target, itemsControl, expectedFirstIndex, expectedCount);
  80. }
  81. [Theory]
  82. [InlineData(0d, 20,10)]
  83. [InlineData(0.5d, 15,20)]
  84. public void Scrolls_Down_More_Than_A_Page(double bufferFactor, int expectedFirstIndex, int expectedCount)
  85. {
  86. using var app = App();
  87. var (target, scroll, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
  88. scroll.Offset = new Vector(0, 200);
  89. Layout(target);
  90. AssertRealizedItems(target, itemsControl, expectedFirstIndex, expectedCount);
  91. }
  92. [Theory]
  93. [InlineData(0d, 11, 10)]
  94. [InlineData(0.5d, 6, 20)]
  95. public void Scrolls_Down_To_Index(double bufferFactor, int expectedFirstIndex, int expectedCount)
  96. {
  97. using var app = App();
  98. var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  99. target.ScrollIntoView(20);
  100. AssertRealizedItems(target, itemsControl, expectedFirstIndex, expectedCount);
  101. }
  102. [Theory]
  103. [InlineData(0d, 90, 20, 10)]
  104. [InlineData(0.5d, 80, 15, 20)]
  105. public void Scrolls_Up_To_Index(double bufferFactor, int firstRealizedIndex, int expectedFirstIndex, int expectedCount)
  106. {
  107. using var app = App();
  108. var (target, scroll, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
  109. scroll.ScrollToEnd();
  110. Layout(target);
  111. Assert.Equal(firstRealizedIndex, target.FirstRealizedIndex);
  112. target.ScrollIntoView(20);
  113. AssertRealizedItems(target, itemsControl, expectedFirstIndex, expectedCount);
  114. }
  115. [Theory]
  116. [InlineData(0d, 11)]
  117. [InlineData(0.5d, 21)]
  118. public void Scrolling_Up_To_Index_Does_Not_Create_A_Page_Of_Unrealized_Elements(double bufferFactor, int expectedCount)
  119. {
  120. using var app = App();
  121. var (target, scroll, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
  122. scroll.ScrollToEnd();
  123. Layout(target);
  124. target.ScrollIntoView(20);
  125. Assert.Equal(expectedCount, target.Children.Count);
  126. }
  127. [Theory]
  128. [InlineData(0d,
  129. 10,
  130. 11,
  131. "-1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10",
  132. 10)]
  133. [InlineData(0.5d,
  134. 20,
  135. 21,
  136. "-1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20",
  137. 20)]
  138. public void Creates_Elements_On_Item_Insert_1(double bufferFactor,
  139. int firstCount,
  140. int secondCount,
  141. string indexesRaw,
  142. int thirdCount)
  143. {
  144. using var app = App();
  145. var (target, _, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
  146. var items = (IList)itemsControl.ItemsSource!;
  147. Assert.Equal(firstCount, target.GetRealizedElements().Count);
  148. items.Insert(0, "new");
  149. Assert.Equal(secondCount, target.GetRealizedElements().Count);
  150. var indexes = GetRealizedIndexes(target, itemsControl);
  151. // Blank space inserted in realized elements and subsequent indexes updated.
  152. Assert.Equal(indexesRaw.Split(", ").Select(Int32.Parse).ToArray(), indexes);
  153. var elements = target.GetRealizedElements().ToList();
  154. Layout(target);
  155. indexes = GetRealizedIndexes(target, itemsControl);
  156. // After layout an element for the new element is created.
  157. Assert.Equal(Enumerable.Range(0, thirdCount), indexes);
  158. // But apart from the new element and the removed last element, all existing elements
  159. // should be the same.
  160. elements[0] = target.GetRealizedElements().ElementAt(0);
  161. elements.RemoveAt(elements.Count - 1);
  162. Assert.Equal(elements, target.GetRealizedElements());
  163. }
  164. [Theory]
  165. [InlineData(0d,
  166. 10,
  167. 11,
  168. "0, 1, -1, 3, 4, 5, 6, 7, 8, 9, 10",
  169. 10)]
  170. [InlineData(0.5d,
  171. 20,
  172. 21,
  173. "0, 1, -1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20",
  174. 20)]
  175. public void Creates_Elements_On_Item_Insert_2(double bufferFactor,
  176. int firstCount,
  177. int secondCount,
  178. string indexesRaw,
  179. int thirdCount)
  180. {
  181. using var app = App();
  182. var (target, _, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
  183. var items = (IList)itemsControl.ItemsSource!;
  184. Assert.Equal(firstCount, target.GetRealizedElements().Count);
  185. items.Insert(2, "new");
  186. Assert.Equal(secondCount, target.GetRealizedElements().Count);
  187. var indexes = GetRealizedIndexes(target, itemsControl);
  188. // Blank space inserted in realized elements and subsequent indexes updated.
  189. Assert.Equal(indexesRaw.Split(", ").Select(Int32.Parse).ToArray(), indexes);
  190. var elements = target.GetRealizedElements().ToList();
  191. Layout(target);
  192. indexes = GetRealizedIndexes(target, itemsControl);
  193. // After layout an element for the new element is created.
  194. Assert.Equal(Enumerable.Range(0, thirdCount), indexes);
  195. // But apart from the new element and the removed last element, all existing elements
  196. // should be the same.
  197. elements[2] = target.GetRealizedElements().ElementAt(2);
  198. elements.RemoveAt(elements.Count - 1);
  199. Assert.Equal(elements, target.GetRealizedElements());
  200. }
  201. [Theory]
  202. [InlineData(0d)]
  203. [InlineData(0.5d)]
  204. public void Updates_Elements_On_Item_Moved(double bufferFactor)
  205. {
  206. // Arrange
  207. using var app = App();
  208. var actualItems = new AvaloniaList<string>(Enumerable
  209. .Range(0, 100)
  210. .Select(x => $"Item {x}"));
  211. var (target, _, itemsControl) = CreateTarget(items: actualItems, bufferFactor:bufferFactor);
  212. var expectedRealizedElementContents = new[] { 1, 2, 0, 3, 4, 5, 6, 7, 8, 9 }
  213. .Select(x => $"Item {x}");
  214. // Act
  215. actualItems.Move(0, 2);
  216. Layout(target);
  217. // Assert
  218. var actualRealizedElementContents = target
  219. .GetRealizedElements()
  220. .Cast<ContentPresenter>()
  221. .Select(x => x.Content);
  222. Assert.Equivalent(expectedRealizedElementContents, actualRealizedElementContents);
  223. }
  224. [Theory]
  225. [InlineData(0d)]
  226. [InlineData(0.5d)]
  227. public void Updates_Elements_On_Item_Range_Moved(double bufferFactor)
  228. {
  229. // Arrange
  230. using var app = App();
  231. var actualItems = new AvaloniaList<string>(Enumerable
  232. .Range(0, 100)
  233. .Select(x => $"Item {x}"));
  234. var (target, _, itemsControl) = CreateTarget(items: actualItems, bufferFactor: bufferFactor);
  235. var expectedRealizedElementContents = new[] { 2, 0, 1, 3, 4, 5, 6, 7, 8, 9 }
  236. .Select(x => $"Item {x}");
  237. // Act
  238. actualItems.MoveRange(0, 2, 3);
  239. Layout(target);
  240. // Assert
  241. var actualRealizedElementContents = target
  242. .GetRealizedElements()
  243. .Cast<ContentPresenter>()
  244. .Select(x => x.Content);
  245. Assert.Equivalent(expectedRealizedElementContents, actualRealizedElementContents);
  246. }
  247. [Theory]
  248. [InlineData(0d, 10, 9)]
  249. [InlineData(0.5d, 20, 19)]
  250. public void Updates_Elements_On_Item_Remove(double bufferFactor, int firstCount, int secondCount)
  251. {
  252. using var app = App();
  253. var (target, _, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  254. var items = (IList)itemsControl.ItemsSource!;
  255. Assert.Equal(firstCount, target.GetRealizedElements().Count);
  256. var toRecycle = target.GetRealizedElements().ElementAt(2);
  257. items.RemoveAt(2);
  258. var indexes = GetRealizedIndexes(target, itemsControl);
  259. // Item removed from realized elements and subsequent row indexes updated.
  260. Assert.Equal(Enumerable.Range(0, secondCount), indexes);
  261. var elements = target.GetRealizedElements().ToList();
  262. Layout(target);
  263. indexes = GetRealizedIndexes(target, itemsControl);
  264. // After layout an element for the newly visible last row is created and indexes updated.
  265. Assert.Equal(Enumerable.Range(0, firstCount), indexes);
  266. // And the removed row should now have been recycled as the last row.
  267. elements.Add(toRecycle);
  268. Assert.Equal(elements, target.GetRealizedElements());
  269. }
  270. [Theory]
  271. [InlineData(0d, 10, "0, 1, -1, 3, 4, 5, 6, 7, 8, 9")]
  272. [InlineData(0.5d, 20, "0, 1, -1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19")]
  273. public void Updates_Elements_On_Item_Replace(double bufferFactor, int firstCount, string indexesRaw)
  274. {
  275. using var app = App();
  276. var (target, _, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  277. var items = (ObservableCollection<string>)itemsControl.ItemsSource!;
  278. Assert.Equal(firstCount, target.GetRealizedElements().Count);
  279. var toReplace = target.GetRealizedElements().ElementAt(2);
  280. items[2] = "new";
  281. // Container being replaced should have been recycled.
  282. Assert.DoesNotContain(toReplace, target.GetRealizedElements());
  283. Assert.False(toReplace!.IsVisible);
  284. var indexes = GetRealizedIndexes(target, itemsControl);
  285. // Item removed from realized elements at old position and space inserted at new position.
  286. Assert.Equal(indexesRaw.Split(", ").Select(Int32.Parse).ToArray(), indexes);
  287. Layout(target);
  288. indexes = GetRealizedIndexes(target, itemsControl);
  289. // After layout the missing container should have been created.
  290. Assert.Equal(Enumerable.Range(0, firstCount), indexes);
  291. }
  292. [Theory]
  293. [InlineData(0d, 10, "0, 1, 2, 3, 4, 5, -1, 7, 8, 9")]
  294. [InlineData(0.5d, 20, "0, 1, 2, 3, 4, 5, -1, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19")]
  295. public void Updates_Elements_On_Item_Move(double bufferFactor, int firstCount, string indexesRaw)
  296. {
  297. using var app = App();
  298. var (target, _, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
  299. var items = (ObservableCollection<string>)itemsControl.ItemsSource!;
  300. Assert.Equal(firstCount, target.GetRealizedElements().Count);
  301. var toMove = target.GetRealizedElements().ElementAt(2);
  302. items.Move(2, 6);
  303. // Container being moved should have been recycled.
  304. Assert.DoesNotContain(toMove, target.GetRealizedElements());
  305. Assert.False(toMove!.IsVisible);
  306. var indexes = GetRealizedIndexes(target, itemsControl);
  307. // Item removed from realized elements at old position and space inserted at new position.
  308. Assert.Equal(indexesRaw.Split(", ").Select(Int32.Parse).ToArray(), indexes);
  309. Layout(target);
  310. indexes = GetRealizedIndexes(target, itemsControl);
  311. // After layout the missing container should have been created.
  312. Assert.Equal(Enumerable.Range(0, firstCount), indexes);
  313. }
  314. [Theory]
  315. [InlineData(0d)]
  316. [InlineData(0.5d)]
  317. public void Removes_Control_Items_From_Panel_On_Item_Remove(double bufferFactor)
  318. {
  319. using var app = App();
  320. var items = new ObservableCollection<Button>(Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10 }));
  321. var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: null, bufferFactor:bufferFactor);
  322. Assert.Equal(1000, scroll.Extent.Height);
  323. var removed = items[1];
  324. items.RemoveAt(1);
  325. Assert.Null(removed.Parent);
  326. Assert.Null(removed.VisualParent);
  327. }
  328. [Theory]
  329. [InlineData(0d)]
  330. [InlineData(0.5d)]
  331. public void Does_Not_Recycle_Focused_Element(double bufferFactor)
  332. {
  333. using var app = App();
  334. var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  335. var focused = target.GetRealizedElements().First()!;
  336. focused.Focusable = true;
  337. focused.Focus();
  338. Assert.True(target.GetRealizedElements().First()!.IsKeyboardFocusWithin);
  339. scroll.Offset = new Vector(0, 200);
  340. Layout(target);
  341. Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin));
  342. }
  343. [Theory]
  344. [InlineData(0d)]
  345. [InlineData(0.5d)]
  346. public void Removing_Item_Of_Focused_Element_Clears_Focus(double bufferFactor)
  347. {
  348. using var app = App();
  349. var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  350. var items = (IList)itemsControl.ItemsSource!;
  351. var focused = target.GetRealizedElements().First()!;
  352. focused.Focusable = true;
  353. focused.Focus();
  354. Assert.True(focused.IsKeyboardFocusWithin);
  355. Assert.Equal(focused, KeyboardNavigation.GetTabOnceActiveElement(itemsControl));
  356. scroll.Offset = new Vector(0, 200);
  357. Layout(target);
  358. items.RemoveAt(0);
  359. Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin));
  360. Assert.All(target.GetRealizedElements(), x => Assert.NotSame(focused, x));
  361. }
  362. [Theory]
  363. [InlineData(0d)]
  364. [InlineData(0.5d)]
  365. public void Scrolling_Back_To_Focused_Element_Uses_Correct_Element(double bufferFactor)
  366. {
  367. using var app = App();
  368. var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  369. var focused = target.GetRealizedElements().First()!;
  370. focused.Focusable = true;
  371. focused.Focus();
  372. Assert.True(focused.IsKeyboardFocusWithin);
  373. scroll.Offset = new Vector(0, 200);
  374. Layout(target);
  375. scroll.Offset = new Vector(0, 0);
  376. Layout(target);
  377. Assert.Same(focused, target.GetRealizedElements().First());
  378. }
  379. [Theory]
  380. [InlineData(0d)]
  381. [InlineData(0.5d)]
  382. public void Focusing_Another_Element_Recycles_Original_Focus_Element(double bufferFactor)
  383. {
  384. using var app = App();
  385. var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  386. var originalFocused = target.GetRealizedElements().First()!;
  387. originalFocused.Focusable = true;
  388. originalFocused.Focus();
  389. scroll.Offset = new Vector(0, 500);
  390. Layout(target);
  391. var newFocused = target.GetRealizedElements().First()!;
  392. newFocused.Focusable = true;
  393. newFocused.Focus();
  394. Assert.False(originalFocused.IsVisible);
  395. }
  396. [Theory]
  397. [InlineData(0d)]
  398. [InlineData(0.5d)]
  399. public void Focused_Element_Losing_Focus_Does_Not_Reset_Selection(double bufferFactor)
  400. {
  401. using var app = App();
  402. var (target, scroll, listBox) = CreateTarget<ListBox, VirtualizingStackPanel>(
  403. styles: new[]
  404. {
  405. new Style(x => x.OfType<ListBoxItem>())
  406. {
  407. Setters =
  408. {
  409. new Setter(ListBoxItem.TemplateProperty, ListBoxItemTemplate()),
  410. }
  411. }
  412. }, bufferFactor: bufferFactor);
  413. listBox.SelectedIndex = 0;
  414. var selectedContainer = target.GetRealizedElements().First()!;
  415. selectedContainer.Focusable = true;
  416. selectedContainer.Focus();
  417. scroll.Offset = new Vector(0, 500);
  418. Layout(target);
  419. var newFocused = target.GetRealizedElements().First()!;
  420. newFocused.Focusable = true;
  421. newFocused.Focus();
  422. Assert.Equal(0, listBox.SelectedIndex);
  423. }
  424. [Theory]
  425. [InlineData(0d, 90, 10, 10)]
  426. [InlineData(0.5d, 80, 0, 20)]
  427. public void Removing_Range_When_Scrolled_To_End_Updates_Viewport(double bufferFactor, int firstIndex, int secondIndex, int count)
  428. {
  429. using var app = App();
  430. var items = new AvaloniaList<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  431. var (target, scroll, itemsControl) = CreateTarget(items: items, bufferFactor: bufferFactor);
  432. scroll.Offset = new Vector(0, 900);
  433. Layout(target);
  434. AssertRealizedItems(target, itemsControl, firstIndex, count);
  435. items.RemoveRange(0, 80);
  436. Layout(target);
  437. AssertRealizedItems(target, itemsControl, secondIndex, count);
  438. Assert.Equal(new Vector(0, 100), scroll.Offset);
  439. }
  440. [Theory]
  441. [InlineData(0d, 90, 10)]
  442. [InlineData(0.5d, 80, 20)]
  443. public void Removing_Range_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport(double bufferFactor, int firstIndex, int count)
  444. {
  445. using var app = App();
  446. var items = new AvaloniaList<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  447. var (target, scroll, itemsControl) = CreateTarget(items: items, bufferFactor: bufferFactor);
  448. scroll.Offset = new Vector(0, 900);
  449. Layout(target);
  450. AssertRealizedItems(target, itemsControl, firstIndex, count);
  451. items.RemoveRange(0, 95);
  452. Layout(target);
  453. AssertRealizedItems(target, itemsControl, 0, 5);
  454. Assert.Equal(new Vector(0, 0), scroll.Offset);
  455. }
  456. [Theory]
  457. [InlineData(0d, 90, 10, 10)]
  458. [InlineData(0.5d, 80,0, 20)]
  459. public void Resetting_Collection_To_Have_Less_Items_When_Scrolled_To_End_Updates_Viewport(double bufferFactor, int firstIndex, int secondIndex, int count)
  460. {
  461. using var app = App();
  462. var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  463. var (target, scroll, itemsControl) = CreateTarget(items: items, bufferFactor: bufferFactor);
  464. scroll.Offset = new Vector(0, 900);
  465. Layout(target);
  466. AssertRealizedItems(target, itemsControl, firstIndex, count);
  467. items.Reset(Enumerable.Range(0, 20).Select(x => $"Item {x}"));
  468. Layout(target);
  469. AssertRealizedItems(target, itemsControl, secondIndex, count);
  470. Assert.Equal(new Vector(0, 100), scroll.Offset);
  471. }
  472. [Theory]
  473. [InlineData(0d, 90, 10)]
  474. [InlineData(0.5d, 80, 20)]
  475. public void Resetting_Collection_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport(double bufferFactor, int firstIndex, int count)
  476. {
  477. using var app = App();
  478. var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  479. var (target, scroll, itemsControl) = CreateTarget(items: items, bufferFactor: bufferFactor);
  480. scroll.Offset = new Vector(0, 900);
  481. Layout(target);
  482. AssertRealizedItems(target, itemsControl, firstIndex, count);
  483. items.Reset(Enumerable.Range(0, 5).Select(x => $"Item {x}"));
  484. Layout(target);
  485. AssertRealizedItems(target, itemsControl, 0, 5);
  486. Assert.Equal(new Vector(0, 0), scroll.Offset);
  487. }
  488. [Theory]
  489. [InlineData(0d, 10, "4,9")]
  490. [InlineData(0.5d, 20, "4,9,14,19")]
  491. public void NthChild_Selector_Works(double bufferFactor, int count, string indexesRaw)
  492. {
  493. using var app = App();
  494. var style = new Style(x => x.OfType<ContentPresenter>().NthChild(5, 0))
  495. {
  496. Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
  497. };
  498. var (target, _, _) = CreateTarget(styles: new[] { style }, bufferFactor: bufferFactor);
  499. var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
  500. Assert.Equal(count, realized.Count);
  501. for (var i = 0; i < count; ++i)
  502. {
  503. var container = realized[i];
  504. var index = target.IndexFromContainer(container);
  505. var redIndexes = indexesRaw.Split(",").Select(Int32.Parse).ToArray();
  506. var expectedBackground = redIndexes.Contains(i) ? Brushes.Red : null;
  507. Assert.Equal(i, index);
  508. Assert.Equal(expectedBackground, container.Background);
  509. }
  510. }
  511. // https://github.com/AvaloniaUI/Avalonia/issues/12838
  512. [Theory]
  513. [InlineData(0d, 10, "4,9")]
  514. [InlineData(0.5d, 20, "4,9,14,19")]
  515. public void NthChild_Selector_Works_For_ItemTemplate_Children(double bufferFactor, int count, string indexesRaw)
  516. {
  517. using var app = App();
  518. var style = new Style(x => x.OfType<ContentPresenter>().NthChild(5, 0).Child().OfType<Canvas>())
  519. {
  520. Setters = { new Setter(Panel.BackgroundProperty, Brushes.Red) },
  521. };
  522. var (target, _, _) = CreateTarget(styles: new[] { style }, bufferFactor: bufferFactor);
  523. var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
  524. Assert.Equal(count, realized.Count);
  525. for (var i = 0; i < count; ++i)
  526. {
  527. var container = realized[i];
  528. var index = target.IndexFromContainer(container);
  529. var redIndexes = indexesRaw.Split(",").Select(Int32.Parse).ToArray();
  530. var expectedBackground = redIndexes.Contains(i) ? Brushes.Red : null;
  531. Assert.Equal(i, index);
  532. Assert.Equal(expectedBackground, ((Canvas)container.Child!).Background);
  533. }
  534. }
  535. [Theory]
  536. [InlineData(0d, 10, "0,5")]
  537. [InlineData(0.5d, 20, "0,5,10,15")]
  538. public void NthLastChild_Selector_Works(double bufferFactor, int count, string indexesRaw)
  539. {
  540. using var app = App();
  541. var style = new Style(x => x.OfType<ContentPresenter>().NthLastChild(5, 0))
  542. {
  543. Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
  544. };
  545. var (target, _, _) = CreateTarget(styles: new[] { style }, bufferFactor: bufferFactor);
  546. var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
  547. Assert.Equal(count, realized.Count);
  548. for (var i = 0; i < count; ++i)
  549. {
  550. var container = realized[i];
  551. var index = target.IndexFromContainer(container);
  552. var redIndexes = indexesRaw.Split(",").Select(Int32.Parse).ToArray();
  553. var expectedBackground = redIndexes.Contains(i) ? Brushes.Red : null;
  554. Assert.Equal(i, index);
  555. Assert.Equal(expectedBackground, container.Background);
  556. }
  557. }
  558. // https://github.com/AvaloniaUI/Avalonia/issues/12838
  559. [Theory]
  560. [InlineData(0d, 10, "0,5")]
  561. [InlineData(0.5d, 20, "0,5,10,15")]
  562. public void NthLastChild_Selector_Works_For_ItemTemplate_Children(double bufferFactor, int count, string indexesRaw)
  563. {
  564. using var app = App();
  565. var style = new Style(x => x.OfType<ContentPresenter>().NthLastChild(5, 0).Child().OfType<Canvas>())
  566. {
  567. Setters = { new Setter(Panel.BackgroundProperty, Brushes.Red) },
  568. };
  569. var (target, _, _) = CreateTarget(styles: new[] { style }, bufferFactor: bufferFactor);
  570. var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
  571. Assert.Equal(count, realized.Count);
  572. for (var i = 0; i < count; ++i)
  573. {
  574. var container = realized[i];
  575. var index = target.IndexFromContainer(container);
  576. var redIndexes = indexesRaw.Split(",").Select(Int32.Parse).ToArray();
  577. var expectedBackground = redIndexes.Contains(i) ? Brushes.Red : null;
  578. Assert.Equal(i, index);
  579. Assert.Equal(expectedBackground, ((Canvas)container.Child!).Background);
  580. }
  581. }
  582. [Theory]
  583. [InlineData(0d, 10)]
  584. [InlineData(0.5d, 15)]
  585. public void ContainerPrepared_Is_Raised_When_Scrolling(double bufferFactor, int expectedRaised)
  586. {
  587. using var app = App();
  588. var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  589. var raised = 0;
  590. itemsControl.ContainerPrepared += (s, e) => ++raised;
  591. scroll.Offset = new Vector(0, 200);
  592. Layout(target);
  593. Assert.Equal(expectedRaised, raised);
  594. }
  595. [Theory]
  596. [InlineData(0d, 10)]
  597. [InlineData(0.5d, 15)]
  598. public void ContainerClearing_Is_Raised_When_Scrolling(double bufferFactor, int expectedRaised)
  599. {
  600. using var app = App();
  601. var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  602. var raised = 0;
  603. itemsControl.ContainerClearing += (s, e) => ++raised;
  604. scroll.Offset = new Vector(0, 200);
  605. Layout(target);
  606. Assert.Equal(expectedRaised, raised);
  607. }
  608. [Theory]
  609. [InlineData(0d, 9)]
  610. [InlineData(0.5d, 19)]
  611. public void ContainerIndexChanged_Is_Raised_On_Insert(double bufferFactor, int expectedRaised)
  612. {
  613. using var app = App();
  614. var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  615. var items = (IList)itemsControl.ItemsSource!;
  616. var raised = 0;
  617. var index = 1;
  618. itemsControl.ContainerIndexChanged += (s, e) =>
  619. {
  620. ++raised;
  621. Assert.Equal(index, e.OldIndex);
  622. Assert.Equal(++index, e.NewIndex);
  623. };
  624. items.Insert(index, "new");
  625. Assert.Equal(expectedRaised, raised);
  626. }
  627. [Theory]
  628. [InlineData(0d, 10, 20)]
  629. [InlineData(0.5d, 20, 15)]
  630. public void ContainerIndexChanged_Is_Raised_When_Item_Inserted_Before_Realized_Elements(double bufferFactor, int expectedRaised, int index)
  631. {
  632. using var app = App();
  633. var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  634. var items = (IList)itemsControl.ItemsSource!;
  635. var raised = 0;
  636. itemsControl.ContainerIndexChanged += (s, e) =>
  637. {
  638. ++raised;
  639. Assert.Equal(index, e.OldIndex);
  640. Assert.Equal(++index, e.NewIndex);
  641. };
  642. scroll.Offset = new Vector(0, 200);
  643. Layout(target);
  644. items.Insert(10, "new");
  645. Assert.Equal(expectedRaised, raised);
  646. }
  647. [Theory]
  648. [InlineData(0d, 8)]
  649. [InlineData(0.5d, 18)]
  650. public void ContainerIndexChanged_Is_Raised_On_Remove(double bufferFactor, int expectedRaised)
  651. {
  652. using var app = App();
  653. var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  654. var items = (IList)itemsControl.ItemsSource!;
  655. var raised = 0;
  656. var index = 1;
  657. itemsControl.ContainerIndexChanged += (s, e) =>
  658. {
  659. ++raised;
  660. Assert.Equal(index + 1, e.OldIndex);
  661. Assert.Equal(index++, e.NewIndex);
  662. };
  663. items.RemoveAt(index);
  664. Assert.Equal(expectedRaised, raised);
  665. }
  666. [Theory]
  667. [InlineData(0d, 10, 20)]
  668. [InlineData(0.5d, 20, 15)]
  669. public void ContainerIndexChanged_Is_Raised_When_Item_Removed_Before_Realized_Elements(double bufferFactor, int expectedRaised, int index)
  670. {
  671. using var app = App();
  672. var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  673. var items = (IList)itemsControl.ItemsSource!;
  674. var raised = 0;
  675. itemsControl.ContainerIndexChanged += (s, e) =>
  676. {
  677. Assert.Equal(index, e.OldIndex);
  678. Assert.Equal(index - 1, e.NewIndex);
  679. ++index;
  680. ++raised;
  681. };
  682. scroll.Offset = new Vector(0, 200);
  683. Layout(target);
  684. items.RemoveAt(10);
  685. Assert.Equal(expectedRaised, raised);
  686. }
  687. [Theory]
  688. [InlineData(0d)]
  689. [InlineData(0.5d)]
  690. public void Fires_Correct_Container_Lifecycle_Events_On_Replace(double bufferFactor)
  691. {
  692. using var app = App();
  693. var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  694. var items = (IList)itemsControl.ItemsSource!;
  695. var events = new List<string>();
  696. itemsControl.ContainerPrepared += (s, e) => events.Add($"Prepared #{e.Container.GetHashCode()} = {e.Index}");
  697. itemsControl.ContainerClearing += (s, e) => events.Add($"Clearing #{e.Container.GetHashCode()}");
  698. itemsControl.ContainerIndexChanged += (s, e) => events.Add($"IndexChanged #{e.Container.GetHashCode()} {e.OldIndex} -> {e.NewIndex}");
  699. var toReplace = target.GetRealizedElements().ElementAt(2)!;
  700. items[2] = "New Item";
  701. Assert.Equal(
  702. new[] { $"Clearing #{toReplace.GetHashCode()}" },
  703. events);
  704. events.Clear();
  705. itemsControl.UpdateLayout();
  706. Assert.Equal(
  707. new[] { $"Prepared #{toReplace.GetHashCode()} = 2" },
  708. events);
  709. events.Clear();
  710. }
  711. [Theory]
  712. [InlineData(0d)]
  713. [InlineData(0.5d)]
  714. public void Scrolling_Down_With_Larger_Element_Does_Not_Cause_Jump_And_Arrives_At_End(double bufferFactor)
  715. {
  716. using var app = App();
  717. var items = Enumerable.Range(0, 1000).Select(x => new ItemWithHeight(x)).ToList();
  718. items[20].Height = 200;
  719. var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate, bufferFactor: bufferFactor);
  720. var index = target.FirstRealizedIndex;
  721. // Scroll down to the larger element.
  722. while (target.LastRealizedIndex < items.Count - 1)
  723. {
  724. scroll.LineDown();
  725. Layout(target);
  726. Assert.True(
  727. target.FirstRealizedIndex >= index,
  728. $"{target.FirstRealizedIndex} is not greater or equal to {index}");
  729. if (scroll.Offset.Y + scroll.Viewport.Height == scroll.Extent.Height)
  730. Assert.Equal(items.Count - 1, target.LastRealizedIndex);
  731. index = target.FirstRealizedIndex;
  732. }
  733. }
  734. [Theory]
  735. [InlineData(0d)]
  736. [InlineData(0.5d)]
  737. public void Scrolling_Up_To_Larger_Element_Does_Not_Cause_Jump(double bufferFactor)
  738. {
  739. using var app = App();
  740. var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x)).ToList();
  741. items[20].Height = 200;
  742. var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate, bufferFactor: bufferFactor);
  743. // Scroll past the larger element.
  744. scroll.Offset = new Vector(0, 600);
  745. Layout(target);
  746. // Precondition checks
  747. Assert.True(target.FirstRealizedIndex > 20);
  748. var index = target.FirstRealizedIndex;
  749. // Scroll up to the top.
  750. while (scroll.Offset.Y > 0)
  751. {
  752. scroll.LineUp();
  753. Layout(target);
  754. Assert.True(target.FirstRealizedIndex <= index, $"{target.FirstRealizedIndex} is not less than {index}");
  755. index = target.FirstRealizedIndex;
  756. }
  757. }
  758. [Theory]
  759. [InlineData(0d)]
  760. [InlineData(0.5d)]
  761. public void Scrolling_Up_To_Smaller_Element_Does_Not_Cause_Jump(double bufferFactor)
  762. {
  763. using var app = App();
  764. var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x, 30)).ToList();
  765. items[20].Height = 25;
  766. var (target, scroll, itemsControl) = CreateTarget(items: items,
  767. itemTemplate: CanvasWithHeightTemplate,
  768. bufferFactor: bufferFactor);
  769. var additionalItemsCount = bufferFactor == 0d
  770. ? 1
  771. // buffer factor of 0.5 and 7 visible items => will be rounded up to 4
  772. // => when we scroll up and are near the _extended_ viewport,
  773. // 4 additional items will be inserted above the current viewport
  774. : Math.Round(target.Children.Count * target.CacheLength, MidpointRounding.AwayFromZero);
  775. // Scroll past the larger element.
  776. scroll.Offset = new Vector(0, 25 * items[0].Height);
  777. Layout(target);
  778. // Precondition checks
  779. Assert.True(target.FirstRealizedIndex > 20);
  780. var index = target.FirstRealizedIndex;
  781. // Scroll up to the top.
  782. while (scroll.Offset.Y > 0)
  783. {
  784. scroll.Offset = scroll.Offset - new Vector(0, 5);
  785. Layout(target);
  786. Assert.True(
  787. target.FirstRealizedIndex <= index,
  788. $"{target.FirstRealizedIndex} is not less than {index}");
  789. Assert.True(
  790. index - target.FirstRealizedIndex <= additionalItemsCount,
  791. $"FirstIndex changed from {index} to {target.FirstRealizedIndex}");
  792. index = target.FirstRealizedIndex;
  793. }
  794. }
  795. [Theory]
  796. [InlineData(0d)]
  797. [InlineData(0.5d)]
  798. public void Does_Not_Throw_When_Estimating_Viewport_With_Ancestor_Margin(double bufferFactor)
  799. {
  800. // Issue #11272
  801. using var app = App();
  802. var (_, _, itemsControl) = CreateUnrootedTarget<ItemsControl>(bufferFactor: bufferFactor);
  803. var container = new Decorator { Margin = new Thickness(100) };
  804. var root = new TestRoot(true, container);
  805. root.LayoutManager.ExecuteInitialLayoutPass();
  806. container.Child = itemsControl;
  807. root.LayoutManager.ExecuteLayoutPass();
  808. }
  809. [Theory]
  810. [InlineData(0d, 20)]
  811. [InlineData(0.5d, 200)]
  812. public void Supports_Null_Recycle_Key_When_Scrolling(double bufferFactor, int offset)
  813. {
  814. using var app = App();
  815. var (_, scroll, itemsControl) = CreateUnrootedTarget<NonRecyclingItemsControl>(bufferFactor: bufferFactor);
  816. var root = CreateRoot(itemsControl);
  817. root.LayoutManager.ExecuteInitialLayoutPass();
  818. var firstItem = itemsControl.ContainerFromIndex(0)!;
  819. scroll.Offset = new(0, offset);
  820. Layout(itemsControl);
  821. Assert.Null(firstItem.Parent);
  822. Assert.Null(firstItem.VisualParent);
  823. Assert.DoesNotContain(firstItem, itemsControl.ItemsPanelRoot!.Children);
  824. }
  825. [Theory]
  826. [InlineData(0d)]
  827. [InlineData(0.5d)]
  828. public void Supports_Null_Recycle_Key_When_Clearing_Items(double bufferFactor)
  829. {
  830. using var app = App();
  831. var (_, _, itemsControl) = CreateUnrootedTarget<NonRecyclingItemsControl>(bufferFactor: bufferFactor);
  832. var root = CreateRoot(itemsControl);
  833. root.LayoutManager.ExecuteInitialLayoutPass();
  834. var firstItem = itemsControl.ContainerFromIndex(0)!;
  835. itemsControl.ItemsSource = null;
  836. Layout(itemsControl);
  837. Assert.Null(firstItem.Parent);
  838. Assert.Null(firstItem.VisualParent);
  839. Assert.Empty(itemsControl.ItemsPanelRoot!.Children);
  840. }
  841. [Theory]
  842. [InlineData(0d)]
  843. [InlineData(0.5d)]
  844. public void ScrollIntoView_On_Effectively_Invisible_Panel_Does_Not_Create_Ghost_Elements(double bufferFactor)
  845. {
  846. var items = new[] { "foo", "bar", "baz" };
  847. var (target, _, itemsControl) = CreateUnrootedTarget<ItemsControl>(items: items, bufferFactor: bufferFactor);
  848. var container = new Decorator { Margin = new Thickness(100), Child = itemsControl };
  849. var root = new TestRoot(true, container);
  850. root.LayoutManager.ExecuteInitialLayoutPass();
  851. // Clear the items and do a layout to recycle all elements.
  852. itemsControl.ItemsSource = null;
  853. root.LayoutManager.ExecuteLayoutPass();
  854. // Should have no realized elements and 3 unrealized elements.
  855. Assert.Equal(0, target.GetRealizedElements().Count);
  856. Assert.Equal(3, target.Children.Count);
  857. // Make the panel effectively invisible and set items.
  858. container.IsVisible = false;
  859. itemsControl.ItemsSource = items;
  860. // Try to scroll into view while effectively invisible.
  861. target.ScrollIntoView(0);
  862. // Make the panel visible and layout.
  863. container.IsVisible = true;
  864. root.LayoutManager.ExecuteLayoutPass();
  865. // Should have 3 realized elements and no unrealized elements.
  866. Assert.Equal(3, target.GetRealizedElements().Count);
  867. Assert.Equal(3, target.Children.Count);
  868. }
  869. // https://github.com/AvaloniaUI/Avalonia/issues/10968
  870. [Theory]
  871. [InlineData(0d)]
  872. [InlineData(0.5d)]
  873. public void Does_Not_Realize_Items_If_Self_Outside_Viewport(double bufferFactor)
  874. {
  875. using var app = App();
  876. var (panel, _, itemsControl) = CreateUnrootedTarget<ItemsControl>(bufferFactor: bufferFactor);
  877. itemsControl.Margin = new Thickness(0.0, 200.0, 0.0, 0.0);
  878. var scrollContentPresenter = new ScrollContentPresenter
  879. {
  880. Width = 100,
  881. Height = 100,
  882. Content = itemsControl
  883. };
  884. var root = CreateRoot(scrollContentPresenter);
  885. root.LayoutManager.ExecuteInitialLayoutPass();
  886. Assert.Equal(1, panel.VisualChildren.Count);
  887. scrollContentPresenter.Content = null;
  888. root.LayoutManager.ExecuteLayoutPass();
  889. scrollContentPresenter.Content = itemsControl;
  890. root.LayoutManager.ExecuteLayoutPass();
  891. Assert.Equal(1, panel.VisualChildren.Count);
  892. }
  893. [Theory]
  894. [InlineData(0d, 0, 8, 1,9)]
  895. [InlineData(0.5d, 0, 17, 0, 17)]
  896. public void Alternating_Backgrounds_Should_Be_Correct_After_Scrolling(double bufferFactor,
  897. int firstIndex1,
  898. int lastIndex1,
  899. int firstIndex2,
  900. int lastIndex2)
  901. {
  902. // Issue #12381.
  903. static void AssertColors(VirtualizingStackPanel target)
  904. {
  905. var containers = target.GetRealizedContainers()!
  906. .Cast<ListBoxItem>()
  907. .ToList();
  908. for (var i = target.FirstRealizedIndex; i <= target.LastRealizedIndex; i++)
  909. {
  910. var container = Assert.IsType<ListBoxItem>(target.ContainerFromIndex(i));
  911. var expectedBackground = i % 2 == 0 ? Colors.Green : Colors.Red;
  912. var brush = Assert.IsAssignableFrom<ISolidColorBrush>(container.Background);
  913. Assert.Equal(expectedBackground, brush.Color);
  914. }
  915. }
  916. using var app = App();
  917. var styles = new[]
  918. {
  919. new Style(x => x.OfType<ListBoxItem>())
  920. {
  921. Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.White) },
  922. },
  923. new Style(x => x.OfType<ListBoxItem>().NthChild(2, 1))
  924. {
  925. Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Green) },
  926. },
  927. new Style(x => x.OfType<ListBoxItem>().NthChild(2, 0))
  928. {
  929. Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
  930. },
  931. };
  932. var (target, scroll, itemsControl) = CreateUnrootedTarget<ListBox>(bufferFactor: bufferFactor);
  933. // We need to display an odd number of items to reproduce the issue.
  934. var root = CreateRoot(itemsControl, clientSize: new(100, 90), styles: styles);
  935. root.LayoutManager.ExecuteInitialLayoutPass();
  936. var containers = target.GetRealizedContainers()!
  937. .Cast<ListBoxItem>()
  938. .ToList();
  939. Assert.Equal(firstIndex1, target.FirstRealizedIndex);
  940. Assert.Equal(lastIndex1, target.LastRealizedIndex);
  941. AssertColors(target);
  942. scroll.Offset = new Vector(0, 10);
  943. target.UpdateLayout();
  944. Assert.Equal(firstIndex2, target.FirstRealizedIndex);
  945. Assert.Equal(lastIndex2, target.LastRealizedIndex);
  946. AssertColors(target);
  947. }
  948. [Theory]
  949. [InlineData(0d, 20)]
  950. [InlineData(0.5d, 15)]
  951. public void Inserting_Item_Before_Viewport_Preserves_FirstRealizedIndex(double bufferFactor, int firstIndex)
  952. {
  953. // Issue #12744
  954. using var app = App();
  955. var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
  956. var items = (IList)itemsControl.ItemsSource!;
  957. // Scroll down 20 items.
  958. scroll.Offset = new Vector(0, 200);
  959. target.UpdateLayout();
  960. Assert.Equal(firstIndex, target.FirstRealizedIndex);
  961. // Insert an item at the beginning.
  962. items.Insert(0, "New Item");
  963. target.UpdateLayout();
  964. // The first realized index should still be 20 as the scroll should be unchanged.
  965. Assert.Equal(firstIndex, target.FirstRealizedIndex);
  966. Assert.Equal(new(0, 200), scroll.Offset);
  967. }
  968. [Theory]
  969. [InlineData(0d)]
  970. [InlineData(0.5d)]
  971. public void Can_Bind_Item_IsVisible(double bufferFactor)
  972. {
  973. using var app = App();
  974. var style = CreateIsVisibleBindingStyle();
  975. var items = Enumerable.Range(0, 100).Select(x => new ItemWithIsVisible(x)).ToList();
  976. var (target, scroll, itemsControl) = CreateTarget(items: items, styles: new[] { style }, bufferFactor: bufferFactor);
  977. var container = target.ContainerFromIndex(2)!;
  978. Assert.True(container.IsVisible);
  979. Assert.Equal(20, container.Bounds.Top);
  980. items[2].IsVisible = false;
  981. Layout(target);
  982. Assert.False(container.IsVisible);
  983. // Next container should be in correct position.
  984. Assert.Equal(20, target.ContainerFromIndex(3)!.Bounds.Top);
  985. }
  986. [Theory]
  987. [InlineData(0d)]
  988. [InlineData(0.5d)]
  989. public void IsVisible_Binding_Persists_After_Scrolling(double bufferFactor)
  990. {
  991. using var app = App();
  992. var style = CreateIsVisibleBindingStyle();
  993. var items = Enumerable.Range(0, 100).Select(x => new ItemWithIsVisible(x)).ToList();
  994. var (target, scroll, itemsControl) = CreateTarget(items: items, styles: new[] { style }, bufferFactor: bufferFactor);
  995. var container = target.ContainerFromIndex(2)!;
  996. Assert.True(container.IsVisible);
  997. Assert.Equal(20, container.Bounds.Top);
  998. items[2].IsVisible = false;
  999. scroll.Offset = new Vector(0, 200);
  1000. Layout(target);
  1001. scroll.Offset = new Vector(0, 0);
  1002. Layout(target);
  1003. container = target.ContainerFromIndex(2)!;
  1004. Assert.False(container.IsVisible);
  1005. }
  1006. [Theory]
  1007. [InlineData(0d)]
  1008. [InlineData(0.5d)]
  1009. public void Recycling_A_Hidden_Control_Shows_It(double bufferFactor)
  1010. {
  1011. using var app = App();
  1012. var style = CreateIsVisibleBindingStyle();
  1013. var itemsList = Enumerable.Range(0, 3).Select(x => new ItemWithIsVisible(x)).ToList();
  1014. var items = new ObservableCollection<ItemWithIsVisible>(itemsList);
  1015. var (target, scroll, itemsControl) = CreateTarget(items: items, styles: new[] { style }, bufferFactor: bufferFactor);
  1016. var container = target.ContainerFromIndex(2)!;
  1017. Assert.True(container.IsVisible);
  1018. Assert.Equal(20, container.Bounds.Top);
  1019. items[2].IsVisible = false;
  1020. Layout(target);
  1021. Assert.False(container.IsVisible);
  1022. items.RemoveAt(2);
  1023. items.Add(new ItemWithIsVisible(3));
  1024. Layout(target);
  1025. Assert.True(container.IsVisible);
  1026. }
  1027. [Theory]
  1028. [InlineData(0d)]
  1029. [InlineData(0.5d)]
  1030. public void ScrollIntoView_With_TargetRect_Outside_Viewport_Should_Scroll_To_Item(double bufferFactor)
  1031. {
  1032. using var app = App();
  1033. var items = Enumerable.Range(0, 101).Select(x => new ItemWithHeight(x, x * 100 + 1));
  1034. var itemTemplate = new FuncDataTemplate<ItemWithHeight>((x, _) =>
  1035. new Border
  1036. {
  1037. Height = 10,
  1038. [!Layoutable.WidthProperty] = new Binding("Height"),
  1039. });
  1040. var (target, scroll, itemsControl) = CreateTarget(
  1041. items: items,
  1042. itemTemplate: itemTemplate,
  1043. styles: new[]
  1044. {
  1045. new Style(x => x.OfType<ScrollViewer>())
  1046. {
  1047. Setters =
  1048. {
  1049. new Setter(ScrollViewer.HorizontalScrollBarVisibilityProperty, ScrollBarVisibility.Visible),
  1050. }
  1051. }
  1052. },
  1053. bufferFactor: bufferFactor);
  1054. itemsControl.ContainerPrepared += (_, ev) =>
  1055. {
  1056. ev.Container.AddHandler(Control.RequestBringIntoViewEvent, (_, e) =>
  1057. {
  1058. var dataContext = (ItemWithHeight)e.TargetObject!.DataContext!;
  1059. e.TargetRect = new Rect(dataContext.Height - 50, 0, 50, 10);
  1060. });
  1061. };
  1062. target.ScrollIntoView(100);
  1063. Assert.Equal(9901, scroll.Offset.X);
  1064. }
  1065. [Theory]
  1066. [InlineData(0d, 10, 10)]
  1067. [InlineData(0.5d, 5, 15)]
  1068. public void ScrollIntoView_Correctly_Scrolls_Down_To_A_Page_Of_Smaller_Items(double bufferFactor, int firstIndex, int count)
  1069. {
  1070. using var app = App();
  1071. // First 10 items have height of 20, next 10 have height of 10.
  1072. var items = Enumerable.Range(0, 20).Select(x => new ItemWithHeight(x, ((29 - x) / 10) * 10));
  1073. var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate, bufferFactor: bufferFactor);
  1074. // Scroll the last item into view.
  1075. target.ScrollIntoView(19);
  1076. // At the time of the scroll, the average item height is 20, so the requested item
  1077. // should be placed at 380 (19 * 20) which therefore results in an extent of 390 to
  1078. // accommodate the item height of 10. This is obviously not a perfect answer, but
  1079. // it's the best we can do without knowing the actual item heights.
  1080. var container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(19));
  1081. Assert.Equal(new Rect(0, 380, 100, 10), container.Bounds);
  1082. Assert.Equal(new Size(100, 100), scroll.Viewport);
  1083. Assert.Equal(new Size(100, 390), scroll.Extent);
  1084. Assert.Equal(new Vector(0, 290), scroll.Offset);
  1085. // Items 10-19 should be visible.
  1086. AssertRealizedItems(target, itemsControl, firstIndex, count);
  1087. }
  1088. [Theory]
  1089. [InlineData(0d, 15, 5, 190, 210, 110)]
  1090. [InlineData(0.5d, 10, 10, 253, 273, 173)]
  1091. public void ScrollIntoView_Correctly_Scrolls_Down_To_A_Page_Of_Larger_Items(double bufferFactor, int firstIndex, int count, int y, int extentHeight, int offset)
  1092. {
  1093. using var app = App();
  1094. // First 10 items have height of 10, next 10 have height of 20.
  1095. var items = Enumerable.Range(0, 20).Select(x => new ItemWithHeight(x, ((x / 10) + 1) * 10));
  1096. var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate, bufferFactor: bufferFactor);
  1097. // Scroll the last item into view.
  1098. target.ScrollIntoView(19);
  1099. // At the time of the scroll, the average item height is 10, so the requested item
  1100. // should be placed at 190 (19 * 10) which therefore results in an extent of 210 to
  1101. // accommodate the item height of 20. This is obviously not a perfect answer, but
  1102. // it's the best we can do without knowing the actual item heights.
  1103. var container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(19));
  1104. Assert.Equal(new Rect(0, y, 100, 20), container.Bounds);
  1105. Assert.Equal(new Size(100, 100), scroll.Viewport);
  1106. Assert.Equal(new Size(100, extentHeight), scroll.Extent);
  1107. Assert.Equal(new Vector(0, offset), scroll.Offset);
  1108. // Items 15-19 should be visible.
  1109. AssertRealizedItems(target, itemsControl, firstIndex, count);
  1110. }
  1111. [Theory]
  1112. [InlineData(0d, 10,10)]
  1113. [InlineData(0.5d, 5, 15)]
  1114. public void ScrollIntoView_Correctly_Scrolls_Right_To_A_Page_Of_Smaller_Items(double bufferFactor, int firstIndex, int count)
  1115. {
  1116. using var app = App();
  1117. // First 10 items have width of 20, next 10 have width of 10.
  1118. var items = Enumerable.Range(0, 20).Select(x => new ItemWithWidth(x, ((29 - x) / 10) * 10));
  1119. var (target, scroll, itemsControl) = CreateTarget(items: items,
  1120. itemTemplate: CanvasWithWidthTemplate,
  1121. orientation: Orientation.Horizontal,
  1122. bufferFactor: bufferFactor);
  1123. // Scroll the last item into view.
  1124. target.ScrollIntoView(19);
  1125. // At the time of the scroll, the average item width is 20, so the requested item
  1126. // should be placed at 380 (19 * 20) which therefore results in an extent of 390 to
  1127. // accommodate the item width of 10. This is obviously not a perfect answer, but
  1128. // it's the best we can do without knowing the actual item widths.
  1129. var container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(19));
  1130. Assert.Equal(new Rect(380, 0, 10, 100), container.Bounds);
  1131. Assert.Equal(new Size(100, 100), scroll.Viewport);
  1132. Assert.Equal(new Size(390, 100), scroll.Extent);
  1133. Assert.Equal(new Vector(290, 0), scroll.Offset);
  1134. // Items 10-19 should be visible.
  1135. AssertRealizedItems(target, itemsControl, firstIndex, count);
  1136. }
  1137. [Theory]
  1138. [InlineData(0d, 15, 5, 190, 210, 110)]
  1139. [InlineData(0.5d, 10, 10, 253, 273, 173)]
  1140. public void ScrollIntoView_Correctly_Scrolls_Right_To_A_Page_Of_Larger_Items(double bufferFactor, int firstIndex, int count, int x, int extentWidth, int offset)
  1141. {
  1142. using var app = App();
  1143. // First 10 items have width of 10, next 10 have width of 20.
  1144. var items = Enumerable.Range(0, 20).Select(x => new ItemWithWidth(x, ((x / 10) + 1) * 10));
  1145. var (target, scroll, itemsControl) = CreateTarget(items: items,
  1146. itemTemplate: CanvasWithWidthTemplate,
  1147. orientation: Orientation.Horizontal,
  1148. bufferFactor: bufferFactor);
  1149. // Scroll the last item into view.
  1150. target.ScrollIntoView(19);
  1151. // At the time of the scroll, the average item width is 10, so the requested item
  1152. // should be placed at 190 (19 * 10) which therefore results in an extent of 210 to
  1153. // accommodate the item width of 20. This is obviously not a perfect answer, but
  1154. // it's the best we can do without knowing the actual item widths.
  1155. var container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(19));
  1156. Assert.Equal(new Rect(x, 0, 20, 100), container.Bounds);
  1157. Assert.Equal(new Size(100, 100), scroll.Viewport);
  1158. Assert.Equal(new Size(extentWidth, 100), scroll.Extent);
  1159. Assert.Equal(new Vector(offset, 0), scroll.Offset);
  1160. // Items 15-19 should be visible.
  1161. AssertRealizedItems(target, itemsControl, firstIndex, count);
  1162. }
  1163. [Theory]
  1164. [InlineData(0d,
  1165. 4,5,
  1166. 8, 11)]
  1167. [InlineData(0.5d,
  1168. 3,6,
  1169. 6, 13)]
  1170. public void Extent_And_Offset_Should_Be_Updated_When_Containers_Resize(double bufferFactor,
  1171. int firstIndex1, int lastIndex1,
  1172. int firstIndex2, int lastIndex2)
  1173. {
  1174. using var app = App();
  1175. // All containers start off with a height of 50 (2 containers fit in viewport).
  1176. var items = Enumerable.Range(0, 20).Select(x => new ItemWithHeight(x, 50)).ToList();
  1177. var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate, bufferFactor: bufferFactor);
  1178. // Scroll to the 5th item (containers 4 and 5 should be visible).
  1179. target.ScrollIntoView(5);
  1180. Assert.Equal(firstIndex1, target.FirstRealizedIndex);
  1181. Assert.Equal(lastIndex1, target.LastRealizedIndex);
  1182. // The extent should be 500 (10 * 50) and the offset should be 200 (4 * 50).
  1183. var container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(5));
  1184. Assert.Equal(new Rect(0, 250, 100, 50), container.Bounds);
  1185. Assert.Equal(new Size(100, 100), scroll.Viewport);
  1186. Assert.Equal(new Size(100, 1000), scroll.Extent);
  1187. Assert.Equal(new Vector(0, 200), scroll.Offset);
  1188. // Update the height of all items to 25 and run a layout pass.
  1189. foreach (var item in items)
  1190. item.Height = 25;
  1191. target.UpdateLayout();
  1192. // The extent should be updated to reflect the new heights. The offset should be
  1193. // unchanged but the first realized index should be updated to 8 (200 / 25).
  1194. Assert.Equal(new Size(100, 100), scroll.Viewport);
  1195. Assert.Equal(new Size(100, 500), scroll.Extent);
  1196. Assert.Equal(new Vector(0, 200), scroll.Offset);
  1197. Assert.Equal(firstIndex2, target.FirstRealizedIndex);
  1198. Assert.Equal(lastIndex2, target.LastRealizedIndex);
  1199. }
  1200. [Theory]
  1201. [InlineData(0d,
  1202. 4, 5,
  1203. 8, 11)]
  1204. [InlineData(0.5d,
  1205. 3, 6,
  1206. 6, 13)]
  1207. public void Focused_Container_Is_Positioned_Correctly_when_Container_Size_Change_Causes_It_To_Be_Moved_Out_Of_Visible_Viewport(double bufferFactor,
  1208. int firstIndex1, int lastIndex1,
  1209. int firstIndex2, int lastIndex2)
  1210. {
  1211. using var app = App();
  1212. // All containers start off with a height of 50 (2 containers fit in viewport).
  1213. var items = Enumerable.Range(0, 20).Select(x => new ItemWithHeight(x, 50)).ToList();
  1214. var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate, bufferFactor: bufferFactor);
  1215. // Scroll to the 5th item (containers 4 and 5 should be visible).
  1216. target.ScrollIntoView(5);
  1217. Assert.Equal(firstIndex1, target.FirstRealizedIndex);
  1218. Assert.Equal(lastIndex1, target.LastRealizedIndex);
  1219. // Focus the 5th item.
  1220. var container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(5));
  1221. container.Focusable = true;
  1222. container.Focus();
  1223. // Update the height of all items to 25 and run a layout pass.
  1224. foreach (var item in items)
  1225. item.Height = 25;
  1226. target.UpdateLayout();
  1227. // The focused container should now be outside the realized range.
  1228. Assert.Equal(firstIndex2, target.FirstRealizedIndex);
  1229. Assert.Equal(lastIndex2, target.LastRealizedIndex);
  1230. // The container should still exist and be positioned outside the visible viewport.
  1231. container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(5));
  1232. Assert.Equal(new Rect(0, 125, 100, 25), container.Bounds);
  1233. }
  1234. [Theory]
  1235. [InlineData(0d,
  1236. 4, 7,
  1237. 3, 6,
  1238. 3, 7)]
  1239. [InlineData(0.5d,
  1240. 0, 7,
  1241. 0, 7,
  1242. 7, 17)]
  1243. public void Focused_Container_Is_Positioned_Correctly_when_Container_Size_Change_Causes_It_To_Be_Moved_Into_Visible_Viewport(double bufferFactor,
  1244. int firstIndex1, int lastIndex1,
  1245. int firstIndex2, int lastIndex2,
  1246. int firstIndex3, int lastIndex3)
  1247. {
  1248. using var app = App();
  1249. // All containers start off with a height of 25 (4 containers fit in viewport).
  1250. var items = Enumerable.Range(0, 20).Select(x => new ItemWithHeight(x, 25)).ToList();
  1251. var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate, bufferFactor: bufferFactor);
  1252. // Scroll to the 5th item (containers 4-7 should be visible).
  1253. target.ScrollIntoView(7);
  1254. Assert.Equal(firstIndex1, target.FirstRealizedIndex);
  1255. Assert.Equal(lastIndex1, target.LastRealizedIndex);
  1256. // Focus the 7th item.
  1257. var container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(7));
  1258. container.Focusable = true;
  1259. container.Focus();
  1260. // Scroll up to the 3rd item (containers 3-6 should still be visible).
  1261. target.ScrollIntoView(3);
  1262. Assert.Equal(firstIndex2, target.FirstRealizedIndex);
  1263. Assert.Equal(lastIndex2, target.LastRealizedIndex);
  1264. // Update the height of all items to 20 and run a layout pass.
  1265. foreach (var item in items)
  1266. item.Height = 20;
  1267. target.UpdateLayout();
  1268. // The focused container should now be inside the realized range.
  1269. Assert.Equal(firstIndex3, target.FirstRealizedIndex);
  1270. Assert.Equal(lastIndex3, target.LastRealizedIndex);
  1271. // The container should be positioned correctly.
  1272. container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(7));
  1273. Assert.Equal(new Rect(0, 140, 100, 20), container.Bounds);
  1274. }
  1275. [Fact]
  1276. public void When_Vertical_Calculates_ViewPort_At_Start_Of_List()
  1277. {
  1278. // Arrange
  1279. using var app = App();
  1280. var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x)).ToList();
  1281. // Act
  1282. var (target, scroll, itemsControl) =
  1283. CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
  1284. items: items,
  1285. itemTemplate: CanvasWithHeightTemplate,
  1286. bufferFactor:0.5d);
  1287. // Assert
  1288. Assert.Equal(0, target.ViewPort.Top);
  1289. Assert.Equal(100, target.ViewPort.Bottom);
  1290. Assert.Equal(0, target.ExtendedViewPort.Top);
  1291. Assert.Equal(200, target.ExtendedViewPort.Bottom);
  1292. }
  1293. [Fact]
  1294. public void When_Vertical_Calculates_ViewPort_At_End_Of_List()
  1295. {
  1296. // Arrange
  1297. using var app = App();
  1298. var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x)).ToList();
  1299. var (target, scroll, itemsControl) =
  1300. CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
  1301. items: items,
  1302. itemTemplate: CanvasWithHeightTemplate,
  1303. bufferFactor: 0.5d);
  1304. // Act
  1305. scroll.Offset = new Vector(0, 910); // scroll to end
  1306. Layout(target);
  1307. // Assert
  1308. Assert.Equal(900, target.ViewPort.Top);
  1309. Assert.Equal(1000, target.ViewPort.Bottom);
  1310. Assert.Equal(800, target.ExtendedViewPort.Top);
  1311. Assert.Equal(1000, target.ExtendedViewPort.Bottom);
  1312. }
  1313. [Fact]
  1314. public void When_Vertical_Calculates_ViewPort_In_Middle_Of_List()
  1315. {
  1316. // Arrange
  1317. using var app = App();
  1318. var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x)).ToList();
  1319. var (target, scroll, itemsControl) =
  1320. CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
  1321. items: items,
  1322. itemTemplate: CanvasWithHeightTemplate,
  1323. bufferFactor: 0.5d);
  1324. // Act
  1325. scroll.Offset = new Vector(0, 500); // scroll to end
  1326. Layout(target);
  1327. // Assert
  1328. Assert.Equal(500, target.ViewPort.Top);
  1329. Assert.Equal(600, target.ViewPort.Bottom);
  1330. Assert.Equal(450, target.ExtendedViewPort.Top);
  1331. Assert.Equal(650, target.ExtendedViewPort.Bottom);
  1332. }
  1333. [Fact]
  1334. public void When_Horizontal_Calculates_ViewPort_At_Start_Of_List()
  1335. {
  1336. // Arrange
  1337. using var app = App();
  1338. var items = Enumerable.Range(0, 100).Select(x => new ItemWithWidth(x)).ToList();
  1339. // Act
  1340. var (target, scroll, itemsControl) =
  1341. CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
  1342. items: items,
  1343. itemTemplate: CanvasWithWidthTemplate,
  1344. orientation: Orientation.Horizontal,
  1345. bufferFactor: 0.5d);
  1346. // Assert
  1347. Assert.Equal(0, target.ViewPort.Left);
  1348. Assert.Equal(100, target.ViewPort.Right);
  1349. Assert.Equal(0, target.ExtendedViewPort.Left);
  1350. Assert.Equal(200, target.ExtendedViewPort.Right);
  1351. }
  1352. [Fact]
  1353. public void When_Horizontal_Calculates_ViewPort_At_End_Of_List()
  1354. {
  1355. // Arrange
  1356. using var app = App();
  1357. var items = Enumerable.Range(0, 100).Select(x => new ItemWithWidth(x)).ToList();
  1358. var (target, scroll, itemsControl) =
  1359. CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
  1360. items: items,
  1361. itemTemplate: CanvasWithWidthTemplate,
  1362. orientation: Orientation.Horizontal,
  1363. bufferFactor: 0.5d);
  1364. // Act
  1365. scroll.Offset = new Vector(900, 0); // scroll to end
  1366. Layout(target);
  1367. // Assert
  1368. Assert.Equal(900, target.ViewPort.Left);
  1369. Assert.Equal(1000, target.ViewPort.Right);
  1370. Assert.Equal(800, target.ExtendedViewPort.Left);
  1371. Assert.Equal(1000, target.ExtendedViewPort.Right);
  1372. }
  1373. [Fact]
  1374. public void When_Horizontal_Calculates_ViewPort_In_Middle_Of_List()
  1375. {
  1376. // Arrange
  1377. using var app = App();
  1378. var items = Enumerable.Range(0, 100).Select(x => new ItemWithWidth(x)).ToList();
  1379. var (target, scroll, itemsControl) =
  1380. CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
  1381. items: items,
  1382. itemTemplate: CanvasWithWidthTemplate,
  1383. orientation: Orientation.Horizontal,
  1384. bufferFactor: 0.5d);
  1385. // Act
  1386. scroll.Offset = new Vector(500, 0); // scroll to end
  1387. Layout(target);
  1388. // Assert
  1389. Assert.Equal(500, target.ViewPort.Left);
  1390. Assert.Equal(600, target.ViewPort.Right);
  1391. Assert.Equal(450, target.ExtendedViewPort.Left);
  1392. Assert.Equal(650, target.ExtendedViewPort.Right);
  1393. }
  1394. [Fact]
  1395. public void Scrolling_Down_Does_Not_Measure_Or_Arrange_Until_Extended_ViewPort_Bounds_Are_Reached()
  1396. {
  1397. using var app = App();
  1398. var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeightAndMeasureArrangeCount(x)).ToList();
  1399. var (target, scroll, itemsControl) =
  1400. CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
  1401. items: items,
  1402. itemTemplate: CanvasWithHeightTemplate,
  1403. bufferFactor: 0.5d);
  1404. Assert.True(target.LastRealizedIndex == 19,
  1405. $"Should show 20 items but last realized index was {target.LastRealizedIndex}");
  1406. // reset counters
  1407. target.ResetMeasureArrangeCounters();
  1408. // shows 20 items, each is 10 high.
  1409. // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
  1410. // until then no measure-arrange call should happen
  1411. // Scroll down until the extended viewport bounds are reached
  1412. while (target.LastRealizedIndex < 20)
  1413. {
  1414. scroll.Offset = new Vector(0, scroll.Offset.Y + 5);
  1415. Layout(target);
  1416. }
  1417. // Assert
  1418. Assert.True(target.Measured == 1, "should be measured only once");
  1419. Assert.True(target.Arranged == 1, "should be arranged only once");
  1420. // the first 5 additional items will be reused when scrolling down, but the remaining 10 visible + 5 additional not touched at all
  1421. var expectedUntouchedItems =
  1422. items.Skip(5 /*additional items*/).Take(15).ToList();
  1423. foreach (var itm in expectedUntouchedItems)
  1424. {
  1425. Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
  1426. Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be arranged but was {itm.Arranged} times");
  1427. }
  1428. var newAdditionalItems = items.Skip(20).Take(5);
  1429. foreach (var itm in newAdditionalItems)
  1430. {
  1431. Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
  1432. Assert.True(itm.Arranged == 1, $"{itm.Caption} should be measured but was {itm.Arranged} times");
  1433. }
  1434. }
  1435. [Fact]
  1436. public void Scrolling_Up_Does_Not_Measure_Or_Arrange_Until_Extended_ViewPort_Bounds_Are_Reached()
  1437. {
  1438. using var app = App();
  1439. var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeightAndMeasureArrangeCount(x)).ToList();
  1440. var (target, scroll, itemsControl) =
  1441. CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
  1442. items: items,
  1443. itemTemplate: CanvasWithHeightTemplate,
  1444. bufferFactor: 0.5d);
  1445. // scroll a bit down so we are not near the start of the list
  1446. scroll.Offset = new Vector(0, 200);
  1447. Layout(target);
  1448. Assert.True(target.FirstRealizedIndex == 15,
  1449. $"Should show items from 20 to 30 (so 15 to 35 including additional items) but first realized index was {target.FirstRealizedIndex}");
  1450. // reset counters
  1451. target.ResetMeasureArrangeCounters();
  1452. // shows 20 items, each is 10 high.
  1453. // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
  1454. // until then no measure-arrange call should happen
  1455. var initialFirstRealizedIndex = target.FirstRealizedIndex;
  1456. // Scroll down until the extended viewport bounds are reached
  1457. while (target.FirstRealizedIndex >= 15)
  1458. {
  1459. scroll.Offset = new Vector(0, scroll.Offset.Y - 5);
  1460. Layout(target);
  1461. }
  1462. // Assert
  1463. Assert.True(target.Measured == 1, "should be measured only once");
  1464. Assert.True(target.Arranged == 1, "should be arranged only once");
  1465. // the last 5 additional items will be reused when scrolling up, but the remaining 10 visible + 5 additional not touched at all
  1466. var expectedUntouchedItems = items.Skip(initialFirstRealizedIndex + 1).Take(15).ToList();
  1467. foreach (var itm in expectedUntouchedItems)
  1468. {
  1469. Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
  1470. Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be arranged but was {itm.Arranged} times");
  1471. }
  1472. // now that we scrolled up to index 19, items 18,17,16,15 and 14 should be the "additional" ones
  1473. var newAdditionalItems = items.Skip(initialFirstRealizedIndex - 6).Take(6);
  1474. foreach (var itm in newAdditionalItems)
  1475. {
  1476. Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
  1477. Assert.True(itm.Arranged == 1, $"{itm.Caption} should be measured but was {itm.Arranged} times");
  1478. }
  1479. }
  1480. [Fact]
  1481. public void Scrolling_Down_To_End_Of_List_Only_Measures_Once_When_Last_Item_Is_Reached()
  1482. {
  1483. using var app = App();
  1484. var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeightAndMeasureArrangeCount(x)).ToList();
  1485. var (target, scroll, itemsControl) =
  1486. CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
  1487. items: items,
  1488. itemTemplate: CanvasWithHeightTemplate,
  1489. bufferFactor: 0.5d);
  1490. // scroll a bit down so we are near the end of the list
  1491. scroll.Offset = new Vector(0, 800); // so we render 75 to 95 with a buffer size of 5
  1492. Layout(target);
  1493. Assert.True(target.LastRealizedIndex == 94,
  1494. $"Should show 20 items but last realized index was {target.LastRealizedIndex}");
  1495. // reset counters
  1496. target.ResetMeasureArrangeCounters();
  1497. // shows 20 items, each is 10 high.
  1498. // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
  1499. // until then no measure-arrange call should happen
  1500. var initialLastRealizedIndex = target.LastRealizedIndex;
  1501. // Scroll down until we reached the very last item
  1502. while (target.LastRealizedIndex < 99)
  1503. {
  1504. scroll.Offset = new Vector(0, scroll.Offset.Y + 5);
  1505. Layout(target);
  1506. }
  1507. // Assert
  1508. Assert.True(target.Measured == 1, "should be measured only once even though we are at the end of the list");
  1509. Assert.True(target.Arranged == 1, "should be arranged only once even though we are at the end of the list");
  1510. // the first 5 additional items will be reused when scrolling down, but the remaining 10 visible + 5 additional not touched at all
  1511. var expectedUntouchedItems =
  1512. items.Skip(initialLastRealizedIndex + 1 - 15).Take(15).ToList();
  1513. foreach (var itm in expectedUntouchedItems)
  1514. {
  1515. Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
  1516. Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be arranged but was {itm.Arranged} times");
  1517. }
  1518. var newAdditionalItems = items.Skip(initialLastRealizedIndex + 1).Take(5);
  1519. foreach (var itm in newAdditionalItems)
  1520. {
  1521. Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
  1522. Assert.True(itm.Arranged == 1, $"{itm.Caption} should be measured but was {itm.Arranged} times");
  1523. }
  1524. }
  1525. [Fact]
  1526. public void Scrolling_Up_To_Start_Of_List_Only_Measures_Once_When_First_Item_Is_Reached()
  1527. {
  1528. using var app = App();
  1529. var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeightAndMeasureArrangeCount(x)).ToList();
  1530. var (target, scroll, itemsControl) =
  1531. CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
  1532. items: items,
  1533. itemTemplate: CanvasWithHeightTemplate,
  1534. bufferFactor: 0.5d);
  1535. // scroll a bit down so we are not near the start of the list
  1536. scroll.Offset = new Vector(0, 105);
  1537. Layout(target);
  1538. Assert.True(target.FirstRealizedIndex == 5,
  1539. $"Should show items from 10 to 20 (so 5 to 25 including additional items) but first realized index was {target.FirstRealizedIndex}");
  1540. // reset counters
  1541. target.ResetMeasureArrangeCounters();
  1542. // shows 20 items, each is 10 high.
  1543. // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
  1544. // until then no measure-arrange call should happen
  1545. // Scroll down until the extended viewport bounds are reached
  1546. while (target.FirstRealizedIndex > 0)
  1547. {
  1548. scroll.Offset = new Vector(0, scroll.Offset.Y - 5);
  1549. Layout(target);
  1550. }
  1551. // Assert
  1552. Assert.True(target.Measured == 1, "should be measured only once even though we are at the start of the list");
  1553. Assert.True(target.Arranged == 1, "should be arranged only once even though we are at the start of the list");
  1554. // the last 5 additional items will be reused when scrolling up, but the remaining 10 visible + 5 additional not touched at all
  1555. var expectedMeasuredItems = items.Take(20).ToList();
  1556. foreach (var itm in expectedMeasuredItems)
  1557. {
  1558. Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
  1559. Assert.True(itm.Arranged == 1, $"{itm.Caption} should be arranged but was {itm.Arranged} times");
  1560. }
  1561. // now that we scrolled up to index 19, items 18,17,16,15 and 14 should be the "additional" ones
  1562. var untouchedItems = items.Skip(20).ToList();
  1563. foreach (var itm in untouchedItems)
  1564. {
  1565. Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
  1566. Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be measured but was {itm.Arranged} times");
  1567. }
  1568. }
  1569. [Fact]
  1570. public void Scrolling_Right_Does_Not_Measure_Or_Arrange_Until_Extended_ViewPort_Bounds_Are_Reached()
  1571. {
  1572. using var app = App();
  1573. var items = Enumerable.Range(0, 100).Select(x => new ItemWithWidthAndMeasureArrangeCount(x)).ToList();
  1574. var (target, scroll, itemsControl) =
  1575. CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
  1576. items: items,
  1577. itemTemplate: CanvasWithWidthTemplate,
  1578. orientation: Orientation.Horizontal,
  1579. bufferFactor: 0.5d);
  1580. Assert.True(target.LastRealizedIndex == 19,
  1581. $"Should show 20 items but last realized index was {target.LastRealizedIndex}");
  1582. // reset counters
  1583. target.ResetMeasureArrangeCounters();
  1584. // shows 20 items, each is 10 high.
  1585. // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
  1586. // until then no measure-arrange call should happen
  1587. // Scroll down until the extended viewport bounds are reached
  1588. while (target.LastRealizedIndex < 20)
  1589. {
  1590. scroll.Offset = new Vector(scroll.Offset.X + 5, 0);
  1591. Layout(target);
  1592. }
  1593. // Assert
  1594. Assert.True(target.Measured == 1, "should be measured only once");
  1595. Assert.True(target.Arranged == 1, "should be arranged only once");
  1596. // the first 5 additional items will be reused when scrolling down, but the remaining 10 visible + 5 additional not touched at all
  1597. var expectedUntouchedItems =
  1598. items.Skip(5 /*additional items*/).Take(15).ToList();
  1599. foreach (var itm in expectedUntouchedItems)
  1600. {
  1601. Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
  1602. Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be arranged but was {itm.Arranged} times");
  1603. }
  1604. var newAdditionalItems = items.Skip(20).Take(5);
  1605. foreach (var itm in newAdditionalItems)
  1606. {
  1607. Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
  1608. Assert.True(itm.Arranged == 1, $"{itm.Caption} should be measured but was {itm.Arranged} times");
  1609. }
  1610. }
  1611. [Fact]
  1612. public void Scrolling_Left_Does_Not_Measure_Or_Arrange_Until_Extended_ViewPort_Bounds_Are_Reached()
  1613. {
  1614. using var app = App();
  1615. var items = Enumerable.Range(0, 100).Select(x => new ItemWithWidthAndMeasureArrangeCount(x)).ToList();
  1616. var (target, scroll, itemsControl) =
  1617. CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
  1618. items: items,
  1619. itemTemplate: CanvasWithWidthTemplate,
  1620. orientation: Orientation.Horizontal,
  1621. bufferFactor: 0.5d);
  1622. // scroll a bit down so we are not near the start of the list
  1623. scroll.Offset = new Vector(200, 0);
  1624. Layout(target);
  1625. Assert.True(target.FirstRealizedIndex == 15,
  1626. $"Should show items from 20 to 30 (so 15 to 35 including additional items) but first realized index was {target.FirstRealizedIndex}");
  1627. // reset counters
  1628. target.ResetMeasureArrangeCounters();
  1629. // shows 20 items, each is 10 high.
  1630. // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
  1631. // until then no measure-arrange call should happen
  1632. var initialFirstRealizedIndex = target.FirstRealizedIndex;
  1633. // Scroll down until the extended viewport bounds are reached
  1634. while (target.FirstRealizedIndex >= 15)
  1635. {
  1636. scroll.Offset = new Vector(scroll.Offset.X - 5, 0);
  1637. Layout(target);
  1638. }
  1639. // Assert
  1640. Assert.True(target.Measured == 1, "should be measured only once");
  1641. Assert.True(target.Arranged == 1, "should be arranged only once");
  1642. // the last 5 additional items will be reused when scrolling up, but the remaining 10 visible + 5 additional not touched at all
  1643. var expectedUntouchedItems = items.Skip(initialFirstRealizedIndex + 1).Take(15).ToList();
  1644. foreach (var itm in expectedUntouchedItems)
  1645. {
  1646. Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
  1647. Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be arranged but was {itm.Arranged} times");
  1648. }
  1649. // now that we scrolled up to index 19, items 18,17,16,15 and 14 should be the "additional" ones
  1650. var newAdditionalItems = items.Skip(initialFirstRealizedIndex - 6).Take(6);
  1651. foreach (var itm in newAdditionalItems)
  1652. {
  1653. Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
  1654. Assert.True(itm.Arranged == 1, $"{itm.Caption} should be measured but was {itm.Arranged} times");
  1655. }
  1656. }
  1657. [Fact]
  1658. public void Scrolling_Right_To_End_Of_List_Only_Measures_Once_When_Last_Item_Is_Reached()
  1659. {
  1660. using var app = App();
  1661. var items = Enumerable.Range(0, 100).Select(x => new ItemWithWidthAndMeasureArrangeCount(x)).ToList();
  1662. var (target, scroll, itemsControl) =
  1663. CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
  1664. items: items,
  1665. itemTemplate: CanvasWithWidthTemplate,
  1666. orientation: Orientation.Horizontal,
  1667. bufferFactor: 0.5d);
  1668. // scroll a bit down so we are near the end of the list
  1669. scroll.Offset = new Vector(800, 0); // so we render 75 to 95 with a buffer size of 5
  1670. Layout(target);
  1671. Assert.True(target.LastRealizedIndex == 94,
  1672. $"Should show 20 items but last realized index was {target.LastRealizedIndex}");
  1673. // reset counters
  1674. target.ResetMeasureArrangeCounters();
  1675. // shows 20 items, each is 10 high.
  1676. // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
  1677. // until then no measure-arrange call should happen
  1678. var initialLastRealizedIndex = target.LastRealizedIndex;
  1679. // Scroll down until we reached the very last item
  1680. while (target.LastRealizedIndex < 99)
  1681. {
  1682. scroll.Offset = new Vector(scroll.Offset.X + 5, 0);
  1683. Layout(target);
  1684. }
  1685. // Assert
  1686. Assert.True(target.Measured == 1, "should be measured only once even though we are at the end of the list");
  1687. Assert.True(target.Arranged == 1, "should be arranged only once even though we are at the end of the list");
  1688. // the first 5 additional items will be reused when scrolling down, but the remaining 10 visible + 5 additional not touched at all
  1689. var expectedUntouchedItems =
  1690. items.Skip(initialLastRealizedIndex + 1 - 15).Take(15).ToList();
  1691. foreach (var itm in expectedUntouchedItems)
  1692. {
  1693. Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
  1694. Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be arranged but was {itm.Arranged} times");
  1695. }
  1696. var newAdditionalItems = items.Skip(initialLastRealizedIndex + 1).Take(5);
  1697. foreach (var itm in newAdditionalItems)
  1698. {
  1699. Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
  1700. Assert.True(itm.Arranged == 1, $"{itm.Caption} should be measured but was {itm.Arranged} times");
  1701. }
  1702. }
  1703. [Fact]
  1704. public void Scrolling_Left_To_Start_Of_List_Only_Measures_Once_When_First_Item_Is_Reached()
  1705. {
  1706. using var app = App();
  1707. var items = Enumerable.Range(0, 100).Select(x => new ItemWithWidthAndMeasureArrangeCount(x)).ToList();
  1708. var (target, scroll, itemsControl) =
  1709. CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
  1710. items: items,
  1711. itemTemplate: CanvasWithWidthTemplate,
  1712. orientation: Orientation.Horizontal,
  1713. bufferFactor: 0.5d);
  1714. // scroll a bit down so we are not near the start of the list
  1715. scroll.Offset = new Vector(105, 0);
  1716. Layout(target);
  1717. Assert.True(target.FirstRealizedIndex == 5,
  1718. $"Should show items from 10 to 20 (so 5 to 25 including additional items) but first realized index was {target.FirstRealizedIndex}");
  1719. // reset counters
  1720. target.ResetMeasureArrangeCounters();
  1721. // shows 20 items, each is 10 high.
  1722. // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
  1723. // until then no measure-arrange call should happen
  1724. // Scroll down until the extended viewport bounds are reached
  1725. while (target.FirstRealizedIndex > 0)
  1726. {
  1727. scroll.Offset = new Vector(scroll.Offset.X - 5, 0);
  1728. Layout(target);
  1729. }
  1730. // Assert
  1731. Assert.True(target.Measured == 1, "should be measured only once even though we are at the start of the list");
  1732. Assert.True(target.Arranged == 1, "should be arranged only once even though we are at the start of the list");
  1733. // the last 5 additional items will be reused when scrolling up, but the remaining 10 visible + 5 additional not touched at all
  1734. var expectedMeasuredItems = items.Take(20).ToList();
  1735. foreach (var itm in expectedMeasuredItems)
  1736. {
  1737. Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
  1738. Assert.True(itm.Arranged == 1, $"{itm.Caption} should be arranged but was {itm.Arranged} times");
  1739. }
  1740. // now that we scrolled up to index 19, items 18,17,16,15 and 14 should be the "additional" ones
  1741. var untouchedItems = items.Skip(20).ToList();
  1742. foreach (var itm in untouchedItems)
  1743. {
  1744. Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
  1745. Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be measured but was {itm.Arranged} times");
  1746. }
  1747. }
  1748. private static IReadOnlyList<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
  1749. {
  1750. return target.GetRealizedElements()
  1751. .Select(x => x is null ? -1 : itemsControl.IndexFromContainer((Control)x))
  1752. .ToList();
  1753. }
  1754. private static void AssertRealizedItems(
  1755. VirtualizingStackPanel target,
  1756. ItemsControl itemsControl,
  1757. int firstIndex,
  1758. int count)
  1759. {
  1760. Assert.All(target.GetRealizedContainers()!, x => Assert.Same(target, x.VisualParent));
  1761. Assert.All(target.GetRealizedContainers()!, x => Assert.Same(itemsControl, x.Parent));
  1762. var childIndexes = target.GetRealizedContainers()!
  1763. .Select(x => itemsControl.IndexFromContainer(x))
  1764. .Where(x => x >= 0)
  1765. .OrderBy(x => x)
  1766. .ToList();
  1767. Assert.Equal(Enumerable.Range(firstIndex, count), childIndexes);
  1768. var visibleChildren = target.Children
  1769. .Where(x => x.IsVisible)
  1770. .ToList();
  1771. Assert.Equal(count, visibleChildren.Count);
  1772. }
  1773. private static void AssertRealizedControlItems<TContainer>(
  1774. VirtualizingStackPanel target,
  1775. ItemsControl itemsControl,
  1776. int firstIndex,
  1777. int count)
  1778. {
  1779. Assert.All(target.GetRealizedContainers()!, x => Assert.IsType<TContainer>(x));
  1780. Assert.All(target.GetRealizedContainers()!, x => Assert.Same(target, x.VisualParent));
  1781. Assert.All(target.GetRealizedContainers()!, x => Assert.Same(itemsControl, x.Parent));
  1782. var childIndexes = target.GetRealizedContainers()!
  1783. .Select(x => itemsControl.IndexFromContainer(x))
  1784. .Where(x => x >= 0)
  1785. .OrderBy(x => x)
  1786. .ToList();
  1787. Assert.Equal(Enumerable.Range(firstIndex, count), childIndexes);
  1788. }
  1789. private static (VirtualizingStackPanel, ScrollViewer, ItemsControl) CreateTarget(
  1790. IEnumerable<object>? items = null,
  1791. Optional<IDataTemplate?> itemTemplate = default,
  1792. IEnumerable<Style>? styles = null,
  1793. Orientation orientation = Orientation.Vertical,
  1794. double bufferFactor = 0.0d)
  1795. {
  1796. return CreateTarget<ItemsControl, VirtualizingStackPanel>(
  1797. items: items,
  1798. itemTemplate: itemTemplate,
  1799. styles: styles,
  1800. orientation: orientation,
  1801. bufferFactor: bufferFactor);
  1802. }
  1803. private static (TStackPanel, ScrollViewer, T) CreateTarget<T, TStackPanel>(
  1804. IEnumerable<object>? items = null,
  1805. Optional<IDataTemplate?> itemTemplate = default,
  1806. IEnumerable<Style>? styles = null,
  1807. Orientation orientation = Orientation.Vertical,
  1808. double bufferFactor = 0.0d)
  1809. where T : ItemsControl, new()
  1810. where TStackPanel : VirtualizingStackPanel, new()
  1811. {
  1812. var (target, scroll, itemsControl) = CreateUnrootedTarget<T, TStackPanel>(items, itemTemplate, orientation, bufferFactor: bufferFactor);
  1813. var root = CreateRoot(itemsControl, styles: styles);
  1814. root.LayoutManager.ExecuteInitialLayoutPass();
  1815. return (target, scroll, itemsControl);
  1816. }
  1817. private static (VirtualizingStackPanel, ScrollViewer, T) CreateUnrootedTarget<T>(
  1818. IEnumerable<object>? items = null,
  1819. Optional<IDataTemplate?> itemTemplate = default,
  1820. Orientation orientation = Orientation.Vertical,
  1821. double bufferFactor = 0.0d)
  1822. where T : ItemsControl, new()
  1823. => CreateUnrootedTarget<T, VirtualizingStackPanel>(items, itemTemplate, orientation, bufferFactor);
  1824. private static (TStackPanel, ScrollViewer, T) CreateUnrootedTarget<T, TStackPanel>(
  1825. IEnumerable<object>? items = null,
  1826. Optional<IDataTemplate?> itemTemplate = default,
  1827. Orientation orientation = Orientation.Vertical,
  1828. double bufferFactor = 0.0d)
  1829. where T : ItemsControl, new()
  1830. where TStackPanel : VirtualizingStackPanel, new()
  1831. {
  1832. var target = new TStackPanel
  1833. {
  1834. Orientation = orientation,
  1835. CacheLength = bufferFactor,
  1836. };
  1837. items ??= new ObservableCollection<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
  1838. var presenter = new ItemsPresenter
  1839. {
  1840. [~ItemsPresenter.ItemsPanelProperty] = new TemplateBinding(ItemsPresenter.ItemsPanelProperty),
  1841. };
  1842. var scroll = new ScrollViewer
  1843. {
  1844. Name = "PART_ScrollViewer",
  1845. Content = presenter,
  1846. };
  1847. if (orientation == Orientation.Horizontal)
  1848. {
  1849. scroll.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
  1850. scroll.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
  1851. }
  1852. scroll.Template = ScrollViewerTemplate();
  1853. var itemsControl = new T
  1854. {
  1855. ItemsSource = items,
  1856. Template = new FuncControlTemplate<T>((_, ns) => scroll.RegisterInNameScope(ns)),
  1857. ItemsPanel = new FuncTemplate<Panel?>(() => target),
  1858. ItemTemplate = itemTemplate.GetValueOrDefault(DefaultItemTemplate()),
  1859. };
  1860. return (target, scroll, itemsControl);
  1861. }
  1862. private static TestRoot CreateRoot(
  1863. Control? child,
  1864. Size? clientSize = null,
  1865. IEnumerable<Style>? styles = null)
  1866. {
  1867. var root = new TestRoot(true, child);
  1868. root.ClientSize = clientSize ?? new(100, 100);
  1869. if (styles is not null)
  1870. root.Styles.AddRange(styles);
  1871. return root;
  1872. }
  1873. private static Style CreateIsVisibleBindingStyle()
  1874. {
  1875. return new Style(x => x.OfType<ContentPresenter>())
  1876. {
  1877. Setters =
  1878. {
  1879. new Setter(Visual.IsVisibleProperty, new Binding("IsVisible")),
  1880. }
  1881. };
  1882. }
  1883. private static IDataTemplate DefaultItemTemplate()
  1884. {
  1885. return new FuncDataTemplate<object>((x, _) => new Canvas { Width = 100, Height = 10 });
  1886. }
  1887. private static void Layout(Control target)
  1888. {
  1889. var root = (ILayoutRoot?)target.GetVisualRoot();
  1890. root?.LayoutManager.ExecuteLayoutPass();
  1891. }
  1892. private static IControlTemplate ListBoxItemTemplate()
  1893. {
  1894. return new FuncControlTemplate<ListBoxItem>((x, ns) =>
  1895. new ContentPresenter
  1896. {
  1897. Name = "PART_ContentPresenter",
  1898. Width = 100,
  1899. Height = 10,
  1900. }.RegisterInNameScope(ns));
  1901. }
  1902. private static IControlTemplate ScrollViewerTemplate()
  1903. {
  1904. return new FuncControlTemplate<ScrollViewer>((x, ns) =>
  1905. new ScrollContentPresenter
  1906. {
  1907. Name = "PART_ScrollContentPresenter",
  1908. }.RegisterInNameScope(ns));
  1909. }
  1910. private static IDisposable App() => UnitTestApplication.Start(TestServices.RealFocus);
  1911. private class ItemWithHeight : NotifyingBase
  1912. {
  1913. private double _height;
  1914. public ItemWithHeight(int index, double height = 10)
  1915. {
  1916. Caption = $"Item {index}";
  1917. Height = height;
  1918. }
  1919. public string Caption { get; set; }
  1920. public double Height
  1921. {
  1922. get => _height;
  1923. set => SetField(ref _height, value);
  1924. }
  1925. }
  1926. private class ItemWithWidth : NotifyingBase
  1927. {
  1928. private double _width;
  1929. public ItemWithWidth(int index, double width = 10)
  1930. {
  1931. Caption = $"Item {index}";
  1932. Width = width;
  1933. }
  1934. public string Caption { get; set; }
  1935. public double Width
  1936. {
  1937. get => _width;
  1938. set => SetField(ref _width, value);
  1939. }
  1940. }
  1941. private class ItemWithIsVisible : NotifyingBase
  1942. {
  1943. private bool _isVisible = true;
  1944. public ItemWithIsVisible(int index)
  1945. {
  1946. Caption = $"Item {index}";
  1947. }
  1948. public string Caption { get; set; }
  1949. public bool IsVisible
  1950. {
  1951. get => _isVisible;
  1952. set => SetField(ref _isVisible, value);
  1953. }
  1954. }
  1955. private class ResettingCollection : List<string>, INotifyCollectionChanged
  1956. {
  1957. public ResettingCollection(IEnumerable<string> items)
  1958. {
  1959. AddRange(items);
  1960. }
  1961. public void Reset(IEnumerable<string> items)
  1962. {
  1963. Clear();
  1964. AddRange(items);
  1965. CollectionChanged?.Invoke(
  1966. this,
  1967. new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
  1968. }
  1969. public event NotifyCollectionChangedEventHandler? CollectionChanged;
  1970. }
  1971. private class NonRecyclingItemsControl : ItemsControl
  1972. {
  1973. protected override Type StyleKeyOverride => typeof(ItemsControl);
  1974. protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
  1975. {
  1976. recycleKey = null;
  1977. return true;
  1978. }
  1979. }
  1980. private interface ICountMeasureArrangeCalls
  1981. {
  1982. int Measured { get; set; }
  1983. int Arranged { get; set; }
  1984. }
  1985. [DebuggerDisplay("{DebuggerDisplay}")]
  1986. private class ItemWithHeightAndMeasureArrangeCount : ItemWithHeight, ICountMeasureArrangeCalls
  1987. {
  1988. public ItemWithHeightAndMeasureArrangeCount(int index, double height = 10) : base(index, height)
  1989. {
  1990. }
  1991. public int Measured { get; set; }
  1992. public int Arranged { get; set; }
  1993. private string DebuggerDisplay => $"{Caption} (height: {Height} m:{Measured} a: {Arranged})";
  1994. }
  1995. [DebuggerDisplay("{DebuggerDisplay}")]
  1996. private class ItemWithWidthAndMeasureArrangeCount : ItemWithWidth, ICountMeasureArrangeCalls
  1997. {
  1998. public ItemWithWidthAndMeasureArrangeCount(int index, double width = 10) : base(index, width)
  1999. {
  2000. }
  2001. public int Measured { get; set; }
  2002. public int Arranged { get; set; }
  2003. private string DebuggerDisplay => $"{Caption} (width: {Width} m:{Measured} a: {Arranged})";
  2004. }
  2005. private class VirtualizingStackPanelCountingMeasureArrange : VirtualizingStackPanel
  2006. {
  2007. public int Measured { get; set; }
  2008. public int Arranged { get; set; }
  2009. public void ResetMeasureArrangeCounters()
  2010. {
  2011. // reset counters
  2012. Measured = 0;
  2013. Arranged = 0;
  2014. foreach (var itm in Items.OfType<ICountMeasureArrangeCalls>())
  2015. {
  2016. itm.Measured = 0;
  2017. itm.Arranged = 0;
  2018. }
  2019. }
  2020. protected override Size MeasureOverride(Size availableSize)
  2021. {
  2022. Measured++;
  2023. return base.MeasureOverride(availableSize);
  2024. }
  2025. protected override Size ArrangeOverride(Size finalSize)
  2026. {
  2027. Arranged++;
  2028. return base.ArrangeOverride(finalSize);
  2029. }
  2030. }
  2031. private class CanvasCountingMeasureArrangeCalls : Canvas
  2032. {
  2033. protected override Size MeasureOverride(Size availableSize)
  2034. {
  2035. if(DataContext is ICountMeasureArrangeCalls itm)
  2036. itm.Measured++;
  2037. return base.MeasureOverride(availableSize);
  2038. }
  2039. protected override Size ArrangeOverride(Size finalSize)
  2040. {
  2041. if(DataContext is ICountMeasureArrangeCalls itm)
  2042. itm.Arranged++;
  2043. return base.ArrangeOverride(finalSize);
  2044. }
  2045. }
  2046. }
  2047. }