Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Components/Web/src/Virtualization/Virtualize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@ private void CalcualteItemDistribution(
_ => MaxItemCount
};

// Count the OverscanCount as used capacity, so we don't end up in a situation where
// the user has set a very low MaxItemCount and we end up in an infinite loading loop.
maxItemCount += OverscanCount * 2;

itemsInSpacer = Math.Max(0, (int)Math.Floor(spacerSize / _itemSize) - OverscanCount);
visibleItemCapacity = (int)Math.Ceiling(containerSize / _itemSize) + 2 * OverscanCount;
unusedItemCapacity = Math.Max(0, visibleItemCapacity - maxItemCount);
Expand Down
99 changes: 97 additions & 2 deletions src/Components/test/E2ETest/Tests/VirtualizationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,14 @@ public void CanLimitMaxItemsRendered(bool useAppContext)
// we only render 10 items due to the MaxItemCount setting
var scrollArea = Browser.Exists(By.Id("virtualize-scroll-area"));
var getItems = () => scrollArea.FindElements(By.ClassName("my-item"));
Browser.Equal(10, () => getItems().Count);
Browser.Equal(16, () => getItems().Count);
Browser.Equal("Id: 0; Name: Thing 0", () => getItems().First().Text);

// Scrolling still works and loads new data, though there's no guarantee about
// exactly how many items will show up at any one time
Browser.ExecuteJavaScript("document.getElementById('virtualize-scroll-area').scrollTop = 300;");
Browser.NotEqual("Id: 0; Name: Thing 0", () => getItems().First().Text);
Browser.True(() => getItems().Count > 3 && getItems().Count <= 10);
Browser.True(() => getItems().Count > 3 && getItems().Count <= 16);
}

[Fact]
Expand Down Expand Up @@ -573,6 +573,101 @@ public void EmptyContentRendered_Async()
int GetPlaceholderCount() => Browser.FindElements(By.Id("async-placeholder")).Count;
}

[Fact]
public void CanElevateEffectiveMaxItemCount_WhenOverscanExceedsMax()
{
Browser.MountTestComponent<VirtualizationLargeOverscan>();
var container = Browser.Exists(By.Id("virtualize-large-overscan"));
// Ensure we have an initial contiguous batch and the elevated effective max has kicked in (>= OverscanCount)
var indices = GetVisibleItemIndices();
Browser.True(() => indices.Count >= 200);

// Give focus so PageDown works
container.Click();

var js = (IJavaScriptExecutor)Browser;
var lastMaxIndex = -1;
var lastScrollTop = -1L;

// Check if we've reached (or effectively reached) the bottom
var scrollHeight = (long)js.ExecuteScript("return arguments[0].scrollHeight", container);
var clientHeight = (long)js.ExecuteScript("return arguments[0].clientHeight", container);
var scrollTop = (long)js.ExecuteScript("return arguments[0].scrollTop", container);
while (scrollTop + clientHeight < scrollHeight)
{
// Validate contiguity on the current page
Browser.True(() => IsCurrentViewContiguous(indices));

// Track progress in indices
var currentMax = indices.Max();
Assert.True(currentMax >= lastMaxIndex, $"Unexpected backward movement: previous max {lastMaxIndex}, current max {currentMax}.");
lastMaxIndex = currentMax;

// Send PageDown
container.SendKeys(Keys.PageDown);

// Wait for scrollTop to change (progress) to avoid infinite loop
var prevScrollTop = scrollTop;
Browser.True(() =>
{
var st = (long)js.ExecuteScript("return arguments[0].scrollTop", container);
if (st > prevScrollTop)
{
lastScrollTop = st;
return true;
}
return false;
});
scrollHeight = (long)js.ExecuteScript("return arguments[0].scrollHeight", container);
clientHeight = (long)js.ExecuteScript("return arguments[0].clientHeight", container);
scrollTop = (long)js.ExecuteScript("return arguments[0].scrollTop", container);
}

// Final contiguous assertion at bottom
Browser.True(() => IsCurrentViewContiguous());

// Helper: check visible items contiguous with no holes
bool IsCurrentViewContiguous(List<int> existingIndices = null)
{
var indices = existingIndices ?? GetVisibleItemIndices();
if (indices.Count == 0)
{
return false;
}

if (indices[^1] - indices[0] != indices.Count - 1)
{
return false;
}
for (var i = 1; i < indices.Count; i++)
{
if (indices[i] - indices[i - 1] != 1)
{
return false;
}
}
return true;
}

List<int> GetVisibleItemIndices()
{
var elements = container.FindElements(By.CssSelector(".large-overscan-item"));
var list = new List<int>(elements.Count);
foreach (var el in elements)
{
var text = el.Text;
if (text.StartsWith("Item ", StringComparison.Ordinal))
{
if (int.TryParse(text.AsSpan(5), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
{
list.Add(value);
}
}
}
return list;
}
}

private string[] GetPeopleNames(IWebElement container)
{
var peopleElements = container.FindElements(By.CssSelector(".person span"));
Expand Down
1 change: 1 addition & 0 deletions src/Components/test/testassets/BasicTestApp/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
<option value="BasicTestApp.VirtualizationMaxItemCount">Virtualization MaxItemCount</option>
<option value="BasicTestApp.VirtualizationMaxItemCount_AppContext">Virtualization MaxItemCount (via AppContext)</option>
<option value="BasicTestApp.VirtualizationTable">Virtualization HTML table</option>
<option value="BasicTestApp.VirtualizationLargeOverscan">Virtualization large overscan</option>
<option value="BasicTestApp.HotReload.RenderOnHotReload">Render on hot reload</option>
<option value="BasicTestApp.SectionsTest.ParentComponentWithTwoChildren">Sections test</option>
<option value="BasicTestApp.SectionsTest.SectionsWithCascadingParameters">Sections with Cascading parameters test</option>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@* Test component to validate behavior when OverscanCount greatly exceeds MaxItemCount. *@
@using Microsoft.AspNetCore.Components.Web.Virtualization

<div id="virtualize-large-overscan" style="height: 600px; overflow-y: auto; outline: 1px solid #999; background:#f8f8f8;">
<Virtualize Items="_items" ItemSize="30" MaxItemCount="100" OverscanCount="200">
<div class="large-overscan-item" @key="context" style="height:30px; line-height:30px; border-bottom:1px solid #ddd;">Item @context</div>
</Virtualize>
</div>

@code {
private IList<int> _items = Enumerable.Range(0, 5000).ToList();
}
Loading