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