Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
95 changes: 95 additions & 0 deletions src/Components/test/E2ETest/Tests/VirtualizationTest.cs
Original file line number Diff line number Diff line change
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