Make Virtualize behave correctly with when ItemSize is unspecified or wrong. (#24920)
This commit is contained in:
parent
24f26e7b6a
commit
6a3887aba8
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue