@@ -44,6 +44,12 @@ public sealed class Virtualize<TItem> : ComponentBase, IVirtualizeJsCallbacks, I
4444
4545 internal long _renderedWindowVersion ;
4646
47+ private long _contentRevision ;
48+
49+ private RenderedWindowIdentity ? _lastRenderedWindowIdentity ;
50+
51+ private TItem [ ] ? _lastLoadedItemsSnapshot ;
52+
4753 internal float _itemSize ;
4854
4955 private float _lastSetItemSize ;
@@ -546,7 +552,12 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
546552 throw oldRefreshException ;
547553 }
548554
549- var renderedWindowVersion = ++ _renderedWindowVersion ;
555+ var renderedWindowIdentity = GetRenderedWindowIdentity ( ) ;
556+ if ( renderedWindowIdentity != _lastRenderedWindowIdentity )
557+ {
558+ _renderedWindowVersion ++ ;
559+ }
560+ var renderedWindowVersion = _renderedWindowVersion ;
550561
551562 builder . OpenElement ( 0 , SpacerElement ) ;
552563 builder . AddAttribute ( 1 , "data-blazor-virtualize-reserved-height" , GetSpacerHeightPx ( _itemsBefore ) ) ;
@@ -628,8 +639,26 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
628639 builder . AddElementReferenceCapture ( 14 , elementReference => _spacerAfter = elementReference ) ;
629640
630641 builder . CloseElement ( ) ;
642+
643+ _lastRenderedWindowIdentity = GetRenderedWindowIdentity ( ) ;
631644 }
632645
646+ private RenderedWindowIdentity GetRenderedWindowIdentity ( )
647+ => new (
648+ _itemsBefore ,
649+ _visibleItemCapacity ,
650+ _unusedItemCapacity ,
651+ _itemCount ,
652+ _loadedItemsStartIndex ,
653+ _lastRenderedItemCount ,
654+ _lastRenderedPlaceholderCount ,
655+ _itemSize ,
656+ _totalMeasuredHeight ,
657+ _measuredItemCount ,
658+ _loading ,
659+ _contentRevision ,
660+ SpacerElement ) ;
661+
633662 private string GetSpacerHeightPx ( int itemCount )
634663 => ( itemCount * GetItemHeight ( ) ) . ToString ( CultureInfo . InvariantCulture ) ;
635664
@@ -647,39 +676,39 @@ private void CancelInFlightScrollForUserInteraction()
647676 }
648677 }
649678
650- void IVirtualizeJsCallbacks . OnBeforeSpacerVisible (
679+ bool IVirtualizeJsCallbacks . OnBeforeSpacerVisible (
651680 float spacerSize ,
652681 float spacerSeparation ,
653682 float containerSize ,
654683 SpacerVisibilityReason reason ,
655684 long renderedWindowVersion )
656685 {
657- if ( renderedWindowVersion != _renderedWindowVersion )
658- {
659- return ;
660- }
661-
662686 if ( _pendingAnchorRestore )
663687 {
664- return ;
688+ return true ;
665689 }
666690 if ( _initialIndex . Phase == InitialIndexPhase . None && InitialItemIndex > 0 )
667691 {
668- return ;
692+ return true ;
693+ }
694+ if ( reason == SpacerVisibilityReason . UserScroll )
695+ {
696+ CancelInFlightScrollForUserInteraction ( ) ;
697+ }
698+ if ( renderedWindowVersion != _renderedWindowVersion )
699+ {
700+ return false ;
669701 }
670702 switch ( reason )
671703 {
672704 case SpacerVisibilityReason . ProgrammaticScroll :
673- return ;
674- case SpacerVisibilityReason . UserScroll :
675- CancelInFlightScrollForUserInteraction ( ) ;
676- break ;
705+ return true ;
677706 case SpacerVisibilityReason . ViewportFill :
678707 // A fill callback while our own scroll is in flight is a side effect of that scroll —
679708 // acting on it would move the target.
680709 if ( _currentScrollCts is not null )
681710 {
682- return ;
711+ return true ;
683712 }
684713 break ;
685714 }
@@ -692,7 +721,7 @@ void IVirtualizeJsCallbacks.OnBeforeSpacerVisible(
692721 ViewportFillDirection . Before ,
693722 visibleItemCapacity ,
694723 unusedItemCapacity ) ;
695- return ;
724+ return true ;
696725 }
697726
698727 // Slide window up by at least one if spacer is visible but position unchanged.
@@ -702,33 +731,33 @@ void IVirtualizeJsCallbacks.OnBeforeSpacerVisible(
702731 }
703732
704733 UpdateItemDistribution ( itemsBefore , visibleItemCapacity , unusedItemCapacity ) ;
734+ return true ;
705735 }
706736
707- void IVirtualizeJsCallbacks . OnAfterSpacerVisible (
737+ bool IVirtualizeJsCallbacks . OnAfterSpacerVisible (
708738 float spacerSize ,
709739 float spacerSeparation ,
710740 float containerSize ,
711741 SpacerVisibilityReason reason ,
712742 long renderedWindowVersion )
713743 {
714- if ( renderedWindowVersion != _renderedWindowVersion )
715- {
716- return ;
717- }
718-
719744 if ( _pendingAnchorRestore || reason == SpacerVisibilityReason . ProgrammaticScroll )
720745 {
721- return ;
746+ return true ;
722747 }
723748 if ( reason == SpacerVisibilityReason . UserScroll )
724749 {
725750 CancelInFlightScrollForUserInteraction ( ) ;
726751 }
727- else if ( reason == SpacerVisibilityReason . ViewportFill && _currentScrollCts is not null )
752+ if ( renderedWindowVersion != _renderedWindowVersion )
753+ {
754+ return false ;
755+ }
756+ if ( reason == SpacerVisibilityReason . ViewportFill && _currentScrollCts is not null )
728757 {
729758 // Bottom-spacer fill while our own scroll is in flight: the window moved but scrollTop hasn't
730759 // landed, so acting on it would undo the target. The real fill runs once the scroll completes.
731- return ;
760+ return true ;
732761 }
733762 var hadNewMeasurements = CalculateItemDistribution ( spacerSize , spacerSeparation , containerSize , out var itemsAfter , out var visibleItemCapacity , out var unusedItemCapacity ) ;
734763
@@ -738,7 +767,7 @@ void IVirtualizeJsCallbacks.OnAfterSpacerVisible(
738767 ViewportFillDirection . After ,
739768 visibleItemCapacity ,
740769 unusedItemCapacity ) ;
741- return ;
770+ return true ;
742771 }
743772
744773 var itemsBefore = Math . Max ( 0 , _itemCount - itemsAfter - visibleItemCapacity ) ;
@@ -761,6 +790,7 @@ void IVirtualizeJsCallbacks.OnAfterSpacerVisible(
761790 }
762791
763792 UpdateItemDistribution ( itemsBefore , visibleItemCapacity , unusedItemCapacity ) ;
793+ return true ;
764794 }
765795
766796 void IVirtualizeJsCallbacks . OnAlignmentCompleted ( VirtualizeAlignmentResult result )
@@ -1083,7 +1113,15 @@ private async ValueTask RefreshDataCoreAsync(bool renderOnSuccess, CancellationT
10831113 }
10841114
10851115 _itemCount = result . TotalItemCount ;
1086- _loadedItems = result . Items ;
1116+ var loadedItems = result . Items . ToArray ( ) ;
1117+ if ( _itemsProvider != DefaultItemsProvider
1118+ || _lastLoadedItemsSnapshot is null
1119+ || ! _lastLoadedItemsSnapshot . SequenceEqual ( loadedItems ) )
1120+ {
1121+ _contentRevision ++ ;
1122+ }
1123+ _lastLoadedItemsSnapshot = loadedItems ;
1124+ _loadedItems = loadedItems ;
10871125 _loadedItemsStartIndex = _itemsBefore ;
10881126
10891127 // For DefaultItemsProvider, capture the first loaded item so we can detect
@@ -1106,6 +1144,7 @@ private async ValueTask RefreshDataCoreAsync(bool renderOnSuccess, CancellationT
11061144 StateHasChanged ( ) ;
11071145 }
11081146 }
1147+
11091148 catch ( Exception e )
11101149 {
11111150 if ( e is OperationCanceledException oce && oce . CancellationToken == cancellationToken )
@@ -1230,6 +1269,21 @@ public async ValueTask DisposeAsync()
12301269 }
12311270 }
12321271
1272+ private readonly record struct RenderedWindowIdentity (
1273+ int ItemsBefore ,
1274+ int VisibleItemCapacity ,
1275+ int UnusedItemCapacity ,
1276+ int ItemCount ,
1277+ int LoadedItemsStartIndex ,
1278+ int LastRenderedItemCount ,
1279+ int LastRenderedPlaceholderCount ,
1280+ float ItemSize ,
1281+ float TotalMeasuredHeight ,
1282+ int MeasuredItemCount ,
1283+ bool IsLoading ,
1284+ long ContentRevision ,
1285+ string SpacerElement ) ;
1286+
12331287 private enum InitialIndexPhase
12341288 {
12351289 None ,
0 commit comments