12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549 |
- using System;
- using System.Collections;
- using System.Collections.Generic;
- using System.Collections.ObjectModel;
- using System.Collections.Specialized;
- using System.Diagnostics;
- using System.Linq;
- using Avalonia.Collections;
- using Avalonia.Controls.Presenters;
- using Avalonia.Controls.Primitives;
- using Avalonia.Controls.Templates;
- using Avalonia.Data;
- using Avalonia.Input;
- using Avalonia.Layout;
- using Avalonia.Media;
- using Avalonia.Styling;
- using Avalonia.UnitTests;
- using Avalonia.VisualTree;
- using Xunit;
- #nullable enable
- namespace Avalonia.Controls.UnitTests
- {
- public class VirtualizingStackPanelTests : ScopedTestBase
- {
- private static FuncDataTemplate<ItemWithHeight> CanvasWithHeightTemplate = new((_, _) =>
- new CanvasCountingMeasureArrangeCalls
- {
- Width = 100,
- [!Layoutable.HeightProperty] = new Binding("Height"),
- });
- private static FuncDataTemplate<ItemWithWidth> CanvasWithWidthTemplate = new((_, _) =>
- new CanvasCountingMeasureArrangeCalls
- {
- Height = 100,
- [!Layoutable.WidthProperty] = new Binding("Width"),
- });
- [Theory]
- [InlineData(0d , 10)]
- [InlineData(0.5d, 20)]
- public void Creates_Initial_Items(double bufferFactor, int expectedCount)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
- Assert.Equal(1000, scroll.Extent.Height);
- AssertRealizedItems(target, itemsControl, 0, expectedCount);
- }
- [Theory]
- [InlineData(0d, 10)]
- [InlineData(0.5d, 20)] // Buffer factor of 0.5. Since at start there is no room, the 10 additional items are just appended
- public void Initializes_Initial_Control_Items(double bufferFactor, int expectedCount)
- {
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10 });
- var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: null, bufferFactor:bufferFactor);
- Assert.Equal(1000, scroll.Extent.Height);
- AssertRealizedControlItems<Button>(target, itemsControl, 0, expectedCount);
- }
- [Theory]
- [InlineData(0d, 2)]
- [InlineData(0.5d, 2)]
- public void Creates_Reassigned_Items(double bufferFactor, int expectedCount)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(items: Array.Empty<object>(), bufferFactor: bufferFactor);
- Assert.Empty(itemsControl.GetRealizedContainers());
- itemsControl.ItemsSource = new[] { "foo", "bar" };
- Layout(target);
- AssertRealizedItems(target, itemsControl, 0, expectedCount);
- }
- [Theory]
- [InlineData(0d, 1, 10)]
- [InlineData(0.5d, 0, 20)]
- public void Scrolls_Down_One_Item(double bufferFactor, int expectedFirstIndex, int expectedCount)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
- scroll.Offset = new Vector(0, 10);
- Layout(target);
- AssertRealizedItems(target, itemsControl, expectedFirstIndex, expectedCount);
- }
- [Theory]
- [InlineData(0d, 20,10)]
- [InlineData(0.5d, 15,20)]
- public void Scrolls_Down_More_Than_A_Page(double bufferFactor, int expectedFirstIndex, int expectedCount)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
- scroll.Offset = new Vector(0, 200);
- Layout(target);
- AssertRealizedItems(target, itemsControl, expectedFirstIndex, expectedCount);
- }
- [Theory]
- [InlineData(0d, 11, 10)]
- [InlineData(0.5d, 6, 20)]
- public void Scrolls_Down_To_Index(double bufferFactor, int expectedFirstIndex, int expectedCount)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- target.ScrollIntoView(20);
- AssertRealizedItems(target, itemsControl, expectedFirstIndex, expectedCount);
- }
- [Theory]
- [InlineData(0d, 90, 20, 10)]
- [InlineData(0.5d, 80, 15, 20)]
- public void Scrolls_Up_To_Index(double bufferFactor, int firstRealizedIndex, int expectedFirstIndex, int expectedCount)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
- scroll.ScrollToEnd();
- Layout(target);
- Assert.Equal(firstRealizedIndex, target.FirstRealizedIndex);
- target.ScrollIntoView(20);
- AssertRealizedItems(target, itemsControl, expectedFirstIndex, expectedCount);
- }
- [Theory]
- [InlineData(0d, 11)]
- [InlineData(0.5d, 21)]
- public void Scrolling_Up_To_Index_Does_Not_Create_A_Page_Of_Unrealized_Elements(double bufferFactor, int expectedCount)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
- scroll.ScrollToEnd();
- Layout(target);
- target.ScrollIntoView(20);
- Assert.Equal(expectedCount, target.Children.Count);
- }
- [Theory]
- [InlineData(0d,
- 10,
- 11,
- "-1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10",
- 10)]
- [InlineData(0.5d,
- 20,
- 21,
- "-1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20",
- 20)]
- public void Creates_Elements_On_Item_Insert_1(double bufferFactor,
- int firstCount,
- int secondCount,
- string indexesRaw,
- int thirdCount)
- {
- using var app = App();
- var (target, _, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
- var items = (IList)itemsControl.ItemsSource!;
- Assert.Equal(firstCount, target.GetRealizedElements().Count);
- items.Insert(0, "new");
- Assert.Equal(secondCount, target.GetRealizedElements().Count);
- var indexes = GetRealizedIndexes(target, itemsControl);
- // Blank space inserted in realized elements and subsequent indexes updated.
- Assert.Equal(indexesRaw.Split(", ").Select(Int32.Parse).ToArray(), indexes);
- var elements = target.GetRealizedElements().ToList();
- Layout(target);
- indexes = GetRealizedIndexes(target, itemsControl);
- // After layout an element for the new element is created.
- Assert.Equal(Enumerable.Range(0, thirdCount), indexes);
- // But apart from the new element and the removed last element, all existing elements
- // should be the same.
- elements[0] = target.GetRealizedElements().ElementAt(0);
- elements.RemoveAt(elements.Count - 1);
- Assert.Equal(elements, target.GetRealizedElements());
- }
- [Theory]
- [InlineData(0d,
- 10,
- 11,
- "0, 1, -1, 3, 4, 5, 6, 7, 8, 9, 10",
- 10)]
- [InlineData(0.5d,
- 20,
- 21,
- "0, 1, -1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20",
- 20)]
- public void Creates_Elements_On_Item_Insert_2(double bufferFactor,
- int firstCount,
- int secondCount,
- string indexesRaw,
- int thirdCount)
- {
- using var app = App();
- var (target, _, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
- var items = (IList)itemsControl.ItemsSource!;
- Assert.Equal(firstCount, target.GetRealizedElements().Count);
- items.Insert(2, "new");
- Assert.Equal(secondCount, target.GetRealizedElements().Count);
- var indexes = GetRealizedIndexes(target, itemsControl);
- // Blank space inserted in realized elements and subsequent indexes updated.
- Assert.Equal(indexesRaw.Split(", ").Select(Int32.Parse).ToArray(), indexes);
- var elements = target.GetRealizedElements().ToList();
- Layout(target);
- indexes = GetRealizedIndexes(target, itemsControl);
- // After layout an element for the new element is created.
- Assert.Equal(Enumerable.Range(0, thirdCount), indexes);
- // But apart from the new element and the removed last element, all existing elements
- // should be the same.
- elements[2] = target.GetRealizedElements().ElementAt(2);
- elements.RemoveAt(elements.Count - 1);
- Assert.Equal(elements, target.GetRealizedElements());
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Updates_Elements_On_Item_Moved(double bufferFactor)
- {
- // Arrange
- using var app = App();
- var actualItems = new AvaloniaList<string>(Enumerable
- .Range(0, 100)
- .Select(x => $"Item {x}"));
- var (target, _, itemsControl) = CreateTarget(items: actualItems, bufferFactor:bufferFactor);
- var expectedRealizedElementContents = new[] { 1, 2, 0, 3, 4, 5, 6, 7, 8, 9 }
- .Select(x => $"Item {x}");
- // Act
- actualItems.Move(0, 2);
- Layout(target);
- // Assert
- var actualRealizedElementContents = target
- .GetRealizedElements()
- .Cast<ContentPresenter>()
- .Select(x => x.Content);
- Assert.Equivalent(expectedRealizedElementContents, actualRealizedElementContents);
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Updates_Elements_On_Item_Range_Moved(double bufferFactor)
- {
- // Arrange
- using var app = App();
- var actualItems = new AvaloniaList<string>(Enumerable
- .Range(0, 100)
- .Select(x => $"Item {x}"));
- var (target, _, itemsControl) = CreateTarget(items: actualItems, bufferFactor: bufferFactor);
- var expectedRealizedElementContents = new[] { 2, 0, 1, 3, 4, 5, 6, 7, 8, 9 }
- .Select(x => $"Item {x}");
- // Act
- actualItems.MoveRange(0, 2, 3);
- Layout(target);
- // Assert
- var actualRealizedElementContents = target
- .GetRealizedElements()
- .Cast<ContentPresenter>()
- .Select(x => x.Content);
- Assert.Equivalent(expectedRealizedElementContents, actualRealizedElementContents);
- }
- [Theory]
- [InlineData(0d, 10, 9)]
- [InlineData(0.5d, 20, 19)]
- public void Updates_Elements_On_Item_Remove(double bufferFactor, int firstCount, int secondCount)
- {
- using var app = App();
- var (target, _, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- var items = (IList)itemsControl.ItemsSource!;
- Assert.Equal(firstCount, target.GetRealizedElements().Count);
- var toRecycle = target.GetRealizedElements().ElementAt(2);
- items.RemoveAt(2);
- var indexes = GetRealizedIndexes(target, itemsControl);
- // Item removed from realized elements and subsequent row indexes updated.
- Assert.Equal(Enumerable.Range(0, secondCount), indexes);
- var elements = target.GetRealizedElements().ToList();
- Layout(target);
- indexes = GetRealizedIndexes(target, itemsControl);
- // After layout an element for the newly visible last row is created and indexes updated.
- Assert.Equal(Enumerable.Range(0, firstCount), indexes);
- // And the removed row should now have been recycled as the last row.
- elements.Add(toRecycle);
- Assert.Equal(elements, target.GetRealizedElements());
- }
- [Theory]
- [InlineData(0d, 10, "0, 1, -1, 3, 4, 5, 6, 7, 8, 9")]
- [InlineData(0.5d, 20, "0, 1, -1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19")]
- public void Updates_Elements_On_Item_Replace(double bufferFactor, int firstCount, string indexesRaw)
- {
- using var app = App();
- var (target, _, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- var items = (ObservableCollection<string>)itemsControl.ItemsSource!;
- Assert.Equal(firstCount, target.GetRealizedElements().Count);
- var toReplace = target.GetRealizedElements().ElementAt(2);
- items[2] = "new";
- // Container being replaced should have been recycled.
- Assert.DoesNotContain(toReplace, target.GetRealizedElements());
- Assert.False(toReplace!.IsVisible);
- var indexes = GetRealizedIndexes(target, itemsControl);
- // Item removed from realized elements at old position and space inserted at new position.
- Assert.Equal(indexesRaw.Split(", ").Select(Int32.Parse).ToArray(), indexes);
- Layout(target);
- indexes = GetRealizedIndexes(target, itemsControl);
- // After layout the missing container should have been created.
- Assert.Equal(Enumerable.Range(0, firstCount), indexes);
- }
- [Theory]
- [InlineData(0d, 10, "0, 1, 2, 3, 4, 5, -1, 7, 8, 9")]
- [InlineData(0.5d, 20, "0, 1, 2, 3, 4, 5, -1, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19")]
- public void Updates_Elements_On_Item_Move(double bufferFactor, int firstCount, string indexesRaw)
- {
- using var app = App();
- var (target, _, itemsControl) = CreateTarget(bufferFactor:bufferFactor);
- var items = (ObservableCollection<string>)itemsControl.ItemsSource!;
- Assert.Equal(firstCount, target.GetRealizedElements().Count);
- var toMove = target.GetRealizedElements().ElementAt(2);
- items.Move(2, 6);
- // Container being moved should have been recycled.
- Assert.DoesNotContain(toMove, target.GetRealizedElements());
- Assert.False(toMove!.IsVisible);
- var indexes = GetRealizedIndexes(target, itemsControl);
- // Item removed from realized elements at old position and space inserted at new position.
- Assert.Equal(indexesRaw.Split(", ").Select(Int32.Parse).ToArray(), indexes);
- Layout(target);
- indexes = GetRealizedIndexes(target, itemsControl);
- // After layout the missing container should have been created.
- Assert.Equal(Enumerable.Range(0, firstCount), indexes);
- }
-
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Removes_Control_Items_From_Panel_On_Item_Remove(double bufferFactor)
- {
- using var app = App();
- var items = new ObservableCollection<Button>(Enumerable.Range(0, 100).Select(x => new Button { Width = 25, Height = 10 }));
- var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: null, bufferFactor:bufferFactor);
- Assert.Equal(1000, scroll.Extent.Height);
- var removed = items[1];
- items.RemoveAt(1);
- Assert.Null(removed.Parent);
- Assert.Null(removed.VisualParent);
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Does_Not_Recycle_Focused_Element(double bufferFactor)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- var focused = target.GetRealizedElements().First()!;
- focused.Focusable = true;
- focused.Focus();
- Assert.True(target.GetRealizedElements().First()!.IsKeyboardFocusWithin);
- scroll.Offset = new Vector(0, 200);
- Layout(target);
- Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin));
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Removing_Item_Of_Focused_Element_Clears_Focus(double bufferFactor)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- var items = (IList)itemsControl.ItemsSource!;
- var focused = target.GetRealizedElements().First()!;
- focused.Focusable = true;
- focused.Focus();
- Assert.True(focused.IsKeyboardFocusWithin);
- Assert.Equal(focused, KeyboardNavigation.GetTabOnceActiveElement(itemsControl));
- scroll.Offset = new Vector(0, 200);
- Layout(target);
- items.RemoveAt(0);
- Assert.All(target.GetRealizedElements(), x => Assert.False(x!.IsKeyboardFocusWithin));
- Assert.All(target.GetRealizedElements(), x => Assert.NotSame(focused, x));
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Scrolling_Back_To_Focused_Element_Uses_Correct_Element(double bufferFactor)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- var focused = target.GetRealizedElements().First()!;
- focused.Focusable = true;
- focused.Focus();
- Assert.True(focused.IsKeyboardFocusWithin);
- scroll.Offset = new Vector(0, 200);
- Layout(target);
- scroll.Offset = new Vector(0, 0);
- Layout(target);
- Assert.Same(focused, target.GetRealizedElements().First());
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Focusing_Another_Element_Recycles_Original_Focus_Element(double bufferFactor)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- var originalFocused = target.GetRealizedElements().First()!;
- originalFocused.Focusable = true;
- originalFocused.Focus();
- scroll.Offset = new Vector(0, 500);
- Layout(target);
- var newFocused = target.GetRealizedElements().First()!;
- newFocused.Focusable = true;
- newFocused.Focus();
- Assert.False(originalFocused.IsVisible);
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Focused_Element_Losing_Focus_Does_Not_Reset_Selection(double bufferFactor)
- {
- using var app = App();
- var (target, scroll, listBox) = CreateTarget<ListBox, VirtualizingStackPanel>(
- styles: new[]
- {
- new Style(x => x.OfType<ListBoxItem>())
- {
- Setters =
- {
- new Setter(ListBoxItem.TemplateProperty, ListBoxItemTemplate()),
- }
- }
- }, bufferFactor: bufferFactor);
- listBox.SelectedIndex = 0;
- var selectedContainer = target.GetRealizedElements().First()!;
- selectedContainer.Focusable = true;
- selectedContainer.Focus();
- scroll.Offset = new Vector(0, 500);
- Layout(target);
- var newFocused = target.GetRealizedElements().First()!;
- newFocused.Focusable = true;
- newFocused.Focus();
- Assert.Equal(0, listBox.SelectedIndex);
- }
- [Theory]
- [InlineData(0d, 90, 10, 10)]
- [InlineData(0.5d, 80, 0, 20)]
- public void Removing_Range_When_Scrolled_To_End_Updates_Viewport(double bufferFactor, int firstIndex, int secondIndex, int count)
- {
- using var app = App();
- var items = new AvaloniaList<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
- var (target, scroll, itemsControl) = CreateTarget(items: items, bufferFactor: bufferFactor);
- scroll.Offset = new Vector(0, 900);
- Layout(target);
- AssertRealizedItems(target, itemsControl, firstIndex, count);
- items.RemoveRange(0, 80);
- Layout(target);
- AssertRealizedItems(target, itemsControl, secondIndex, count);
- Assert.Equal(new Vector(0, 100), scroll.Offset);
- }
- [Theory]
- [InlineData(0d, 90, 10)]
- [InlineData(0.5d, 80, 20)]
- public void Removing_Range_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport(double bufferFactor, int firstIndex, int count)
- {
- using var app = App();
- var items = new AvaloniaList<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
- var (target, scroll, itemsControl) = CreateTarget(items: items, bufferFactor: bufferFactor);
- scroll.Offset = new Vector(0, 900);
- Layout(target);
- AssertRealizedItems(target, itemsControl, firstIndex, count);
- items.RemoveRange(0, 95);
- Layout(target);
- AssertRealizedItems(target, itemsControl, 0, 5);
- Assert.Equal(new Vector(0, 0), scroll.Offset);
- }
- [Theory]
- [InlineData(0d, 90, 10, 10)]
- [InlineData(0.5d, 80,0, 20)]
- public void Resetting_Collection_To_Have_Less_Items_When_Scrolled_To_End_Updates_Viewport(double bufferFactor, int firstIndex, int secondIndex, int count)
- {
- using var app = App();
- var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
- var (target, scroll, itemsControl) = CreateTarget(items: items, bufferFactor: bufferFactor);
- scroll.Offset = new Vector(0, 900);
- Layout(target);
- AssertRealizedItems(target, itemsControl, firstIndex, count);
- items.Reset(Enumerable.Range(0, 20).Select(x => $"Item {x}"));
- Layout(target);
- AssertRealizedItems(target, itemsControl, secondIndex, count);
- Assert.Equal(new Vector(0, 100), scroll.Offset);
- }
- [Theory]
- [InlineData(0d, 90, 10)]
- [InlineData(0.5d, 80, 20)]
- public void Resetting_Collection_To_Have_Less_Than_A_Page_Of_Items_When_Scrolled_To_End_Updates_Viewport(double bufferFactor, int firstIndex, int count)
- {
- using var app = App();
- var items = new ResettingCollection(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
- var (target, scroll, itemsControl) = CreateTarget(items: items, bufferFactor: bufferFactor);
- scroll.Offset = new Vector(0, 900);
- Layout(target);
- AssertRealizedItems(target, itemsControl, firstIndex, count);
- items.Reset(Enumerable.Range(0, 5).Select(x => $"Item {x}"));
- Layout(target);
- AssertRealizedItems(target, itemsControl, 0, 5);
- Assert.Equal(new Vector(0, 0), scroll.Offset);
- }
- [Theory]
- [InlineData(0d, 10, "4,9")]
- [InlineData(0.5d, 20, "4,9,14,19")]
- public void NthChild_Selector_Works(double bufferFactor, int count, string indexesRaw)
- {
- using var app = App();
- var style = new Style(x => x.OfType<ContentPresenter>().NthChild(5, 0))
- {
- Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
- };
- var (target, _, _) = CreateTarget(styles: new[] { style }, bufferFactor: bufferFactor);
- var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
- Assert.Equal(count, realized.Count);
- for (var i = 0; i < count; ++i)
- {
- var container = realized[i];
- var index = target.IndexFromContainer(container);
- var redIndexes = indexesRaw.Split(",").Select(Int32.Parse).ToArray();
- var expectedBackground = redIndexes.Contains(i) ? Brushes.Red : null;
- Assert.Equal(i, index);
- Assert.Equal(expectedBackground, container.Background);
- }
- }
- // https://github.com/AvaloniaUI/Avalonia/issues/12838
- [Theory]
- [InlineData(0d, 10, "4,9")]
- [InlineData(0.5d, 20, "4,9,14,19")]
- public void NthChild_Selector_Works_For_ItemTemplate_Children(double bufferFactor, int count, string indexesRaw)
- {
- using var app = App();
- var style = new Style(x => x.OfType<ContentPresenter>().NthChild(5, 0).Child().OfType<Canvas>())
- {
- Setters = { new Setter(Panel.BackgroundProperty, Brushes.Red) },
- };
- var (target, _, _) = CreateTarget(styles: new[] { style }, bufferFactor: bufferFactor);
- var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
- Assert.Equal(count, realized.Count);
- for (var i = 0; i < count; ++i)
- {
- var container = realized[i];
- var index = target.IndexFromContainer(container);
- var redIndexes = indexesRaw.Split(",").Select(Int32.Parse).ToArray();
- var expectedBackground = redIndexes.Contains(i) ? Brushes.Red : null;
- Assert.Equal(i, index);
- Assert.Equal(expectedBackground, ((Canvas)container.Child!).Background);
- }
- }
- [Theory]
- [InlineData(0d, 10, "0,5")]
- [InlineData(0.5d, 20, "0,5,10,15")]
- public void NthLastChild_Selector_Works(double bufferFactor, int count, string indexesRaw)
- {
- using var app = App();
- var style = new Style(x => x.OfType<ContentPresenter>().NthLastChild(5, 0))
- {
- Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
- };
- var (target, _, _) = CreateTarget(styles: new[] { style }, bufferFactor: bufferFactor);
- var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
- Assert.Equal(count, realized.Count);
- for (var i = 0; i < count; ++i)
- {
- var container = realized[i];
- var index = target.IndexFromContainer(container);
- var redIndexes = indexesRaw.Split(",").Select(Int32.Parse).ToArray();
- var expectedBackground = redIndexes.Contains(i) ? Brushes.Red : null;
- Assert.Equal(i, index);
- Assert.Equal(expectedBackground, container.Background);
- }
- }
- // https://github.com/AvaloniaUI/Avalonia/issues/12838
- [Theory]
- [InlineData(0d, 10, "0,5")]
- [InlineData(0.5d, 20, "0,5,10,15")]
- public void NthLastChild_Selector_Works_For_ItemTemplate_Children(double bufferFactor, int count, string indexesRaw)
- {
- using var app = App();
- var style = new Style(x => x.OfType<ContentPresenter>().NthLastChild(5, 0).Child().OfType<Canvas>())
- {
- Setters = { new Setter(Panel.BackgroundProperty, Brushes.Red) },
- };
- var (target, _, _) = CreateTarget(styles: new[] { style }, bufferFactor: bufferFactor);
- var realized = target.GetRealizedContainers()!.Cast<ContentPresenter>().ToList();
- Assert.Equal(count, realized.Count);
- for (var i = 0; i < count; ++i)
- {
- var container = realized[i];
- var index = target.IndexFromContainer(container);
- var redIndexes = indexesRaw.Split(",").Select(Int32.Parse).ToArray();
- var expectedBackground = redIndexes.Contains(i) ? Brushes.Red : null;
- Assert.Equal(i, index);
- Assert.Equal(expectedBackground, ((Canvas)container.Child!).Background);
- }
- }
- [Theory]
- [InlineData(0d, 10)]
- [InlineData(0.5d, 15)]
- public void ContainerPrepared_Is_Raised_When_Scrolling(double bufferFactor, int expectedRaised)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- var raised = 0;
- itemsControl.ContainerPrepared += (s, e) => ++raised;
- scroll.Offset = new Vector(0, 200);
- Layout(target);
- Assert.Equal(expectedRaised, raised);
- }
- [Theory]
- [InlineData(0d, 10)]
- [InlineData(0.5d, 15)]
- public void ContainerClearing_Is_Raised_When_Scrolling(double bufferFactor, int expectedRaised)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- var raised = 0;
- itemsControl.ContainerClearing += (s, e) => ++raised;
- scroll.Offset = new Vector(0, 200);
- Layout(target);
- Assert.Equal(expectedRaised, raised);
- }
- [Theory]
- [InlineData(0d, 9)]
- [InlineData(0.5d, 19)]
- public void ContainerIndexChanged_Is_Raised_On_Insert(double bufferFactor, int expectedRaised)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- var items = (IList)itemsControl.ItemsSource!;
- var raised = 0;
- var index = 1;
- itemsControl.ContainerIndexChanged += (s, e) =>
- {
- ++raised;
- Assert.Equal(index, e.OldIndex);
- Assert.Equal(++index, e.NewIndex);
- };
- items.Insert(index, "new");
- Assert.Equal(expectedRaised, raised);
- }
- [Theory]
- [InlineData(0d, 10, 20)]
- [InlineData(0.5d, 20, 15)]
- public void ContainerIndexChanged_Is_Raised_When_Item_Inserted_Before_Realized_Elements(double bufferFactor, int expectedRaised, int index)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- var items = (IList)itemsControl.ItemsSource!;
- var raised = 0;
- itemsControl.ContainerIndexChanged += (s, e) =>
- {
- ++raised;
- Assert.Equal(index, e.OldIndex);
- Assert.Equal(++index, e.NewIndex);
- };
- scroll.Offset = new Vector(0, 200);
- Layout(target);
- items.Insert(10, "new");
- Assert.Equal(expectedRaised, raised);
- }
- [Theory]
- [InlineData(0d, 8)]
- [InlineData(0.5d, 18)]
- public void ContainerIndexChanged_Is_Raised_On_Remove(double bufferFactor, int expectedRaised)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- var items = (IList)itemsControl.ItemsSource!;
- var raised = 0;
- var index = 1;
- itemsControl.ContainerIndexChanged += (s, e) =>
- {
- ++raised;
- Assert.Equal(index + 1, e.OldIndex);
- Assert.Equal(index++, e.NewIndex);
- };
- items.RemoveAt(index);
- Assert.Equal(expectedRaised, raised);
- }
- [Theory]
- [InlineData(0d, 10, 20)]
- [InlineData(0.5d, 20, 15)]
- public void ContainerIndexChanged_Is_Raised_When_Item_Removed_Before_Realized_Elements(double bufferFactor, int expectedRaised, int index)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- var items = (IList)itemsControl.ItemsSource!;
- var raised = 0;
- itemsControl.ContainerIndexChanged += (s, e) =>
- {
- Assert.Equal(index, e.OldIndex);
- Assert.Equal(index - 1, e.NewIndex);
- ++index;
- ++raised;
- };
- scroll.Offset = new Vector(0, 200);
- Layout(target);
- items.RemoveAt(10);
- Assert.Equal(expectedRaised, raised);
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Fires_Correct_Container_Lifecycle_Events_On_Replace(double bufferFactor)
- {
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- var items = (IList)itemsControl.ItemsSource!;
- var events = new List<string>();
- itemsControl.ContainerPrepared += (s, e) => events.Add($"Prepared #{e.Container.GetHashCode()} = {e.Index}");
- itemsControl.ContainerClearing += (s, e) => events.Add($"Clearing #{e.Container.GetHashCode()}");
- itemsControl.ContainerIndexChanged += (s, e) => events.Add($"IndexChanged #{e.Container.GetHashCode()} {e.OldIndex} -> {e.NewIndex}");
- var toReplace = target.GetRealizedElements().ElementAt(2)!;
- items[2] = "New Item";
- Assert.Equal(
- new[] { $"Clearing #{toReplace.GetHashCode()}" },
- events);
- events.Clear();
- itemsControl.UpdateLayout();
- Assert.Equal(
- new[] { $"Prepared #{toReplace.GetHashCode()} = 2" },
- events);
- events.Clear();
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Scrolling_Down_With_Larger_Element_Does_Not_Cause_Jump_And_Arrives_At_End(double bufferFactor)
- {
- using var app = App();
- var items = Enumerable.Range(0, 1000).Select(x => new ItemWithHeight(x)).ToList();
- items[20].Height = 200;
- var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate, bufferFactor: bufferFactor);
- var index = target.FirstRealizedIndex;
- // Scroll down to the larger element.
- while (target.LastRealizedIndex < items.Count - 1)
- {
- scroll.LineDown();
- Layout(target);
- Assert.True(
- target.FirstRealizedIndex >= index,
- $"{target.FirstRealizedIndex} is not greater or equal to {index}");
- if (scroll.Offset.Y + scroll.Viewport.Height == scroll.Extent.Height)
- Assert.Equal(items.Count - 1, target.LastRealizedIndex);
- index = target.FirstRealizedIndex;
- }
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Scrolling_Up_To_Larger_Element_Does_Not_Cause_Jump(double bufferFactor)
- {
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x)).ToList();
- items[20].Height = 200;
- var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate, bufferFactor: bufferFactor);
- // Scroll past the larger element.
- scroll.Offset = new Vector(0, 600);
- Layout(target);
- // Precondition checks
- Assert.True(target.FirstRealizedIndex > 20);
- var index = target.FirstRealizedIndex;
- // Scroll up to the top.
- while (scroll.Offset.Y > 0)
- {
- scroll.LineUp();
- Layout(target);
- Assert.True(target.FirstRealizedIndex <= index, $"{target.FirstRealizedIndex} is not less than {index}");
- index = target.FirstRealizedIndex;
- }
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Scrolling_Up_To_Smaller_Element_Does_Not_Cause_Jump(double bufferFactor)
- {
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x, 30)).ToList();
- items[20].Height = 25;
- var (target, scroll, itemsControl) = CreateTarget(items: items,
- itemTemplate: CanvasWithHeightTemplate,
- bufferFactor: bufferFactor);
- var additionalItemsCount = bufferFactor == 0d
- ? 1
- // buffer factor of 0.5 and 7 visible items => will be rounded up to 4
- // => when we scroll up and are near the _extended_ viewport,
- // 4 additional items will be inserted above the current viewport
- : Math.Round(target.Children.Count * target.CacheLength, MidpointRounding.AwayFromZero);
- // Scroll past the larger element.
- scroll.Offset = new Vector(0, 25 * items[0].Height);
- Layout(target);
- // Precondition checks
- Assert.True(target.FirstRealizedIndex > 20);
- var index = target.FirstRealizedIndex;
- // Scroll up to the top.
- while (scroll.Offset.Y > 0)
- {
- scroll.Offset = scroll.Offset - new Vector(0, 5);
- Layout(target);
- Assert.True(
- target.FirstRealizedIndex <= index,
- $"{target.FirstRealizedIndex} is not less than {index}");
- Assert.True(
- index - target.FirstRealizedIndex <= additionalItemsCount,
- $"FirstIndex changed from {index} to {target.FirstRealizedIndex}");
- index = target.FirstRealizedIndex;
- }
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Does_Not_Throw_When_Estimating_Viewport_With_Ancestor_Margin(double bufferFactor)
- {
- // Issue #11272
- using var app = App();
- var (_, _, itemsControl) = CreateUnrootedTarget<ItemsControl>(bufferFactor: bufferFactor);
- var container = new Decorator { Margin = new Thickness(100) };
- var root = new TestRoot(true, container);
- root.LayoutManager.ExecuteInitialLayoutPass();
- container.Child = itemsControl;
- root.LayoutManager.ExecuteLayoutPass();
- }
- [Theory]
- [InlineData(0d, 20)]
- [InlineData(0.5d, 200)]
- public void Supports_Null_Recycle_Key_When_Scrolling(double bufferFactor, int offset)
- {
- using var app = App();
- var (_, scroll, itemsControl) = CreateUnrootedTarget<NonRecyclingItemsControl>(bufferFactor: bufferFactor);
- var root = CreateRoot(itemsControl);
- root.LayoutManager.ExecuteInitialLayoutPass();
- var firstItem = itemsControl.ContainerFromIndex(0)!;
- scroll.Offset = new(0, offset);
- Layout(itemsControl);
- Assert.Null(firstItem.Parent);
- Assert.Null(firstItem.VisualParent);
- Assert.DoesNotContain(firstItem, itemsControl.ItemsPanelRoot!.Children);
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Supports_Null_Recycle_Key_When_Clearing_Items(double bufferFactor)
- {
- using var app = App();
- var (_, _, itemsControl) = CreateUnrootedTarget<NonRecyclingItemsControl>(bufferFactor: bufferFactor);
- var root = CreateRoot(itemsControl);
- root.LayoutManager.ExecuteInitialLayoutPass();
- var firstItem = itemsControl.ContainerFromIndex(0)!;
- itemsControl.ItemsSource = null;
- Layout(itemsControl);
- Assert.Null(firstItem.Parent);
- Assert.Null(firstItem.VisualParent);
- Assert.Empty(itemsControl.ItemsPanelRoot!.Children);
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void ScrollIntoView_On_Effectively_Invisible_Panel_Does_Not_Create_Ghost_Elements(double bufferFactor)
- {
- var items = new[] { "foo", "bar", "baz" };
- var (target, _, itemsControl) = CreateUnrootedTarget<ItemsControl>(items: items, bufferFactor: bufferFactor);
- var container = new Decorator { Margin = new Thickness(100), Child = itemsControl };
- var root = new TestRoot(true, container);
- root.LayoutManager.ExecuteInitialLayoutPass();
- // Clear the items and do a layout to recycle all elements.
- itemsControl.ItemsSource = null;
- root.LayoutManager.ExecuteLayoutPass();
- // Should have no realized elements and 3 unrealized elements.
- Assert.Equal(0, target.GetRealizedElements().Count);
- Assert.Equal(3, target.Children.Count);
- // Make the panel effectively invisible and set items.
- container.IsVisible = false;
- itemsControl.ItemsSource = items;
- // Try to scroll into view while effectively invisible.
- target.ScrollIntoView(0);
- // Make the panel visible and layout.
- container.IsVisible = true;
- root.LayoutManager.ExecuteLayoutPass();
- // Should have 3 realized elements and no unrealized elements.
- Assert.Equal(3, target.GetRealizedElements().Count);
- Assert.Equal(3, target.Children.Count);
- }
- // https://github.com/AvaloniaUI/Avalonia/issues/10968
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Does_Not_Realize_Items_If_Self_Outside_Viewport(double bufferFactor)
- {
- using var app = App();
- var (panel, _, itemsControl) = CreateUnrootedTarget<ItemsControl>(bufferFactor: bufferFactor);
- itemsControl.Margin = new Thickness(0.0, 200.0, 0.0, 0.0);
- var scrollContentPresenter = new ScrollContentPresenter
- {
- Width = 100,
- Height = 100,
- Content = itemsControl
- };
- var root = CreateRoot(scrollContentPresenter);
- root.LayoutManager.ExecuteInitialLayoutPass();
- Assert.Equal(1, panel.VisualChildren.Count);
- scrollContentPresenter.Content = null;
- root.LayoutManager.ExecuteLayoutPass();
- scrollContentPresenter.Content = itemsControl;
- root.LayoutManager.ExecuteLayoutPass();
- Assert.Equal(1, panel.VisualChildren.Count);
- }
- [Theory]
- [InlineData(0d, 0, 8, 1,9)]
- [InlineData(0.5d, 0, 17, 0, 17)]
- public void Alternating_Backgrounds_Should_Be_Correct_After_Scrolling(double bufferFactor,
- int firstIndex1,
- int lastIndex1,
- int firstIndex2,
- int lastIndex2)
- {
- // Issue #12381.
- static void AssertColors(VirtualizingStackPanel target)
- {
- var containers = target.GetRealizedContainers()!
- .Cast<ListBoxItem>()
- .ToList();
- for (var i = target.FirstRealizedIndex; i <= target.LastRealizedIndex; i++)
- {
- var container = Assert.IsType<ListBoxItem>(target.ContainerFromIndex(i));
- var expectedBackground = i % 2 == 0 ? Colors.Green : Colors.Red;
- var brush = Assert.IsAssignableFrom<ISolidColorBrush>(container.Background);
- Assert.Equal(expectedBackground, brush.Color);
- }
- }
- using var app = App();
- var styles = new[]
- {
- new Style(x => x.OfType<ListBoxItem>())
- {
- Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.White) },
- },
- new Style(x => x.OfType<ListBoxItem>().NthChild(2, 1))
- {
- Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Green) },
- },
- new Style(x => x.OfType<ListBoxItem>().NthChild(2, 0))
- {
- Setters = { new Setter(ListBoxItem.BackgroundProperty, Brushes.Red) },
- },
- };
- var (target, scroll, itemsControl) = CreateUnrootedTarget<ListBox>(bufferFactor: bufferFactor);
- // We need to display an odd number of items to reproduce the issue.
- var root = CreateRoot(itemsControl, clientSize: new(100, 90), styles: styles);
- root.LayoutManager.ExecuteInitialLayoutPass();
- var containers = target.GetRealizedContainers()!
- .Cast<ListBoxItem>()
- .ToList();
- Assert.Equal(firstIndex1, target.FirstRealizedIndex);
- Assert.Equal(lastIndex1, target.LastRealizedIndex);
- AssertColors(target);
- scroll.Offset = new Vector(0, 10);
- target.UpdateLayout();
- Assert.Equal(firstIndex2, target.FirstRealizedIndex);
- Assert.Equal(lastIndex2, target.LastRealizedIndex);
- AssertColors(target);
- }
- [Theory]
- [InlineData(0d, 20)]
- [InlineData(0.5d, 15)]
- public void Inserting_Item_Before_Viewport_Preserves_FirstRealizedIndex(double bufferFactor, int firstIndex)
- {
- // Issue #12744
- using var app = App();
- var (target, scroll, itemsControl) = CreateTarget(bufferFactor: bufferFactor);
- var items = (IList)itemsControl.ItemsSource!;
- // Scroll down 20 items.
- scroll.Offset = new Vector(0, 200);
- target.UpdateLayout();
- Assert.Equal(firstIndex, target.FirstRealizedIndex);
- // Insert an item at the beginning.
- items.Insert(0, "New Item");
- target.UpdateLayout();
- // The first realized index should still be 20 as the scroll should be unchanged.
- Assert.Equal(firstIndex, target.FirstRealizedIndex);
- Assert.Equal(new(0, 200), scroll.Offset);
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Can_Bind_Item_IsVisible(double bufferFactor)
- {
- using var app = App();
- var style = CreateIsVisibleBindingStyle();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithIsVisible(x)).ToList();
- var (target, scroll, itemsControl) = CreateTarget(items: items, styles: new[] { style }, bufferFactor: bufferFactor);
- var container = target.ContainerFromIndex(2)!;
- Assert.True(container.IsVisible);
- Assert.Equal(20, container.Bounds.Top);
- items[2].IsVisible = false;
- Layout(target);
- Assert.False(container.IsVisible);
- // Next container should be in correct position.
- Assert.Equal(20, target.ContainerFromIndex(3)!.Bounds.Top);
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void IsVisible_Binding_Persists_After_Scrolling(double bufferFactor)
- {
- using var app = App();
- var style = CreateIsVisibleBindingStyle();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithIsVisible(x)).ToList();
- var (target, scroll, itemsControl) = CreateTarget(items: items, styles: new[] { style }, bufferFactor: bufferFactor);
- var container = target.ContainerFromIndex(2)!;
- Assert.True(container.IsVisible);
- Assert.Equal(20, container.Bounds.Top);
- items[2].IsVisible = false;
- scroll.Offset = new Vector(0, 200);
- Layout(target);
- scroll.Offset = new Vector(0, 0);
- Layout(target);
- container = target.ContainerFromIndex(2)!;
- Assert.False(container.IsVisible);
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void Recycling_A_Hidden_Control_Shows_It(double bufferFactor)
- {
- using var app = App();
- var style = CreateIsVisibleBindingStyle();
- var itemsList = Enumerable.Range(0, 3).Select(x => new ItemWithIsVisible(x)).ToList();
- var items = new ObservableCollection<ItemWithIsVisible>(itemsList);
- var (target, scroll, itemsControl) = CreateTarget(items: items, styles: new[] { style }, bufferFactor: bufferFactor);
- var container = target.ContainerFromIndex(2)!;
- Assert.True(container.IsVisible);
- Assert.Equal(20, container.Bounds.Top);
- items[2].IsVisible = false;
- Layout(target);
- Assert.False(container.IsVisible);
- items.RemoveAt(2);
- items.Add(new ItemWithIsVisible(3));
- Layout(target);
- Assert.True(container.IsVisible);
- }
- [Theory]
- [InlineData(0d)]
- [InlineData(0.5d)]
- public void ScrollIntoView_With_TargetRect_Outside_Viewport_Should_Scroll_To_Item(double bufferFactor)
- {
- using var app = App();
- var items = Enumerable.Range(0, 101).Select(x => new ItemWithHeight(x, x * 100 + 1));
- var itemTemplate = new FuncDataTemplate<ItemWithHeight>((x, _) =>
- new Border
- {
- Height = 10,
- [!Layoutable.WidthProperty] = new Binding("Height"),
- });
- var (target, scroll, itemsControl) = CreateTarget(
- items: items,
- itemTemplate: itemTemplate,
- styles: new[]
- {
- new Style(x => x.OfType<ScrollViewer>())
- {
- Setters =
- {
- new Setter(ScrollViewer.HorizontalScrollBarVisibilityProperty, ScrollBarVisibility.Visible),
- }
- }
- },
- bufferFactor: bufferFactor);
- itemsControl.ContainerPrepared += (_, ev) =>
- {
- ev.Container.AddHandler(Control.RequestBringIntoViewEvent, (_, e) =>
- {
- var dataContext = (ItemWithHeight)e.TargetObject!.DataContext!;
- e.TargetRect = new Rect(dataContext.Height - 50, 0, 50, 10);
- });
- };
- target.ScrollIntoView(100);
- Assert.Equal(9901, scroll.Offset.X);
- }
- [Theory]
- [InlineData(0d, 10, 10)]
- [InlineData(0.5d, 5, 15)]
- public void ScrollIntoView_Correctly_Scrolls_Down_To_A_Page_Of_Smaller_Items(double bufferFactor, int firstIndex, int count)
- {
- using var app = App();
- // First 10 items have height of 20, next 10 have height of 10.
- var items = Enumerable.Range(0, 20).Select(x => new ItemWithHeight(x, ((29 - x) / 10) * 10));
- var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate, bufferFactor: bufferFactor);
- // Scroll the last item into view.
- target.ScrollIntoView(19);
- // At the time of the scroll, the average item height is 20, so the requested item
- // should be placed at 380 (19 * 20) which therefore results in an extent of 390 to
- // accommodate the item height of 10. This is obviously not a perfect answer, but
- // it's the best we can do without knowing the actual item heights.
- var container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(19));
- Assert.Equal(new Rect(0, 380, 100, 10), container.Bounds);
- Assert.Equal(new Size(100, 100), scroll.Viewport);
- Assert.Equal(new Size(100, 390), scroll.Extent);
- Assert.Equal(new Vector(0, 290), scroll.Offset);
- // Items 10-19 should be visible.
- AssertRealizedItems(target, itemsControl, firstIndex, count);
- }
- [Theory]
- [InlineData(0d, 15, 5, 190, 210, 110)]
- [InlineData(0.5d, 10, 10, 253, 273, 173)]
- public void ScrollIntoView_Correctly_Scrolls_Down_To_A_Page_Of_Larger_Items(double bufferFactor, int firstIndex, int count, int y, int extentHeight, int offset)
- {
- using var app = App();
- // First 10 items have height of 10, next 10 have height of 20.
- var items = Enumerable.Range(0, 20).Select(x => new ItemWithHeight(x, ((x / 10) + 1) * 10));
- var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate, bufferFactor: bufferFactor);
- // Scroll the last item into view.
- target.ScrollIntoView(19);
- // At the time of the scroll, the average item height is 10, so the requested item
- // should be placed at 190 (19 * 10) which therefore results in an extent of 210 to
- // accommodate the item height of 20. This is obviously not a perfect answer, but
- // it's the best we can do without knowing the actual item heights.
- var container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(19));
- Assert.Equal(new Rect(0, y, 100, 20), container.Bounds);
- Assert.Equal(new Size(100, 100), scroll.Viewport);
- Assert.Equal(new Size(100, extentHeight), scroll.Extent);
- Assert.Equal(new Vector(0, offset), scroll.Offset);
- // Items 15-19 should be visible.
- AssertRealizedItems(target, itemsControl, firstIndex, count);
- }
- [Theory]
- [InlineData(0d, 10,10)]
- [InlineData(0.5d, 5, 15)]
- public void ScrollIntoView_Correctly_Scrolls_Right_To_A_Page_Of_Smaller_Items(double bufferFactor, int firstIndex, int count)
- {
- using var app = App();
- // First 10 items have width of 20, next 10 have width of 10.
- var items = Enumerable.Range(0, 20).Select(x => new ItemWithWidth(x, ((29 - x) / 10) * 10));
- var (target, scroll, itemsControl) = CreateTarget(items: items,
- itemTemplate: CanvasWithWidthTemplate,
- orientation: Orientation.Horizontal,
- bufferFactor: bufferFactor);
- // Scroll the last item into view.
- target.ScrollIntoView(19);
- // At the time of the scroll, the average item width is 20, so the requested item
- // should be placed at 380 (19 * 20) which therefore results in an extent of 390 to
- // accommodate the item width of 10. This is obviously not a perfect answer, but
- // it's the best we can do without knowing the actual item widths.
- var container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(19));
- Assert.Equal(new Rect(380, 0, 10, 100), container.Bounds);
- Assert.Equal(new Size(100, 100), scroll.Viewport);
- Assert.Equal(new Size(390, 100), scroll.Extent);
- Assert.Equal(new Vector(290, 0), scroll.Offset);
- // Items 10-19 should be visible.
- AssertRealizedItems(target, itemsControl, firstIndex, count);
- }
- [Theory]
- [InlineData(0d, 15, 5, 190, 210, 110)]
- [InlineData(0.5d, 10, 10, 253, 273, 173)]
- public void ScrollIntoView_Correctly_Scrolls_Right_To_A_Page_Of_Larger_Items(double bufferFactor, int firstIndex, int count, int x, int extentWidth, int offset)
- {
- using var app = App();
- // First 10 items have width of 10, next 10 have width of 20.
- var items = Enumerable.Range(0, 20).Select(x => new ItemWithWidth(x, ((x / 10) + 1) * 10));
- var (target, scroll, itemsControl) = CreateTarget(items: items,
- itemTemplate: CanvasWithWidthTemplate,
- orientation: Orientation.Horizontal,
- bufferFactor: bufferFactor);
- // Scroll the last item into view.
- target.ScrollIntoView(19);
- // At the time of the scroll, the average item width is 10, so the requested item
- // should be placed at 190 (19 * 10) which therefore results in an extent of 210 to
- // accommodate the item width of 20. This is obviously not a perfect answer, but
- // it's the best we can do without knowing the actual item widths.
- var container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(19));
- Assert.Equal(new Rect(x, 0, 20, 100), container.Bounds);
- Assert.Equal(new Size(100, 100), scroll.Viewport);
- Assert.Equal(new Size(extentWidth, 100), scroll.Extent);
- Assert.Equal(new Vector(offset, 0), scroll.Offset);
- // Items 15-19 should be visible.
- AssertRealizedItems(target, itemsControl, firstIndex, count);
- }
- [Theory]
- [InlineData(0d,
- 4,5,
- 8, 11)]
- [InlineData(0.5d,
- 3,6,
- 6, 13)]
- public void Extent_And_Offset_Should_Be_Updated_When_Containers_Resize(double bufferFactor,
- int firstIndex1, int lastIndex1,
- int firstIndex2, int lastIndex2)
- {
- using var app = App();
- // All containers start off with a height of 50 (2 containers fit in viewport).
- var items = Enumerable.Range(0, 20).Select(x => new ItemWithHeight(x, 50)).ToList();
- var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate, bufferFactor: bufferFactor);
- // Scroll to the 5th item (containers 4 and 5 should be visible).
- target.ScrollIntoView(5);
- Assert.Equal(firstIndex1, target.FirstRealizedIndex);
- Assert.Equal(lastIndex1, target.LastRealizedIndex);
- // The extent should be 500 (10 * 50) and the offset should be 200 (4 * 50).
- var container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(5));
- Assert.Equal(new Rect(0, 250, 100, 50), container.Bounds);
- Assert.Equal(new Size(100, 100), scroll.Viewport);
- Assert.Equal(new Size(100, 1000), scroll.Extent);
- Assert.Equal(new Vector(0, 200), scroll.Offset);
- // Update the height of all items to 25 and run a layout pass.
- foreach (var item in items)
- item.Height = 25;
- target.UpdateLayout();
- // The extent should be updated to reflect the new heights. The offset should be
- // unchanged but the first realized index should be updated to 8 (200 / 25).
- Assert.Equal(new Size(100, 100), scroll.Viewport);
- Assert.Equal(new Size(100, 500), scroll.Extent);
- Assert.Equal(new Vector(0, 200), scroll.Offset);
- Assert.Equal(firstIndex2, target.FirstRealizedIndex);
- Assert.Equal(lastIndex2, target.LastRealizedIndex);
- }
- [Theory]
- [InlineData(0d,
- 4, 5,
- 8, 11)]
- [InlineData(0.5d,
- 3, 6,
- 6, 13)]
- public void Focused_Container_Is_Positioned_Correctly_when_Container_Size_Change_Causes_It_To_Be_Moved_Out_Of_Visible_Viewport(double bufferFactor,
- int firstIndex1, int lastIndex1,
- int firstIndex2, int lastIndex2)
- {
- using var app = App();
- // All containers start off with a height of 50 (2 containers fit in viewport).
- var items = Enumerable.Range(0, 20).Select(x => new ItemWithHeight(x, 50)).ToList();
- var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate, bufferFactor: bufferFactor);
- // Scroll to the 5th item (containers 4 and 5 should be visible).
- target.ScrollIntoView(5);
- Assert.Equal(firstIndex1, target.FirstRealizedIndex);
- Assert.Equal(lastIndex1, target.LastRealizedIndex);
- // Focus the 5th item.
- var container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(5));
- container.Focusable = true;
- container.Focus();
- // Update the height of all items to 25 and run a layout pass.
- foreach (var item in items)
- item.Height = 25;
- target.UpdateLayout();
- // The focused container should now be outside the realized range.
- Assert.Equal(firstIndex2, target.FirstRealizedIndex);
- Assert.Equal(lastIndex2, target.LastRealizedIndex);
- // The container should still exist and be positioned outside the visible viewport.
- container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(5));
- Assert.Equal(new Rect(0, 125, 100, 25), container.Bounds);
- }
- [Theory]
- [InlineData(0d,
- 4, 7,
- 3, 6,
- 3, 7)]
- [InlineData(0.5d,
- 0, 7,
- 0, 7,
- 7, 17)]
- public void Focused_Container_Is_Positioned_Correctly_when_Container_Size_Change_Causes_It_To_Be_Moved_Into_Visible_Viewport(double bufferFactor,
- int firstIndex1, int lastIndex1,
- int firstIndex2, int lastIndex2,
- int firstIndex3, int lastIndex3)
- {
- using var app = App();
- // All containers start off with a height of 25 (4 containers fit in viewport).
- var items = Enumerable.Range(0, 20).Select(x => new ItemWithHeight(x, 25)).ToList();
- var (target, scroll, itemsControl) = CreateTarget(items: items, itemTemplate: CanvasWithHeightTemplate, bufferFactor: bufferFactor);
- // Scroll to the 5th item (containers 4-7 should be visible).
- target.ScrollIntoView(7);
- Assert.Equal(firstIndex1, target.FirstRealizedIndex);
- Assert.Equal(lastIndex1, target.LastRealizedIndex);
- // Focus the 7th item.
- var container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(7));
- container.Focusable = true;
- container.Focus();
- // Scroll up to the 3rd item (containers 3-6 should still be visible).
- target.ScrollIntoView(3);
- Assert.Equal(firstIndex2, target.FirstRealizedIndex);
- Assert.Equal(lastIndex2, target.LastRealizedIndex);
- // Update the height of all items to 20 and run a layout pass.
- foreach (var item in items)
- item.Height = 20;
- target.UpdateLayout();
- // The focused container should now be inside the realized range.
- Assert.Equal(firstIndex3, target.FirstRealizedIndex);
- Assert.Equal(lastIndex3, target.LastRealizedIndex);
- // The container should be positioned correctly.
- container = Assert.IsType<ContentPresenter>(target.ContainerFromIndex(7));
- Assert.Equal(new Rect(0, 140, 100, 20), container.Bounds);
- }
- [Fact]
- public void When_Vertical_Calculates_ViewPort_At_Start_Of_List()
- {
- // Arrange
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x)).ToList();
- // Act
- var (target, scroll, itemsControl) =
- CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
- items: items,
- itemTemplate: CanvasWithHeightTemplate,
- bufferFactor:0.5d);
- // Assert
- Assert.Equal(0, target.ViewPort.Top);
- Assert.Equal(100, target.ViewPort.Bottom);
- Assert.Equal(0, target.ExtendedViewPort.Top);
- Assert.Equal(200, target.ExtendedViewPort.Bottom);
- }
- [Fact]
- public void When_Vertical_Calculates_ViewPort_At_End_Of_List()
- {
- // Arrange
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x)).ToList();
- var (target, scroll, itemsControl) =
- CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
- items: items,
- itemTemplate: CanvasWithHeightTemplate,
- bufferFactor: 0.5d);
- // Act
- scroll.Offset = new Vector(0, 910); // scroll to end
- Layout(target);
- // Assert
- Assert.Equal(900, target.ViewPort.Top);
- Assert.Equal(1000, target.ViewPort.Bottom);
- Assert.Equal(800, target.ExtendedViewPort.Top);
- Assert.Equal(1000, target.ExtendedViewPort.Bottom);
- }
- [Fact]
- public void When_Vertical_Calculates_ViewPort_In_Middle_Of_List()
- {
- // Arrange
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeight(x)).ToList();
- var (target, scroll, itemsControl) =
- CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
- items: items,
- itemTemplate: CanvasWithHeightTemplate,
- bufferFactor: 0.5d);
- // Act
- scroll.Offset = new Vector(0, 500); // scroll to end
- Layout(target);
- // Assert
- Assert.Equal(500, target.ViewPort.Top);
- Assert.Equal(600, target.ViewPort.Bottom);
- Assert.Equal(450, target.ExtendedViewPort.Top);
- Assert.Equal(650, target.ExtendedViewPort.Bottom);
- }
- [Fact]
- public void When_Horizontal_Calculates_ViewPort_At_Start_Of_List()
- {
- // Arrange
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithWidth(x)).ToList();
- // Act
- var (target, scroll, itemsControl) =
- CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
- items: items,
- itemTemplate: CanvasWithWidthTemplate,
- orientation: Orientation.Horizontal,
- bufferFactor: 0.5d);
- // Assert
- Assert.Equal(0, target.ViewPort.Left);
- Assert.Equal(100, target.ViewPort.Right);
- Assert.Equal(0, target.ExtendedViewPort.Left);
- Assert.Equal(200, target.ExtendedViewPort.Right);
- }
- [Fact]
- public void When_Horizontal_Calculates_ViewPort_At_End_Of_List()
- {
- // Arrange
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithWidth(x)).ToList();
- var (target, scroll, itemsControl) =
- CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
- items: items,
- itemTemplate: CanvasWithWidthTemplate,
- orientation: Orientation.Horizontal,
- bufferFactor: 0.5d);
- // Act
- scroll.Offset = new Vector(900, 0); // scroll to end
- Layout(target);
- // Assert
- Assert.Equal(900, target.ViewPort.Left);
- Assert.Equal(1000, target.ViewPort.Right);
- Assert.Equal(800, target.ExtendedViewPort.Left);
- Assert.Equal(1000, target.ExtendedViewPort.Right);
- }
- [Fact]
- public void When_Horizontal_Calculates_ViewPort_In_Middle_Of_List()
- {
- // Arrange
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithWidth(x)).ToList();
- var (target, scroll, itemsControl) =
- CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
- items: items,
- itemTemplate: CanvasWithWidthTemplate,
- orientation: Orientation.Horizontal,
- bufferFactor: 0.5d);
- // Act
- scroll.Offset = new Vector(500, 0); // scroll to end
- Layout(target);
- // Assert
- Assert.Equal(500, target.ViewPort.Left);
- Assert.Equal(600, target.ViewPort.Right);
- Assert.Equal(450, target.ExtendedViewPort.Left);
- Assert.Equal(650, target.ExtendedViewPort.Right);
- }
- [Fact]
- public void Scrolling_Down_Does_Not_Measure_Or_Arrange_Until_Extended_ViewPort_Bounds_Are_Reached()
- {
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeightAndMeasureArrangeCount(x)).ToList();
- var (target, scroll, itemsControl) =
- CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
- items: items,
- itemTemplate: CanvasWithHeightTemplate,
- bufferFactor: 0.5d);
- Assert.True(target.LastRealizedIndex == 19,
- $"Should show 20 items but last realized index was {target.LastRealizedIndex}");
- // reset counters
- target.ResetMeasureArrangeCounters();
- // shows 20 items, each is 10 high.
- // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
- // until then no measure-arrange call should happen
- // Scroll down until the extended viewport bounds are reached
- while (target.LastRealizedIndex < 20)
- {
- scroll.Offset = new Vector(0, scroll.Offset.Y + 5);
- Layout(target);
- }
- // Assert
- Assert.True(target.Measured == 1, "should be measured only once");
- Assert.True(target.Arranged == 1, "should be arranged only once");
- // the first 5 additional items will be reused when scrolling down, but the remaining 10 visible + 5 additional not touched at all
- var expectedUntouchedItems =
- items.Skip(5 /*additional items*/).Take(15).ToList();
- foreach (var itm in expectedUntouchedItems)
- {
- Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be arranged but was {itm.Arranged} times");
- }
- var newAdditionalItems = items.Skip(20).Take(5);
- foreach (var itm in newAdditionalItems)
- {
- Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 1, $"{itm.Caption} should be measured but was {itm.Arranged} times");
- }
- }
- [Fact]
- public void Scrolling_Up_Does_Not_Measure_Or_Arrange_Until_Extended_ViewPort_Bounds_Are_Reached()
- {
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeightAndMeasureArrangeCount(x)).ToList();
- var (target, scroll, itemsControl) =
- CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
- items: items,
- itemTemplate: CanvasWithHeightTemplate,
- bufferFactor: 0.5d);
- // scroll a bit down so we are not near the start of the list
- scroll.Offset = new Vector(0, 200);
- Layout(target);
- Assert.True(target.FirstRealizedIndex == 15,
- $"Should show items from 20 to 30 (so 15 to 35 including additional items) but first realized index was {target.FirstRealizedIndex}");
- // reset counters
- target.ResetMeasureArrangeCounters();
- // shows 20 items, each is 10 high.
- // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
- // until then no measure-arrange call should happen
- var initialFirstRealizedIndex = target.FirstRealizedIndex;
- // Scroll down until the extended viewport bounds are reached
- while (target.FirstRealizedIndex >= 15)
- {
- scroll.Offset = new Vector(0, scroll.Offset.Y - 5);
- Layout(target);
- }
- // Assert
- Assert.True(target.Measured == 1, "should be measured only once");
- Assert.True(target.Arranged == 1, "should be arranged only once");
- // the last 5 additional items will be reused when scrolling up, but the remaining 10 visible + 5 additional not touched at all
- var expectedUntouchedItems = items.Skip(initialFirstRealizedIndex + 1).Take(15).ToList();
- foreach (var itm in expectedUntouchedItems)
- {
- Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be arranged but was {itm.Arranged} times");
- }
- // now that we scrolled up to index 19, items 18,17,16,15 and 14 should be the "additional" ones
- var newAdditionalItems = items.Skip(initialFirstRealizedIndex - 6).Take(6);
- foreach (var itm in newAdditionalItems)
- {
- Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 1, $"{itm.Caption} should be measured but was {itm.Arranged} times");
- }
- }
- [Fact]
- public void Scrolling_Down_To_End_Of_List_Only_Measures_Once_When_Last_Item_Is_Reached()
- {
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeightAndMeasureArrangeCount(x)).ToList();
- var (target, scroll, itemsControl) =
- CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
- items: items,
- itemTemplate: CanvasWithHeightTemplate,
- bufferFactor: 0.5d);
- // scroll a bit down so we are near the end of the list
- scroll.Offset = new Vector(0, 800); // so we render 75 to 95 with a buffer size of 5
- Layout(target);
- Assert.True(target.LastRealizedIndex == 94,
- $"Should show 20 items but last realized index was {target.LastRealizedIndex}");
- // reset counters
- target.ResetMeasureArrangeCounters();
- // shows 20 items, each is 10 high.
- // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
- // until then no measure-arrange call should happen
- var initialLastRealizedIndex = target.LastRealizedIndex;
- // Scroll down until we reached the very last item
- while (target.LastRealizedIndex < 99)
- {
- scroll.Offset = new Vector(0, scroll.Offset.Y + 5);
- Layout(target);
- }
- // Assert
- Assert.True(target.Measured == 1, "should be measured only once even though we are at the end of the list");
- Assert.True(target.Arranged == 1, "should be arranged only once even though we are at the end of the list");
- // the first 5 additional items will be reused when scrolling down, but the remaining 10 visible + 5 additional not touched at all
- var expectedUntouchedItems =
- items.Skip(initialLastRealizedIndex + 1 - 15).Take(15).ToList();
- foreach (var itm in expectedUntouchedItems)
- {
- Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be arranged but was {itm.Arranged} times");
- }
- var newAdditionalItems = items.Skip(initialLastRealizedIndex + 1).Take(5);
- foreach (var itm in newAdditionalItems)
- {
- Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 1, $"{itm.Caption} should be measured but was {itm.Arranged} times");
- }
- }
- [Fact]
- public void Scrolling_Up_To_Start_Of_List_Only_Measures_Once_When_First_Item_Is_Reached()
- {
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithHeightAndMeasureArrangeCount(x)).ToList();
- var (target, scroll, itemsControl) =
- CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
- items: items,
- itemTemplate: CanvasWithHeightTemplate,
- bufferFactor: 0.5d);
- // scroll a bit down so we are not near the start of the list
- scroll.Offset = new Vector(0, 105);
- Layout(target);
- Assert.True(target.FirstRealizedIndex == 5,
- $"Should show items from 10 to 20 (so 5 to 25 including additional items) but first realized index was {target.FirstRealizedIndex}");
- // reset counters
- target.ResetMeasureArrangeCounters();
- // shows 20 items, each is 10 high.
- // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
- // until then no measure-arrange call should happen
- // Scroll down until the extended viewport bounds are reached
- while (target.FirstRealizedIndex > 0)
- {
- scroll.Offset = new Vector(0, scroll.Offset.Y - 5);
- Layout(target);
- }
- // Assert
- Assert.True(target.Measured == 1, "should be measured only once even though we are at the start of the list");
- Assert.True(target.Arranged == 1, "should be arranged only once even though we are at the start of the list");
- // the last 5 additional items will be reused when scrolling up, but the remaining 10 visible + 5 additional not touched at all
- var expectedMeasuredItems = items.Take(20).ToList();
- foreach (var itm in expectedMeasuredItems)
- {
- Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 1, $"{itm.Caption} should be arranged but was {itm.Arranged} times");
- }
- // now that we scrolled up to index 19, items 18,17,16,15 and 14 should be the "additional" ones
- var untouchedItems = items.Skip(20).ToList();
- foreach (var itm in untouchedItems)
- {
- Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be measured but was {itm.Arranged} times");
- }
- }
- [Fact]
- public void Scrolling_Right_Does_Not_Measure_Or_Arrange_Until_Extended_ViewPort_Bounds_Are_Reached()
- {
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithWidthAndMeasureArrangeCount(x)).ToList();
- var (target, scroll, itemsControl) =
- CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
- items: items,
- itemTemplate: CanvasWithWidthTemplate,
- orientation: Orientation.Horizontal,
- bufferFactor: 0.5d);
- Assert.True(target.LastRealizedIndex == 19,
- $"Should show 20 items but last realized index was {target.LastRealizedIndex}");
- // reset counters
- target.ResetMeasureArrangeCounters();
- // shows 20 items, each is 10 high.
- // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
- // until then no measure-arrange call should happen
- // Scroll down until the extended viewport bounds are reached
- while (target.LastRealizedIndex < 20)
- {
- scroll.Offset = new Vector(scroll.Offset.X + 5, 0);
- Layout(target);
- }
- // Assert
- Assert.True(target.Measured == 1, "should be measured only once");
- Assert.True(target.Arranged == 1, "should be arranged only once");
- // the first 5 additional items will be reused when scrolling down, but the remaining 10 visible + 5 additional not touched at all
- var expectedUntouchedItems =
- items.Skip(5 /*additional items*/).Take(15).ToList();
- foreach (var itm in expectedUntouchedItems)
- {
- Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be arranged but was {itm.Arranged} times");
- }
- var newAdditionalItems = items.Skip(20).Take(5);
- foreach (var itm in newAdditionalItems)
- {
- Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 1, $"{itm.Caption} should be measured but was {itm.Arranged} times");
- }
- }
- [Fact]
- public void Scrolling_Left_Does_Not_Measure_Or_Arrange_Until_Extended_ViewPort_Bounds_Are_Reached()
- {
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithWidthAndMeasureArrangeCount(x)).ToList();
- var (target, scroll, itemsControl) =
- CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
- items: items,
- itemTemplate: CanvasWithWidthTemplate,
- orientation: Orientation.Horizontal,
- bufferFactor: 0.5d);
- // scroll a bit down so we are not near the start of the list
- scroll.Offset = new Vector(200, 0);
- Layout(target);
- Assert.True(target.FirstRealizedIndex == 15,
- $"Should show items from 20 to 30 (so 15 to 35 including additional items) but first realized index was {target.FirstRealizedIndex}");
- // reset counters
- target.ResetMeasureArrangeCounters();
- // shows 20 items, each is 10 high.
- // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
- // until then no measure-arrange call should happen
- var initialFirstRealizedIndex = target.FirstRealizedIndex;
- // Scroll down until the extended viewport bounds are reached
- while (target.FirstRealizedIndex >= 15)
- {
- scroll.Offset = new Vector(scroll.Offset.X - 5, 0);
- Layout(target);
- }
- // Assert
- Assert.True(target.Measured == 1, "should be measured only once");
- Assert.True(target.Arranged == 1, "should be arranged only once");
- // the last 5 additional items will be reused when scrolling up, but the remaining 10 visible + 5 additional not touched at all
- var expectedUntouchedItems = items.Skip(initialFirstRealizedIndex + 1).Take(15).ToList();
- foreach (var itm in expectedUntouchedItems)
- {
- Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be arranged but was {itm.Arranged} times");
- }
- // now that we scrolled up to index 19, items 18,17,16,15 and 14 should be the "additional" ones
- var newAdditionalItems = items.Skip(initialFirstRealizedIndex - 6).Take(6);
- foreach (var itm in newAdditionalItems)
- {
- Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 1, $"{itm.Caption} should be measured but was {itm.Arranged} times");
- }
- }
- [Fact]
- public void Scrolling_Right_To_End_Of_List_Only_Measures_Once_When_Last_Item_Is_Reached()
- {
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithWidthAndMeasureArrangeCount(x)).ToList();
- var (target, scroll, itemsControl) =
- CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
- items: items,
- itemTemplate: CanvasWithWidthTemplate,
- orientation: Orientation.Horizontal,
- bufferFactor: 0.5d);
- // scroll a bit down so we are near the end of the list
- scroll.Offset = new Vector(800, 0); // so we render 75 to 95 with a buffer size of 5
- Layout(target);
- Assert.True(target.LastRealizedIndex == 94,
- $"Should show 20 items but last realized index was {target.LastRealizedIndex}");
- // reset counters
- target.ResetMeasureArrangeCounters();
- // shows 20 items, each is 10 high.
- // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
- // until then no measure-arrange call should happen
- var initialLastRealizedIndex = target.LastRealizedIndex;
- // Scroll down until we reached the very last item
- while (target.LastRealizedIndex < 99)
- {
- scroll.Offset = new Vector(scroll.Offset.X + 5, 0);
- Layout(target);
- }
- // Assert
- Assert.True(target.Measured == 1, "should be measured only once even though we are at the end of the list");
- Assert.True(target.Arranged == 1, "should be arranged only once even though we are at the end of the list");
- // the first 5 additional items will be reused when scrolling down, but the remaining 10 visible + 5 additional not touched at all
- var expectedUntouchedItems =
- items.Skip(initialLastRealizedIndex + 1 - 15).Take(15).ToList();
- foreach (var itm in expectedUntouchedItems)
- {
- Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be arranged but was {itm.Arranged} times");
- }
- var newAdditionalItems = items.Skip(initialLastRealizedIndex + 1).Take(5);
- foreach (var itm in newAdditionalItems)
- {
- Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 1, $"{itm.Caption} should be measured but was {itm.Arranged} times");
- }
- }
- [Fact]
- public void Scrolling_Left_To_Start_Of_List_Only_Measures_Once_When_First_Item_Is_Reached()
- {
- using var app = App();
- var items = Enumerable.Range(0, 100).Select(x => new ItemWithWidthAndMeasureArrangeCount(x)).ToList();
- var (target, scroll, itemsControl) =
- CreateTarget<ItemsControl, VirtualizingStackPanelCountingMeasureArrange>(
- items: items,
- itemTemplate: CanvasWithWidthTemplate,
- orientation: Orientation.Horizontal,
- bufferFactor: 0.5d);
- // scroll a bit down so we are not near the start of the list
- scroll.Offset = new Vector(105, 0);
- Layout(target);
- Assert.True(target.FirstRealizedIndex == 5,
- $"Should show items from 10 to 20 (so 5 to 25 including additional items) but first realized index was {target.FirstRealizedIndex}");
- // reset counters
- target.ResetMeasureArrangeCounters();
- // shows 20 items, each is 10 high.
- // visible are 10 => need to scroll down 100px until the next 5 (visible*BufferFactor) additional items are added.
- // until then no measure-arrange call should happen
- // Scroll down until the extended viewport bounds are reached
- while (target.FirstRealizedIndex > 0)
- {
- scroll.Offset = new Vector(scroll.Offset.X - 5, 0);
- Layout(target);
- }
- // Assert
- Assert.True(target.Measured == 1, "should be measured only once even though we are at the start of the list");
- Assert.True(target.Arranged == 1, "should be arranged only once even though we are at the start of the list");
- // the last 5 additional items will be reused when scrolling up, but the remaining 10 visible + 5 additional not touched at all
- var expectedMeasuredItems = items.Take(20).ToList();
- foreach (var itm in expectedMeasuredItems)
- {
- Assert.True(itm.Measured == 1, $"{itm.Caption} should be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 1, $"{itm.Caption} should be arranged but was {itm.Arranged} times");
- }
- // now that we scrolled up to index 19, items 18,17,16,15 and 14 should be the "additional" ones
- var untouchedItems = items.Skip(20).ToList();
- foreach (var itm in untouchedItems)
- {
- Assert.True(itm.Measured == 0, $"{itm.Caption} should not be measured but was {itm.Measured} times");
- Assert.True(itm.Arranged == 0, $"{itm.Caption} should not be measured but was {itm.Arranged} times");
- }
- }
- private static IReadOnlyList<int> GetRealizedIndexes(VirtualizingStackPanel target, ItemsControl itemsControl)
- {
- return target.GetRealizedElements()
- .Select(x => x is null ? -1 : itemsControl.IndexFromContainer((Control)x))
- .ToList();
- }
- private static void AssertRealizedItems(
- VirtualizingStackPanel target,
- ItemsControl itemsControl,
- int firstIndex,
- int count)
- {
- Assert.All(target.GetRealizedContainers()!, x => Assert.Same(target, x.VisualParent));
- Assert.All(target.GetRealizedContainers()!, x => Assert.Same(itemsControl, x.Parent));
- var childIndexes = target.GetRealizedContainers()!
- .Select(x => itemsControl.IndexFromContainer(x))
- .Where(x => x >= 0)
- .OrderBy(x => x)
- .ToList();
- Assert.Equal(Enumerable.Range(firstIndex, count), childIndexes);
- var visibleChildren = target.Children
- .Where(x => x.IsVisible)
- .ToList();
- Assert.Equal(count, visibleChildren.Count);
- }
- private static void AssertRealizedControlItems<TContainer>(
- VirtualizingStackPanel target,
- ItemsControl itemsControl,
- int firstIndex,
- int count)
- {
- Assert.All(target.GetRealizedContainers()!, x => Assert.IsType<TContainer>(x));
- Assert.All(target.GetRealizedContainers()!, x => Assert.Same(target, x.VisualParent));
- Assert.All(target.GetRealizedContainers()!, x => Assert.Same(itemsControl, x.Parent));
- var childIndexes = target.GetRealizedContainers()!
- .Select(x => itemsControl.IndexFromContainer(x))
- .Where(x => x >= 0)
- .OrderBy(x => x)
- .ToList();
- Assert.Equal(Enumerable.Range(firstIndex, count), childIndexes);
- }
- private static (VirtualizingStackPanel, ScrollViewer, ItemsControl) CreateTarget(
- IEnumerable<object>? items = null,
- Optional<IDataTemplate?> itemTemplate = default,
- IEnumerable<Style>? styles = null,
- Orientation orientation = Orientation.Vertical,
- double bufferFactor = 0.0d)
- {
- return CreateTarget<ItemsControl, VirtualizingStackPanel>(
- items: items,
- itemTemplate: itemTemplate,
- styles: styles,
- orientation: orientation,
- bufferFactor: bufferFactor);
- }
- private static (TStackPanel, ScrollViewer, T) CreateTarget<T, TStackPanel>(
- IEnumerable<object>? items = null,
- Optional<IDataTemplate?> itemTemplate = default,
- IEnumerable<Style>? styles = null,
- Orientation orientation = Orientation.Vertical,
- double bufferFactor = 0.0d)
- where T : ItemsControl, new()
- where TStackPanel : VirtualizingStackPanel, new()
- {
- var (target, scroll, itemsControl) = CreateUnrootedTarget<T, TStackPanel>(items, itemTemplate, orientation, bufferFactor: bufferFactor);
- var root = CreateRoot(itemsControl, styles: styles);
- root.LayoutManager.ExecuteInitialLayoutPass();
- return (target, scroll, itemsControl);
- }
- private static (VirtualizingStackPanel, ScrollViewer, T) CreateUnrootedTarget<T>(
- IEnumerable<object>? items = null,
- Optional<IDataTemplate?> itemTemplate = default,
- Orientation orientation = Orientation.Vertical,
- double bufferFactor = 0.0d)
- where T : ItemsControl, new()
- => CreateUnrootedTarget<T, VirtualizingStackPanel>(items, itemTemplate, orientation, bufferFactor);
- private static (TStackPanel, ScrollViewer, T) CreateUnrootedTarget<T, TStackPanel>(
- IEnumerable<object>? items = null,
- Optional<IDataTemplate?> itemTemplate = default,
- Orientation orientation = Orientation.Vertical,
- double bufferFactor = 0.0d)
- where T : ItemsControl, new()
- where TStackPanel : VirtualizingStackPanel, new()
- {
- var target = new TStackPanel
- {
- Orientation = orientation,
- CacheLength = bufferFactor,
- };
- items ??= new ObservableCollection<string>(Enumerable.Range(0, 100).Select(x => $"Item {x}"));
- var presenter = new ItemsPresenter
- {
- [~ItemsPresenter.ItemsPanelProperty] = new TemplateBinding(ItemsPresenter.ItemsPanelProperty),
- };
- var scroll = new ScrollViewer
- {
- Name = "PART_ScrollViewer",
- Content = presenter,
- };
- if (orientation == Orientation.Horizontal)
- {
- scroll.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
- scroll.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled;
- }
- scroll.Template = ScrollViewerTemplate();
- var itemsControl = new T
- {
- ItemsSource = items,
- Template = new FuncControlTemplate<T>((_, ns) => scroll.RegisterInNameScope(ns)),
- ItemsPanel = new FuncTemplate<Panel?>(() => target),
- ItemTemplate = itemTemplate.GetValueOrDefault(DefaultItemTemplate()),
- };
- return (target, scroll, itemsControl);
- }
- private static TestRoot CreateRoot(
- Control? child,
- Size? clientSize = null,
- IEnumerable<Style>? styles = null)
- {
- var root = new TestRoot(true, child);
- root.ClientSize = clientSize ?? new(100, 100);
- if (styles is not null)
- root.Styles.AddRange(styles);
- return root;
- }
- private static Style CreateIsVisibleBindingStyle()
- {
- return new Style(x => x.OfType<ContentPresenter>())
- {
- Setters =
- {
- new Setter(Visual.IsVisibleProperty, new Binding("IsVisible")),
- }
- };
- }
- private static IDataTemplate DefaultItemTemplate()
- {
- return new FuncDataTemplate<object>((x, _) => new Canvas { Width = 100, Height = 10 });
- }
- private static void Layout(Control target)
- {
- var root = (ILayoutRoot?)target.GetVisualRoot();
- root?.LayoutManager.ExecuteLayoutPass();
- }
- private static IControlTemplate ListBoxItemTemplate()
- {
- return new FuncControlTemplate<ListBoxItem>((x, ns) =>
- new ContentPresenter
- {
- Name = "PART_ContentPresenter",
- Width = 100,
- Height = 10,
- }.RegisterInNameScope(ns));
- }
- private static IControlTemplate ScrollViewerTemplate()
- {
- return new FuncControlTemplate<ScrollViewer>((x, ns) =>
- new ScrollContentPresenter
- {
- Name = "PART_ScrollContentPresenter",
- }.RegisterInNameScope(ns));
- }
- private static IDisposable App() => UnitTestApplication.Start(TestServices.RealFocus);
- private class ItemWithHeight : NotifyingBase
- {
- private double _height;
- public ItemWithHeight(int index, double height = 10)
- {
- Caption = $"Item {index}";
- Height = height;
- }
- public string Caption { get; set; }
- public double Height
- {
- get => _height;
- set => SetField(ref _height, value);
- }
- }
- private class ItemWithWidth : NotifyingBase
- {
- private double _width;
- public ItemWithWidth(int index, double width = 10)
- {
- Caption = $"Item {index}";
- Width = width;
- }
- public string Caption { get; set; }
- public double Width
- {
- get => _width;
- set => SetField(ref _width, value);
- }
- }
- private class ItemWithIsVisible : NotifyingBase
- {
- private bool _isVisible = true;
- public ItemWithIsVisible(int index)
- {
- Caption = $"Item {index}";
- }
- public string Caption { get; set; }
- public bool IsVisible
- {
- get => _isVisible;
- set => SetField(ref _isVisible, value);
- }
- }
- private class ResettingCollection : List<string>, INotifyCollectionChanged
- {
- public ResettingCollection(IEnumerable<string> items)
- {
- AddRange(items);
- }
- public void Reset(IEnumerable<string> items)
- {
- Clear();
- AddRange(items);
- CollectionChanged?.Invoke(
- this,
- new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
- }
- public event NotifyCollectionChangedEventHandler? CollectionChanged;
- }
- private class NonRecyclingItemsControl : ItemsControl
- {
- protected override Type StyleKeyOverride => typeof(ItemsControl);
- protected internal override bool NeedsContainerOverride(object? item, int index, out object? recycleKey)
- {
- recycleKey = null;
- return true;
- }
- }
- private interface ICountMeasureArrangeCalls
- {
- int Measured { get; set; }
- int Arranged { get; set; }
- }
- [DebuggerDisplay("{DebuggerDisplay}")]
- private class ItemWithHeightAndMeasureArrangeCount : ItemWithHeight, ICountMeasureArrangeCalls
- {
- public ItemWithHeightAndMeasureArrangeCount(int index, double height = 10) : base(index, height)
- {
- }
- public int Measured { get; set; }
- public int Arranged { get; set; }
- private string DebuggerDisplay => $"{Caption} (height: {Height} m:{Measured} a: {Arranged})";
- }
- [DebuggerDisplay("{DebuggerDisplay}")]
- private class ItemWithWidthAndMeasureArrangeCount : ItemWithWidth, ICountMeasureArrangeCalls
- {
- public ItemWithWidthAndMeasureArrangeCount(int index, double width = 10) : base(index, width)
- {
- }
- public int Measured { get; set; }
- public int Arranged { get; set; }
- private string DebuggerDisplay => $"{Caption} (width: {Width} m:{Measured} a: {Arranged})";
- }
- private class VirtualizingStackPanelCountingMeasureArrange : VirtualizingStackPanel
- {
- public int Measured { get; set; }
- public int Arranged { get; set; }
- public void ResetMeasureArrangeCounters()
- {
- // reset counters
- Measured = 0;
- Arranged = 0;
- foreach (var itm in Items.OfType<ICountMeasureArrangeCalls>())
- {
- itm.Measured = 0;
- itm.Arranged = 0;
- }
- }
- protected override Size MeasureOverride(Size availableSize)
- {
- Measured++;
- return base.MeasureOverride(availableSize);
- }
- protected override Size ArrangeOverride(Size finalSize)
- {
- Arranged++;
- return base.ArrangeOverride(finalSize);
- }
- }
- private class CanvasCountingMeasureArrangeCalls : Canvas
- {
- protected override Size MeasureOverride(Size availableSize)
- {
- if(DataContext is ICountMeasureArrangeCalls itm)
- itm.Measured++;
- return base.MeasureOverride(availableSize);
- }
- protected override Size ArrangeOverride(Size finalSize)
- {
- if(DataContext is ICountMeasureArrangeCalls itm)
- itm.Arranged++;
- return base.ArrangeOverride(finalSize);
- }
- }
- }
- }
|