Skip to content

Commit 4ef5e10

Browse files
Virtualization support (#24179)
1 parent 3f15d26 commit 4ef5e10

File tree

18 files changed

+1011
-5
lines changed

18 files changed

+1011
-5
lines changed

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

Lines changed: 4 additions & 4 deletions
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/GlobalExports.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { navigateTo, internalFunctions as navigationManagerInternalFunctions } from './Services/NavigationManager';
22
import { attachRootComponentToElement } from './Rendering/Renderer';
33
import { domFunctions } from './DomWrapper';
4+
import { Virtualize } from './Virtualize';
45

56
// Make the following APIs available in global scope for invocation from JS
67
window['Blazor'] = {
@@ -10,5 +11,6 @@ window['Blazor'] = {
1011
attachRootComponentToElement,
1112
navigationManager: navigationManagerInternalFunctions,
1213
domWrapper: domFunctions,
14+
Virtualize,
1315
},
1416
};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
export const Virtualize = {
2+
init,
3+
dispose,
4+
};
5+
6+
const observersByDotNetId = {};
7+
8+
function findClosestScrollContainer(element: Element | null): Element | null {
9+
if (!element) {
10+
return null;
11+
}
12+
13+
const style = getComputedStyle(element);
14+
15+
if (style.overflowY !== 'visible') {
16+
return element;
17+
}
18+
19+
return findClosestScrollContainer(element.parentElement);
20+
}
21+
22+
function init(dotNetHelper: any, spacerBefore: HTMLElement, spacerAfter: HTMLElement, rootMargin = 50): void {
23+
const intersectionObserver = new IntersectionObserver(intersectionCallback, {
24+
root: findClosestScrollContainer(spacerBefore),
25+
rootMargin: `${rootMargin}px`,
26+
});
27+
28+
intersectionObserver.observe(spacerBefore);
29+
intersectionObserver.observe(spacerAfter);
30+
31+
const mutationObserverBefore = createSpacerMutationObserver(spacerBefore);
32+
const mutationObserverAfter = createSpacerMutationObserver(spacerAfter);
33+
34+
observersByDotNetId[dotNetHelper._id] = {
35+
intersectionObserver,
36+
mutationObserverBefore,
37+
mutationObserverAfter,
38+
};
39+
40+
function createSpacerMutationObserver(spacer: HTMLElement): MutationObserver {
41+
// Without the use of thresholds, IntersectionObserver only detects binary changes in visibility,
42+
// so if a spacer gets resized but remains visible, no additional callbacks will occur. By unobserving
43+
// and reobserving spacers when they get resized, the intersection callback will re-run if they remain visible.
44+
const mutationObserver = new MutationObserver((): void => {
45+
intersectionObserver.unobserve(spacer);
46+
intersectionObserver.observe(spacer);
47+
});
48+
49+
mutationObserver.observe(spacer, { attributes: true });
50+
51+
return mutationObserver;
52+
}
53+
54+
function intersectionCallback(entries: IntersectionObserverEntry[]): void {
55+
entries.forEach((entry): void => {
56+
if (!entry.isIntersecting) {
57+
return;
58+
}
59+
60+
const containerSize = entry.rootBounds?.height;
61+
62+
if (entry.target === spacerBefore) {
63+
dotNetHelper.invokeMethodAsync('OnSpacerBeforeVisible', entry.intersectionRect.top - entry.boundingClientRect.top, containerSize);
64+
} else if (entry.target === spacerAfter && spacerAfter.offsetHeight > 0) {
65+
// When we first start up, both the "before" and "after" spacers will be visible, but it's only relevant to raise a
66+
// single event to load the initial data. To avoid raising two events, skip the one for the "after" spacer if we know
67+
// it's meaningless to talk about any overlap into it.
68+
dotNetHelper.invokeMethodAsync('OnSpacerAfterVisible', entry.boundingClientRect.bottom - entry.intersectionRect.bottom, containerSize);
69+
}
70+
});
71+
}
72+
}
73+
74+
function dispose(dotNetHelper: any): void {
75+
const observers = observersByDotNetId[dotNetHelper._id];
76+
77+
if (observers) {
78+
observers.intersectionObserver.disconnect();
79+
observers.mutationObserverBefore.disconnect();
80+
observers.mutationObserverAfter.disconnect();
81+
82+
dotNetHelper.dispose();
83+
84+
delete observersByDotNetId[dotNetHelper._id];
85+
}
86+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Components.Web.Virtualization
5+
{
6+
internal interface IVirtualizeJsCallbacks
7+
{
8+
void OnBeforeSpacerVisible(float spacerSize, float containerSize);
9+
void OnAfterSpacerVisible(float spacerSize, float containerSize);
10+
}
11+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading.Tasks;
5+
6+
namespace Microsoft.AspNetCore.Components.Web.Virtualization
7+
{
8+
/// <summary>
9+
/// A function that provides items to a virtualized source.
10+
/// </summary>
11+
/// <typeparam name="TItem">The type of the context for each item in the list.</typeparam>
12+
/// <param name="request">The <see cref="ItemsProviderRequest"/> defining the request details.</param>
13+
/// <returns>A <see cref="ValueTask"/> whose result is a <see cref="ItemsProviderResult{TItem}"/> upon successful completion.</returns>
14+
public delegate ValueTask<ItemsProviderResult<TItem>> ItemsProviderDelegate<TItem>(ItemsProviderRequest request);
15+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading;
5+
6+
namespace Microsoft.AspNetCore.Components.Web.Virtualization
7+
{
8+
/// <summary>
9+
/// Represents a request to an <see cref="ItemsProviderDelegate{TItem}"/>.
10+
/// </summary>
11+
public readonly struct ItemsProviderRequest
12+
{
13+
/// <summary>
14+
/// The start index of the data segment requested.
15+
/// </summary>
16+
public int StartIndex { get; }
17+
18+
/// <summary>
19+
/// The requested number of items to be provided. The actual number of provided items does not need to match
20+
/// this value.
21+
/// </summary>
22+
public int Count { get; }
23+
24+
/// <summary>
25+
/// The <see cref="System.Threading.CancellationToken"/> used to relay cancellation of the request.
26+
/// </summary>
27+
public CancellationToken CancellationToken { get; }
28+
29+
/// <summary>
30+
/// Constructs a new <see cref="ItemsProviderRequest"/> instance.
31+
/// </summary>
32+
/// <param name="startIndex">The start index of the data segment requested.</param>
33+
/// <param name="count">The requested number of items to be provided.</param>
34+
/// <param name="cancellationToken">
35+
/// The <see cref="System.Threading.CancellationToken"/> used to relay cancellation of the request.
36+
/// </param>
37+
public ItemsProviderRequest(int startIndex, int count, CancellationToken cancellationToken)
38+
{
39+
StartIndex = startIndex;
40+
Count = count;
41+
CancellationToken = cancellationToken;
42+
}
43+
}
44+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
6+
namespace Microsoft.AspNetCore.Components.Web.Virtualization
7+
{
8+
/// <summary>
9+
/// Represents the result of a <see cref="ItemsProviderDelegate{TItem}"/>.
10+
/// </summary>
11+
/// <typeparam name="TItem">The type of the context for each item in the list.</typeparam>
12+
public readonly struct ItemsProviderResult<TItem>
13+
{
14+
/// <summary>
15+
/// The items to provide.
16+
/// </summary>
17+
public IEnumerable<TItem> Items { get; }
18+
19+
/// <summary>
20+
/// The total item count in the source generating the items provided.
21+
/// </summary>
22+
public int TotalItemCount { get; }
23+
24+
/// <summary>
25+
/// Instantiates a new <see cref="ItemsProviderResult{TItem}"/> instance.
26+
/// </summary>
27+
/// <param name="items">The items to provide.</param>
28+
/// <param name="totalItemCount">The total item count in the source generating the items provided.</param>
29+
public ItemsProviderResult(IEnumerable<TItem> items, int totalItemCount)
30+
{
31+
Items = items;
32+
TotalItemCount = totalItemCount;
33+
}
34+
}
35+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Components.Web.Virtualization
5+
{
6+
/// <summary>
7+
/// Contains context for a placeholder in a virtualized list.
8+
/// </summary>
9+
public readonly struct PlaceholderContext
10+
{
11+
/// <summary>
12+
/// The item index of the placeholder.
13+
/// </summary>
14+
public int Index { get; }
15+
16+
/// <summary>
17+
/// Constructs a new <see cref="PlaceholderContext"/> instance.
18+
/// </summary>
19+
/// <param name="index">The item index of the placeholder.</param>
20+
public PlaceholderContext(int index)
21+
{
22+
Index = index;
23+
}
24+
}
25+
}

0 commit comments

Comments
 (0)