@@ -25,6 +25,14 @@ public sealed class Virtualize<TItem> : ComponentBase, IVirtualizeJsCallbacks, I
25
25
26
26
private int _visibleItemCapacity ;
27
27
28
+ // If the client reports a viewport so large that it could show more than MaxItemCount items,
29
+ // we keep track of the "unused" capacity, which is the amount of blank space we want to leave
30
+ // at the bottom of the viewport (as a number of items). If we didn't leave this blank space,
31
+ // then the bottom spacer would always stay visible and the client would request more items in an
32
+ // infinite (but asynchronous) loop, as it would believe there are more items to render and
33
+ // enough space to render them into.
34
+ private int _unusedItemCapacity ;
35
+
28
36
private int _itemCount ;
29
37
30
38
private int _loadedItemsStartIndex ;
@@ -118,6 +126,22 @@ public sealed class Virtualize<TItem> : ComponentBase, IVirtualizeJsCallbacks, I
118
126
[ Parameter ]
119
127
public string SpacerElement { get ; set ; } = "div" ;
120
128
129
+ /*
130
+ This API will be added in .NET 9 but cannot be added in a .NET 8 or earlier patch,
131
+ as we can't change public API in patches.
132
+
133
+ /// <summary>
134
+ /// Gets or sets the maximum number of items that will be rendered, even if the client reports
135
+ /// that its viewport is large enough to show more. The default value is 100.
136
+ ///
137
+ /// This should only be used as a safeguard against excessive memory usage or large data loads.
138
+ /// Do not set this to a smaller number than you expect to fit on a realistic-sized window, because
139
+ /// that will leave a blank gap below and the user may not be able to see the rest of the content.
140
+ /// </summary>
141
+ [Parameter]
142
+ public int MaxItemCount { get; set; } = 100;
143
+ */
144
+
121
145
/// <summary>
122
146
/// Instructs the component to re-request data from its <see cref="ItemsProvider"/>.
123
147
/// This is useful if external data may have changed. There is no need to call this
@@ -264,18 +288,23 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
264
288
var itemsAfter = Math . Max ( 0 , _itemCount - _visibleItemCapacity - _itemsBefore ) ;
265
289
266
290
builder . OpenElement ( 7 , SpacerElement ) ;
267
- builder . AddAttribute ( 8 , "style" , GetSpacerStyle ( itemsAfter ) ) ;
291
+ builder . AddAttribute ( 8 , "style" , GetSpacerStyle ( itemsAfter , _unusedItemCapacity ) ) ;
268
292
builder . AddElementReferenceCapture ( 9 , elementReference => _spacerAfter = elementReference ) ;
269
293
270
294
builder . CloseElement ( ) ;
271
295
}
272
296
297
+ private string GetSpacerStyle ( int itemsInSpacer , int numItemsGapAbove )
298
+ => numItemsGapAbove == 0
299
+ ? GetSpacerStyle ( itemsInSpacer )
300
+ : $ "height: { ( itemsInSpacer * _itemSize ) . ToString ( CultureInfo . InvariantCulture ) } px; flex-shrink: 0; transform: translateY({ ( numItemsGapAbove * _itemSize ) . ToString ( CultureInfo . InvariantCulture ) } px);";
301
+
273
302
private string GetSpacerStyle ( int itemsInSpacer )
274
303
=> $ "height: { ( itemsInSpacer * _itemSize ) . ToString ( CultureInfo . InvariantCulture ) } px; flex-shrink: 0;";
275
304
276
305
void IVirtualizeJsCallbacks . OnBeforeSpacerVisible ( float spacerSize , float spacerSeparation , float containerSize )
277
306
{
278
- CalcualteItemDistribution ( spacerSize , spacerSeparation , containerSize , out var itemsBefore , out var visibleItemCapacity ) ;
307
+ CalcualteItemDistribution ( spacerSize , spacerSeparation , containerSize , out var itemsBefore , out var visibleItemCapacity , out var unusedItemCapacity ) ;
279
308
280
309
// Since we know the before spacer is now visible, we absolutely have to slide the window up
281
310
// by at least one element. If we're not doing that, the previous item size info we had must
@@ -286,12 +315,12 @@ void IVirtualizeJsCallbacks.OnBeforeSpacerVisible(float spacerSize, float spacer
286
315
itemsBefore -- ;
287
316
}
288
317
289
- UpdateItemDistribution ( itemsBefore , visibleItemCapacity ) ;
318
+ UpdateItemDistribution ( itemsBefore , visibleItemCapacity , unusedItemCapacity ) ;
290
319
}
291
320
292
321
void IVirtualizeJsCallbacks . OnAfterSpacerVisible ( float spacerSize , float spacerSeparation , float containerSize )
293
322
{
294
- CalcualteItemDistribution ( spacerSize , spacerSeparation , containerSize , out var itemsAfter , out var visibleItemCapacity ) ;
323
+ CalcualteItemDistribution ( spacerSize , spacerSeparation , containerSize , out var itemsAfter , out var visibleItemCapacity , out var unusedItemCapacity ) ;
295
324
296
325
var itemsBefore = Math . Max ( 0 , _itemCount - itemsAfter - visibleItemCapacity ) ;
297
326
@@ -304,15 +333,16 @@ void IVirtualizeJsCallbacks.OnAfterSpacerVisible(float spacerSize, float spacerS
304
333
itemsBefore ++ ;
305
334
}
306
335
307
- UpdateItemDistribution ( itemsBefore , visibleItemCapacity ) ;
336
+ UpdateItemDistribution ( itemsBefore , visibleItemCapacity , unusedItemCapacity ) ;
308
337
}
309
338
310
339
private void CalcualteItemDistribution (
311
340
float spacerSize ,
312
341
float spacerSeparation ,
313
342
float containerSize ,
314
343
out int itemsInSpacer ,
315
- out int visibleItemCapacity )
344
+ out int visibleItemCapacity ,
345
+ out int unusedItemCapacity )
316
346
{
317
347
if ( _lastRenderedItemCount > 0 )
318
348
{
@@ -326,11 +356,21 @@ private void CalcualteItemDistribution(
326
356
_itemSize = ItemSize ;
327
357
}
328
358
359
+ // This AppContext data exists as a stopgap for .NET 8 and earlier, since this is being added in a patch
360
+ // where we can't add new public API.
361
+ var maxItemCount = AppContext . GetData ( "Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize.MaxItemCount" ) switch
362
+ {
363
+ int val => val , // In .NET 9, this will be Math.Min(val, MaxItemCount)
364
+ _ => 1000 // In .NET 9, this will be MaxItemCount
365
+ } ;
366
+
329
367
itemsInSpacer = Math . Max ( 0 , ( int ) Math . Floor ( spacerSize / _itemSize ) - OverscanCount ) ;
330
368
visibleItemCapacity = ( int ) Math . Ceiling ( containerSize / _itemSize ) + 2 * OverscanCount ;
369
+ unusedItemCapacity = Math . Max ( 0 , visibleItemCapacity - maxItemCount ) ;
370
+ visibleItemCapacity -= unusedItemCapacity ;
331
371
}
332
372
333
- private void UpdateItemDistribution ( int itemsBefore , int visibleItemCapacity )
373
+ private void UpdateItemDistribution ( int itemsBefore , int visibleItemCapacity , int unusedItemCapacity )
334
374
{
335
375
// If the itemcount just changed to a lower number, and we're already scrolled past the end of the new
336
376
// reduced set of items, clamp the scroll position to the new maximum
@@ -340,10 +380,11 @@ private void UpdateItemDistribution(int itemsBefore, int visibleItemCapacity)
340
380
}
341
381
342
382
// If anything about the offset changed, re-render
343
- if ( itemsBefore != _itemsBefore || visibleItemCapacity != _visibleItemCapacity )
383
+ if ( itemsBefore != _itemsBefore || visibleItemCapacity != _visibleItemCapacity || unusedItemCapacity != _unusedItemCapacity )
344
384
{
345
385
_itemsBefore = itemsBefore ;
346
386
_visibleItemCapacity = visibleItemCapacity ;
387
+ _unusedItemCapacity = unusedItemCapacity ;
347
388
var refreshTask = RefreshDataCoreAsync ( renderOnSuccess : true ) ;
348
389
349
390
if ( ! refreshTask . IsCompleted )
0 commit comments