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
2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webassembly.js

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions src/Components/Web.JS/src/Virtualize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,16 @@ function init(dotNetHelper: any, spacerBefore: HTMLElement, spacerAfter: HTMLEle
return;
}

const spacerSeparation = spacerAfter.offsetTop - (spacerBefore.offsetTop + spacerBefore.offsetHeight);
const containerSize = entry.rootBounds?.height;

if (entry.target === spacerBefore) {
dotNetHelper.invokeMethodAsync('OnSpacerBeforeVisible', entry.intersectionRect.top - entry.boundingClientRect.top, containerSize);
dotNetHelper.invokeMethodAsync('OnSpacerBeforeVisible', entry.intersectionRect.top - entry.boundingClientRect.top, spacerSeparation, containerSize);
} else if (entry.target === spacerAfter && spacerAfter.offsetHeight > 0) {
// When we first start up, both the "before" and "after" spacers will be visible, but it's only relevant to raise a
// single event to load the initial data. To avoid raising two events, skip the one for the "after" spacer if we know
// it's meaningless to talk about any overlap into it.
dotNetHelper.invokeMethodAsync('OnSpacerAfterVisible', entry.boundingClientRect.bottom - entry.intersectionRect.bottom, containerSize);
dotNetHelper.invokeMethodAsync('OnSpacerAfterVisible', entry.boundingClientRect.bottom - entry.intersectionRect.bottom, spacerSeparation, containerSize);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
{
internal interface IVirtualizeJsCallbacks
{
void OnBeforeSpacerVisible(float spacerSize, float containerSize);
void OnAfterSpacerVisible(float spacerSize, float containerSize);
void OnBeforeSpacerVisible(float spacerSize, float spacerSeparation, float containerSize);
void OnAfterSpacerVisible(float spacerSize, float spacerSeparation, float containerSize);
}
}
13 changes: 12 additions & 1 deletion src/Components/Web/src/Virtualization/PlaceholderContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,24 @@ public readonly struct PlaceholderContext
/// </summary>
public int Index { get; }

/// <summary>
/// The size of the placeholder in pixels.
/// <para>
/// For virtualized components with vertical scrolling, this would be the height of the placeholder in pixels.
/// For virtualized components with horizontal scrolling, this would be the width of the placeholder in pixels.
/// </para>
/// </summary>
public float Size { get; }

/// <summary>
/// Constructs a new <see cref="PlaceholderContext"/> instance.
/// </summary>
/// <param name="index">The item index of the placeholder.</param>
public PlaceholderContext(int index)
/// <param name="size">The size of the placeholder in pixels.</param>
public PlaceholderContext(int index, float size = 0f)
{
Index = index;
Size = size;
}
}
}
64 changes: 49 additions & 15 deletions src/Components/Web/src/Virtualization/Virtualize.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ public sealed class Virtualize<TItem> : ComponentBase, IVirtualizeJsCallbacks, I

private int _loadedItemsStartIndex;

private int _lastRenderedItemCount;

private int _lastRenderedPlaceholderCount;

private float _itemSize;

private IEnumerable<TItem>? _loadedItems;

private CancellationTokenSource? _refreshCts;
Expand Down Expand Up @@ -65,10 +71,10 @@ public sealed class Virtualize<TItem> : ComponentBase, IVirtualizeJsCallbacks, I
public RenderFragment<PlaceholderContext>? Placeholder { get; set; }

/// <summary>
/// Gets the size of each item in pixels.
/// Gets the size of each item in pixels. Defaults to 50px.
/// </summary>
[Parameter]
public float ItemSize { get; set; }
public float ItemSize { get; set; } = 50f;

/// <summary>
/// Gets or sets the function providing items to the list.
Expand All @@ -88,7 +94,12 @@ protected override void OnParametersSet()
if (ItemSize <= 0)
{
throw new InvalidOperationException(
$"{GetType()} requires a positive value for parameter '{nameof(ItemSize)}' to perform virtualization.");
$"{GetType()} requires a positive value for parameter '{nameof(ItemSize)}'.");
}

if (_itemSize <= 0)
{
_itemSize = ItemSize;
}

if (ItemsProvider != null)
Expand Down Expand Up @@ -154,11 +165,13 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
{
// This is a rare case where it's valid for the sequence number to be programmatically incremented.
// This is only true because we know for certain that no other content will be alongside it.
builder.AddContent(renderIndex, _placeholder, new PlaceholderContext(renderIndex));
builder.AddContent(renderIndex, _placeholder, new PlaceholderContext(renderIndex, _itemSize));
}

builder.CloseRegion();

_lastRenderedItemCount = 0;

// Render the loaded items.
if (_loadedItems != null && _itemTemplate != null)
{
Expand All @@ -171,18 +184,22 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
foreach (var item in itemsToShow)
{
_itemTemplate(item)(builder);
renderIndex++;
_lastRenderedItemCount++;
}

renderIndex += _lastRenderedItemCount;

builder.CloseRegion();
}

_lastRenderedPlaceholderCount = Math.Max(0, lastItemIndex - _itemsBefore - _lastRenderedItemCount);

builder.OpenRegion(5);

// Render the placeholders after the loaded items.
for (; renderIndex < lastItemIndex; renderIndex++)
{
builder.AddContent(renderIndex, _placeholder, new PlaceholderContext(renderIndex));
builder.AddContent(renderIndex, _placeholder, new PlaceholderContext(renderIndex, _itemSize));
}

builder.CloseRegion();
Expand All @@ -197,28 +214,45 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
}

private string GetSpacerStyle(int itemsInSpacer)
=> $"height: {itemsInSpacer * ItemSize}px;";
=> $"height: {itemsInSpacer * _itemSize}px;";

void IVirtualizeJsCallbacks.OnBeforeSpacerVisible(float spacerSize, float containerSize)
void IVirtualizeJsCallbacks.OnBeforeSpacerVisible(float spacerSize, float spacerSeparation, float containerSize)
{
CalcualteItemDistribution(spacerSize, containerSize, out var itemsBefore, out var visibleItemCapacity);
CalcualteItemDistribution(spacerSize, spacerSeparation, containerSize, out var itemsBefore, out var visibleItemCapacity);

UpdateItemDistribution(itemsBefore, visibleItemCapacity);
}

void IVirtualizeJsCallbacks.OnAfterSpacerVisible(float spacerSize, float containerSize)
void IVirtualizeJsCallbacks.OnAfterSpacerVisible(float spacerSize, float spacerSeparation, float containerSize)
{
CalcualteItemDistribution(spacerSize, containerSize, out var itemsAfter, out var visibleItemCapacity);
CalcualteItemDistribution(spacerSize, spacerSeparation, containerSize, out var itemsAfter, out var visibleItemCapacity);

var itemsBefore = Math.Max(0, _itemCount - itemsAfter - visibleItemCapacity);

UpdateItemDistribution(itemsBefore, visibleItemCapacity);
}

private void CalcualteItemDistribution(float spacerSize, float containerSize, out int itemsInSpacer, out int visibleItemCapacity)
private void CalcualteItemDistribution(
float spacerSize,
float spacerSeparation,
float containerSize,
out int itemsInSpacer,
out int visibleItemCapacity)
{
itemsInSpacer = Math.Max(0, (int)Math.Floor(spacerSize / ItemSize) - 1);
visibleItemCapacity = (int)Math.Ceiling(containerSize / ItemSize) + 2;
if (_lastRenderedItemCount > 0)
{
_itemSize = (spacerSeparation - (_lastRenderedPlaceholderCount * _itemSize)) / _lastRenderedItemCount;
}

if (_itemSize <= 0)
{
// At this point, something unusual has occurred, likely due to misuse of this component.
// Reset the calculated item size to the user-provided item size.
_itemSize = ItemSize;
}

itemsInSpacer = Math.Max(0, (int)Math.Floor(spacerSize / _itemSize) - 1);
visibleItemCapacity = (int)Math.Ceiling(containerSize / _itemSize) + 2;
}

private void UpdateItemDistribution(int itemsBefore, int visibleItemCapacity)
Expand Down Expand Up @@ -285,7 +319,7 @@ private ValueTask<ItemsProviderResult<TItem>> DefaultItemsProvider(ItemsProvider
private RenderFragment DefaultPlaceholder(PlaceholderContext context) => (builder) =>
{
builder.OpenElement(0, "div");
builder.AddAttribute(1, "style", $"height: {ItemSize}px;");
builder.AddAttribute(1, "style", $"height: {_itemSize}px;");
builder.CloseElement();
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ public async ValueTask InitializeAsync(ElementReference spacerBefore, ElementRef
}

[JSInvokable]
public void OnSpacerBeforeVisible(float spacerSize, float containerSize)
public void OnSpacerBeforeVisible(float spacerSize, float spacerSeparation, float containerSize)
{
_owner.OnBeforeSpacerVisible(spacerSize, containerSize);
_owner.OnBeforeSpacerVisible(spacerSize, spacerSeparation, containerSize);
}

[JSInvokable]
public void OnSpacerAfterVisible(float spacerSize, float containerSize)
public void OnSpacerAfterVisible(float spacerSize, float spacerSeparation, float containerSize)
{
_owner.OnAfterSpacerVisible(spacerSize, containerSize);
_owner.OnAfterSpacerVisible(spacerSize, spacerSeparation, containerSize);
}

public async ValueTask DisposeAsync()
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Web/test/Virtualization/VirtualizeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public async Task Virtualize_DispatchesExceptionsFromItemsProviderThroughRendere
Assert.NotNull(renderedVirtualize);

// Simulate a JS spacer callback.
((IVirtualizeJsCallbacks)renderedVirtualize).OnAfterSpacerVisible(10f, 100f);
((IVirtualizeJsCallbacks)renderedVirtualize).OnAfterSpacerVisible(10f, 50f, 100f);

// Validate that the exception is dispatched through the renderer.
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await testRenderer.RenderRootComponentAsync(componentId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<div @key="context" id="async-item" style="height: @(itemSize)px; background-color: rgb(@((context % 2) * 255), @((1-(context % 2)) * 255), 255);">Item @context</div>
</ItemContent>
<Placeholder>
<div id="async-placeholder" style="height: @(itemSize)px; background-color: orange;">Loading item @context.Index...</div>
<div id="async-placeholder" style="height: @(context.Size)px; background-color: orange;">Loading item @context.Index...</div>
</Placeholder>
</Virtualize>
</div>
Expand Down