Make Virtualize behave correctly with when ItemSize is unspecified or wrong. (#24920)

This commit is contained in:
Mackinnon Buck 2020-08-18 20:15:50 -07:00 committed by GitHub
parent 24f26e7b6a
commit 6a3887aba8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 74 additions and 28 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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);
}
});
}

View File

@ -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);
}
}

View File

@ -13,13 +13,24 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
/// </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;
}
}
}

View File

@ -31,6 +31,12 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
private int _loadedItemsStartIndex;
private int _lastRenderedItemCount;
private int _lastRenderedPlaceholderCount;
private float _itemSize;
private IEnumerable<TItem>? _loadedItems;
private CancellationTokenSource? _refreshCts;
@ -65,10 +71,10 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
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.
@ -88,7 +94,12 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
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)
@ -154,11 +165,13 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
{
// 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)
{
@ -171,18 +184,22 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
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();
@ -197,28 +214,45 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
}
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)
@ -285,7 +319,7 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
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();
};

View File

@ -30,15 +30,15 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
}
[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()

View File

@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.Components.Virtualization
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));

View File

@ -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>