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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const spacerSeparation = spacerAfter.offsetTop - (spacerBefore.offsetTop + spacerBefore.offsetHeight);
|
||||||
const containerSize = entry.rootBounds?.height;
|
const containerSize = entry.rootBounds?.height;
|
||||||
|
|
||||||
if (entry.target === spacerBefore) {
|
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) {
|
} 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
|
// 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
|
// 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.
|
// 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
|
internal interface IVirtualizeJsCallbacks
|
||||||
{
|
{
|
||||||
void OnBeforeSpacerVisible(float spacerSize, float containerSize);
|
void OnBeforeSpacerVisible(float spacerSize, float spacerSeparation, float containerSize);
|
||||||
void OnAfterSpacerVisible(float spacerSize, float containerSize);
|
void OnAfterSpacerVisible(float spacerSize, float spacerSeparation, float containerSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,24 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Index { get; }
|
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>
|
/// <summary>
|
||||||
/// Constructs a new <see cref="PlaceholderContext"/> instance.
|
/// Constructs a new <see cref="PlaceholderContext"/> instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="index">The item index of the placeholder.</param>
|
/// <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;
|
Index = index;
|
||||||
|
Size = size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,12 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
|
||||||
private int _loadedItemsStartIndex;
|
private int _loadedItemsStartIndex;
|
||||||
|
|
||||||
|
private int _lastRenderedItemCount;
|
||||||
|
|
||||||
|
private int _lastRenderedPlaceholderCount;
|
||||||
|
|
||||||
|
private float _itemSize;
|
||||||
|
|
||||||
private IEnumerable<TItem>? _loadedItems;
|
private IEnumerable<TItem>? _loadedItems;
|
||||||
|
|
||||||
private CancellationTokenSource? _refreshCts;
|
private CancellationTokenSource? _refreshCts;
|
||||||
|
|
@ -65,10 +71,10 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
public RenderFragment<PlaceholderContext>? Placeholder { get; set; }
|
public RenderFragment<PlaceholderContext>? Placeholder { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the size of each item in pixels.
|
/// Gets the size of each item in pixels. Defaults to 50px.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public float ItemSize { get; set; }
|
public float ItemSize { get; set; } = 50f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the function providing items to the list.
|
/// Gets or sets the function providing items to the list.
|
||||||
|
|
@ -88,7 +94,12 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
if (ItemSize <= 0)
|
if (ItemSize <= 0)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
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)
|
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 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.
|
// 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();
|
builder.CloseRegion();
|
||||||
|
|
||||||
|
_lastRenderedItemCount = 0;
|
||||||
|
|
||||||
// Render the loaded items.
|
// Render the loaded items.
|
||||||
if (_loadedItems != null && _itemTemplate != null)
|
if (_loadedItems != null && _itemTemplate != null)
|
||||||
{
|
{
|
||||||
|
|
@ -171,18 +184,22 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
foreach (var item in itemsToShow)
|
foreach (var item in itemsToShow)
|
||||||
{
|
{
|
||||||
_itemTemplate(item)(builder);
|
_itemTemplate(item)(builder);
|
||||||
renderIndex++;
|
_lastRenderedItemCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderIndex += _lastRenderedItemCount;
|
||||||
|
|
||||||
builder.CloseRegion();
|
builder.CloseRegion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_lastRenderedPlaceholderCount = Math.Max(0, lastItemIndex - _itemsBefore - _lastRenderedItemCount);
|
||||||
|
|
||||||
builder.OpenRegion(5);
|
builder.OpenRegion(5);
|
||||||
|
|
||||||
// Render the placeholders after the loaded items.
|
// Render the placeholders after the loaded items.
|
||||||
for (; renderIndex < lastItemIndex; renderIndex++)
|
for (; renderIndex < lastItemIndex; renderIndex++)
|
||||||
{
|
{
|
||||||
builder.AddContent(renderIndex, _placeholder, new PlaceholderContext(renderIndex));
|
builder.AddContent(renderIndex, _placeholder, new PlaceholderContext(renderIndex, _itemSize));
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.CloseRegion();
|
builder.CloseRegion();
|
||||||
|
|
@ -197,28 +214,45 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetSpacerStyle(int itemsInSpacer)
|
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);
|
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);
|
var itemsBefore = Math.Max(0, _itemCount - itemsAfter - visibleItemCapacity);
|
||||||
|
|
||||||
UpdateItemDistribution(itemsBefore, 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);
|
if (_lastRenderedItemCount > 0)
|
||||||
visibleItemCapacity = (int)Math.Ceiling(containerSize / ItemSize) + 2;
|
{
|
||||||
|
_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)
|
private void UpdateItemDistribution(int itemsBefore, int visibleItemCapacity)
|
||||||
|
|
@ -285,7 +319,7 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
private RenderFragment DefaultPlaceholder(PlaceholderContext context) => (builder) =>
|
private RenderFragment DefaultPlaceholder(PlaceholderContext context) => (builder) =>
|
||||||
{
|
{
|
||||||
builder.OpenElement(0, "div");
|
builder.OpenElement(0, "div");
|
||||||
builder.AddAttribute(1, "style", $"height: {ItemSize}px;");
|
builder.AddAttribute(1, "style", $"height: {_itemSize}px;");
|
||||||
builder.CloseElement();
|
builder.CloseElement();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,15 +30,15 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
}
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
[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]
|
[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()
|
public async ValueTask DisposeAsync()
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.Components.Virtualization
|
||||||
Assert.NotNull(renderedVirtualize);
|
Assert.NotNull(renderedVirtualize);
|
||||||
|
|
||||||
// Simulate a JS spacer callback.
|
// 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.
|
// Validate that the exception is dispatched through the renderer.
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await testRenderer.RenderRootComponentAsync(componentId));
|
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>
|
<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>
|
</ItemContent>
|
||||||
<Placeholder>
|
<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>
|
</Placeholder>
|
||||||
</Virtualize>
|
</Virtualize>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue