Skip to content

Commit 6a3887a

Browse files
Make Virtualize behave correctly with when ItemSize is unspecified or wrong. (#24920)
1 parent 24f26e7 commit 6a3887a

File tree

9 files changed

+74
-28
lines changed

9 files changed

+74
-28
lines changed

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webassembly.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Virtualize.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,16 @@ function init(dotNetHelper: any, spacerBefore: HTMLElement, spacerAfter: HTMLEle
5757
return;
5858
}
5959

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

6263
if (entry.target === spacerBefore) {
63-
dotNetHelper.invokeMethodAsync('OnSpacerBeforeVisible', entry.intersectionRect.top - entry.boundingClientRect.top, containerSize);
64+
dotNetHelper.invokeMethodAsync('OnSpacerBeforeVisible', entry.intersectionRect.top - entry.boundingClientRect.top, spacerSeparation, containerSize);
6465
} else if (entry.target === spacerAfter && spacerAfter.offsetHeight > 0) {
6566
// When we first start up, both the "before" and "after" spacers will be visible, but it's only relevant to raise a
6667
// single event to load the initial data. To avoid raising two events, skip the one for the "after" spacer if we know
6768
// it's meaningless to talk about any overlap into it.
68-
dotNetHelper.invokeMethodAsync('OnSpacerAfterVisible', entry.boundingClientRect.bottom - entry.intersectionRect.bottom, containerSize);
69+
dotNetHelper.invokeMethodAsync('OnSpacerAfterVisible', entry.boundingClientRect.bottom - entry.intersectionRect.bottom, spacerSeparation, containerSize);
6970
}
7071
});
7172
}

src/Components/Web/src/Virtualization/IVirtualizeJsCallbacks.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.Components.Web.Virtualization
55
{
66
internal interface IVirtualizeJsCallbacks
77
{
8-
void OnBeforeSpacerVisible(float spacerSize, float containerSize);
9-
void OnAfterSpacerVisible(float spacerSize, float containerSize);
8+
void OnBeforeSpacerVisible(float spacerSize, float spacerSeparation, float containerSize);
9+
void OnAfterSpacerVisible(float spacerSize, float spacerSeparation, float containerSize);
1010
}
1111
}

src/Components/Web/src/Virtualization/PlaceholderContext.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,24 @@ public readonly struct PlaceholderContext
1313
/// </summary>
1414
public int Index { get; }
1515

16+
/// <summary>
17+
/// The size of the placeholder in pixels.
18+
/// <para>
19+
/// For virtualized components with vertical scrolling, this would be the height of the placeholder in pixels.
20+
/// For virtualized components with horizontal scrolling, this would be the width of the placeholder in pixels.
21+
/// </para>
22+
/// </summary>
23+
public float Size { get; }
24+
1625
/// <summary>
1726
/// Constructs a new <see cref="PlaceholderContext"/> instance.
1827
/// </summary>
1928
/// <param name="index">The item index of the placeholder.</param>
20-
public PlaceholderContext(int index)
29+
/// <param name="size">The size of the placeholder in pixels.</param>
30+
public PlaceholderContext(int index, float size = 0f)
2131
{
2232
Index = index;
33+
Size = size;
2334
}
2435
}
2536
}

src/Components/Web/src/Virtualization/Virtualize.cs

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ public sealed class Virtualize<TItem> : ComponentBase, IVirtualizeJsCallbacks, I
3131

3232
private int _loadedItemsStartIndex;
3333

34+
private int _lastRenderedItemCount;
35+
36+
private int _lastRenderedPlaceholderCount;
37+
38+
private float _itemSize;
39+
3440
private IEnumerable<TItem>? _loadedItems;
3541

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

6773
/// <summary>
68-
/// Gets the size of each item in pixels.
74+
/// Gets the size of each item in pixels. Defaults to 50px.
6975
/// </summary>
7076
[Parameter]
71-
public float ItemSize { get; set; }
77+
public float ItemSize { get; set; } = 50f;
7278

7379
/// <summary>
7480
/// Gets or sets the function providing items to the list.
@@ -88,7 +94,12 @@ protected override void OnParametersSet()
8894
if (ItemSize <= 0)
8995
{
9096
throw new InvalidOperationException(
91-
$"{GetType()} requires a positive value for parameter '{nameof(ItemSize)}' to perform virtualization.");
97+
$"{GetType()} requires a positive value for parameter '{nameof(ItemSize)}'.");
98+
}
99+
100+
if (_itemSize <= 0)
101+
{
102+
_itemSize = ItemSize;
92103
}
93104

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

160171
builder.CloseRegion();
161172

173+
_lastRenderedItemCount = 0;
174+
162175
// Render the loaded items.
163176
if (_loadedItems != null && _itemTemplate != null)
164177
{
@@ -171,18 +184,22 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
171184
foreach (var item in itemsToShow)
172185
{
173186
_itemTemplate(item)(builder);
174-
renderIndex++;
187+
_lastRenderedItemCount++;
175188
}
176189

190+
renderIndex += _lastRenderedItemCount;
191+
177192
builder.CloseRegion();
178193
}
179194

195+
_lastRenderedPlaceholderCount = Math.Max(0, lastItemIndex - _itemsBefore - _lastRenderedItemCount);
196+
180197
builder.OpenRegion(5);
181198

182199
// Render the placeholders after the loaded items.
183200
for (; renderIndex < lastItemIndex; renderIndex++)
184201
{
185-
builder.AddContent(renderIndex, _placeholder, new PlaceholderContext(renderIndex));
202+
builder.AddContent(renderIndex, _placeholder, new PlaceholderContext(renderIndex, _itemSize));
186203
}
187204

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

199216
private string GetSpacerStyle(int itemsInSpacer)
200-
=> $"height: {itemsInSpacer * ItemSize}px;";
217+
=> $"height: {itemsInSpacer * _itemSize}px;";
201218

202-
void IVirtualizeJsCallbacks.OnBeforeSpacerVisible(float spacerSize, float containerSize)
219+
void IVirtualizeJsCallbacks.OnBeforeSpacerVisible(float spacerSize, float spacerSeparation, float containerSize)
203220
{
204-
CalcualteItemDistribution(spacerSize, containerSize, out var itemsBefore, out var visibleItemCapacity);
221+
CalcualteItemDistribution(spacerSize, spacerSeparation, containerSize, out var itemsBefore, out var visibleItemCapacity);
205222

206223
UpdateItemDistribution(itemsBefore, visibleItemCapacity);
207224
}
208225

209-
void IVirtualizeJsCallbacks.OnAfterSpacerVisible(float spacerSize, float containerSize)
226+
void IVirtualizeJsCallbacks.OnAfterSpacerVisible(float spacerSize, float spacerSeparation, float containerSize)
210227
{
211-
CalcualteItemDistribution(spacerSize, containerSize, out var itemsAfter, out var visibleItemCapacity);
228+
CalcualteItemDistribution(spacerSize, spacerSeparation, containerSize, out var itemsAfter, out var visibleItemCapacity);
212229

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

215232
UpdateItemDistribution(itemsBefore, visibleItemCapacity);
216233
}
217234

218-
private void CalcualteItemDistribution(float spacerSize, float containerSize, out int itemsInSpacer, out int visibleItemCapacity)
235+
private void CalcualteItemDistribution(
236+
float spacerSize,
237+
float spacerSeparation,
238+
float containerSize,
239+
out int itemsInSpacer,
240+
out int visibleItemCapacity)
219241
{
220-
itemsInSpacer = Math.Max(0, (int)Math.Floor(spacerSize / ItemSize) - 1);
221-
visibleItemCapacity = (int)Math.Ceiling(containerSize / ItemSize) + 2;
242+
if (_lastRenderedItemCount > 0)
243+
{
244+
_itemSize = (spacerSeparation - (_lastRenderedPlaceholderCount * _itemSize)) / _lastRenderedItemCount;
245+
}
246+
247+
if (_itemSize <= 0)
248+
{
249+
// At this point, something unusual has occurred, likely due to misuse of this component.
250+
// Reset the calculated item size to the user-provided item size.
251+
_itemSize = ItemSize;
252+
}
253+
254+
itemsInSpacer = Math.Max(0, (int)Math.Floor(spacerSize / _itemSize) - 1);
255+
visibleItemCapacity = (int)Math.Ceiling(containerSize / _itemSize) + 2;
222256
}
223257

224258
private void UpdateItemDistribution(int itemsBefore, int visibleItemCapacity)
@@ -285,7 +319,7 @@ private ValueTask<ItemsProviderResult<TItem>> DefaultItemsProvider(ItemsProvider
285319
private RenderFragment DefaultPlaceholder(PlaceholderContext context) => (builder) =>
286320
{
287321
builder.OpenElement(0, "div");
288-
builder.AddAttribute(1, "style", $"height: {ItemSize}px;");
322+
builder.AddAttribute(1, "style", $"height: {_itemSize}px;");
289323
builder.CloseElement();
290324
};
291325

src/Components/Web/src/Virtualization/VirtualizeJsInterop.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ public async ValueTask InitializeAsync(ElementReference spacerBefore, ElementRef
3030
}
3131

3232
[JSInvokable]
33-
public void OnSpacerBeforeVisible(float spacerSize, float containerSize)
33+
public void OnSpacerBeforeVisible(float spacerSize, float spacerSeparation, float containerSize)
3434
{
35-
_owner.OnBeforeSpacerVisible(spacerSize, containerSize);
35+
_owner.OnBeforeSpacerVisible(spacerSize, spacerSeparation, containerSize);
3636
}
3737

3838
[JSInvokable]
39-
public void OnSpacerAfterVisible(float spacerSize, float containerSize)
39+
public void OnSpacerAfterVisible(float spacerSize, float spacerSeparation, float containerSize)
4040
{
41-
_owner.OnAfterSpacerVisible(spacerSize, containerSize);
41+
_owner.OnAfterSpacerVisible(spacerSize, spacerSeparation, containerSize);
4242
}
4343

4444
public async ValueTask DisposeAsync()

src/Components/Web/test/Virtualization/VirtualizeTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public async Task Virtualize_DispatchesExceptionsFromItemsProviderThroughRendere
9797
Assert.NotNull(renderedVirtualize);
9898

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

102102
// Validate that the exception is dispatched through the renderer.
103103
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await testRenderer.RenderRootComponentAsync(componentId));

src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<div @key="context" id="async-item" style="height: @(itemSize)px; background-color: rgb(@((context % 2) * 255), @((1-(context % 2)) * 255), 255);">Item @context</div>
2323
</ItemContent>
2424
<Placeholder>
25-
<div id="async-placeholder" style="height: @(itemSize)px; background-color: orange;">Loading item @context.Index...</div>
25+
<div id="async-placeholder" style="height: @(context.Size)px; background-color: orange;">Loading item @context.Index...</div>
2626
</Placeholder>
2727
</Virtualize>
2828
</div>

0 commit comments

Comments
 (0)