Skip to content

Commit 0d90cbb

Browse files
committed
Basic virtualization prototype.
1 parent 0b415b9 commit 0d90cbb

File tree

7 files changed

+217
-0
lines changed

7 files changed

+217
-0
lines changed

AspNetCore.sln

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1430,12 +1430,20 @@ EndProject
14301430
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sdk", "Sdk", "{FED4267E-E5E4-49C5-98DB-8B3F203596EE}"
14311431
EndProject
14321432
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly", "src\Components\WebAssembly\Sdk\src\Microsoft.NET.Sdk.BlazorWebAssembly.csproj", "{6B2734BF-C61D-4889-ABBF-456A4075D59B}"
1433+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicTestApp", "src\Components\test\testassets\BasicTestApp\BasicTestApp.csproj", "{46FB7E93-1294-4068-B80A-D4864F78277A}"
14331434
EndProject
14341435
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.Tests", "src\Components\WebAssembly\Sdk\test\Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj", "{83371889-9A3E-4D16-AE77-EB4F83BC6374}"
14351436
EndProject
14361437
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests", "src\Components\WebAssembly\Sdk\integrationtests\Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj", "{525EBCB4-A870-470B-BC90-845306C337D1}"
14371438
EndProject
14381439
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.Tools", "src\Components\WebAssembly\Sdk\tools\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj", "{175E5CD8-92D4-46BB-882E-3A930D3302D4}"
1440+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComponentsApp.Server", "src\Components\test\testassets\ComponentsApp.Server\ComponentsApp.Server.csproj", "{19974360-4A63-425A-94DB-C2C940A3A97A}"
1441+
EndProject
1442+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LazyTestContentPackage", "src\Components\test\testassets\LazyTestContentPackage\LazyTestContentPackage.csproj", "{ADF9C126-F322-4E34-AFD3-E626A4487206}"
1443+
EndProject
1444+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestContentPackage", "src\Components\test\testassets\TestContentPackage\TestContentPackage.csproj", "{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}"
1445+
EndProject
1446+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components.TestServer", "src\Components\test\testassets\TestServer\Components.TestServer.csproj", "{8A59AF88-4A82-46ED-977D-D909001F8107}"
14391447
EndProject
14401448
Global
14411449
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Microsoft.AspNetCore.Components.Rendering;
5+
6+
namespace Microsoft.AspNetCore.Components.Virtualization
7+
{
8+
public abstract class VirtualizeBase<TItem> : ComponentBase
9+
{
10+
private int _itemsAbove;
11+
12+
private int _itemsVisible;
13+
14+
private int _itemsBelow;
15+
16+
protected ElementReference TopSpacer { get; private set; }
17+
18+
protected ElementReference BottomSpacer { get; private set; }
19+
20+
[Parameter]
21+
public ICollection<TItem> Items { get; set; } = default!;
22+
23+
[Parameter]
24+
public float ItemSize { get; set; }
25+
26+
[Parameter]
27+
public RenderFragment<TItem>? ChildContent { get; set; }
28+
29+
protected override void OnParametersSet()
30+
{
31+
if (Items == null)
32+
{
33+
throw new InvalidOperationException(
34+
$"Parameter '{nameof(Items)}' must be specified and non-null.");
35+
}
36+
37+
if (ItemSize <= 0f)
38+
{
39+
throw new InvalidOperationException(
40+
$"Parameter '{nameof(ItemSize)}' must be specified and greater than zero.");
41+
}
42+
43+
_itemsBelow = Items.Count;
44+
}
45+
46+
protected void UpdateTopSpacer(float spacerSize, float containerSize)
47+
=> CalculateSpacerItemDistribution(spacerSize, containerSize, out _itemsAbove, out _itemsBelow);
48+
49+
protected void UpdateBottomSpacer(float spacerSize, float containerSize)
50+
=> CalculateSpacerItemDistribution(spacerSize, containerSize, out _itemsBelow, out _itemsAbove);
51+
52+
private void CalculateSpacerItemDistribution(float spacerSize, float containerSize, out int itemsInThisSpacer, out int itemsInOtherSpacer)
53+
{
54+
_itemsVisible = (int)(containerSize / ItemSize) + 1; // TODO: Custom number of "padding" elements?
55+
itemsInThisSpacer = (int)(spacerSize / ItemSize);
56+
itemsInOtherSpacer = Items.Count - itemsInThisSpacer - _itemsVisible;
57+
58+
StateHasChanged();
59+
}
60+
61+
private string GetSpacerStyle(int itemsInSpacer)
62+
{
63+
return $"height: {itemsInSpacer * ItemSize}px;";
64+
}
65+
66+
protected override void BuildRenderTree(RenderTreeBuilder builder)
67+
{
68+
builder.OpenElement(0, "div");
69+
builder.AddAttribute(1, "key", "top-spacer");
70+
builder.AddAttribute(2, "style", GetSpacerStyle(_itemsAbove));
71+
builder.AddElementReferenceCapture(3, elementReference => TopSpacer = elementReference);
72+
builder.CloseElement();
73+
74+
if (ChildContent != null)
75+
{
76+
builder.AddContent(4, new RenderFragment(builder =>
77+
{
78+
foreach (var item in Items.Skip(_itemsAbove).Take(_itemsVisible))
79+
{
80+
ChildContent(item)?.Invoke(builder);
81+
}
82+
}));
83+
}
84+
85+
builder.OpenElement(5, "div");
86+
builder.AddAttribute(6, "key", "bottom-spacer");
87+
builder.AddAttribute(7, "style", GetSpacerStyle(_itemsBelow));
88+
builder.AddElementReferenceCapture(8, elementReference =>
89+
BottomSpacer = elementReference);
90+
builder.CloseElement();
91+
}
92+
}
93+
}

src/Components/Web.JS/src/GlobalExports.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { navigateTo, internalFunctions as navigationManagerInternalFunctions } f
22
import { attachRootComponentToElement } from './Rendering/Renderer';
33
import { domFunctions } from './DomWrapper';
44
import { setProfilingEnabled } from './Platform/Profiling';
5+
import { Virtualize } from './Virtualize';
56

67
// Make the following APIs available in global scope for invocation from JS
78
window['Blazor'] = {
@@ -12,5 +13,6 @@ window['Blazor'] = {
1213
navigationManager: navigationManagerInternalFunctions,
1314
domWrapper: domFunctions,
1415
setProfilingEnabled: setProfilingEnabled,
16+
Virtualize,
1517
},
1618
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
function findClosestScrollContainer(element: Element | null): Element | null {
2+
if (!element) {
3+
return null;
4+
}
5+
6+
const style = getComputedStyle(element);
7+
8+
if (style.overflowY !== 'visible') {
9+
return element;
10+
}
11+
12+
return findClosestScrollContainer(element.parentElement);
13+
}
14+
15+
function init(component: any, topSpacer: Element, bottomSpacer: Element, rootMargin = 50): void {
16+
const intersectionObserver = new IntersectionObserver(intersectionCallback, {
17+
root: findClosestScrollContainer(topSpacer),
18+
rootMargin: `${rootMargin}px`,
19+
});
20+
21+
intersectionObserver.observe(topSpacer);
22+
intersectionObserver.observe(bottomSpacer);
23+
24+
const mutationObserver = new MutationObserver((): void => {
25+
intersectionObserver.unobserve(topSpacer);
26+
intersectionObserver.unobserve(bottomSpacer);
27+
intersectionObserver.observe(topSpacer);
28+
intersectionObserver.observe(bottomSpacer);
29+
});
30+
31+
mutationObserver.observe(topSpacer, { attributes: true });
32+
33+
function intersectionCallback(entries: IntersectionObserverEntry[]): void {
34+
entries.forEach((entry): void => {
35+
if (!entry.isIntersecting) {
36+
return;
37+
}
38+
39+
const containerSize = entry.rootBounds?.height;
40+
41+
if (entry.target === topSpacer) {
42+
component.invokeMethodAsync('OnTopSpacerVisible', entry.intersectionRect.top - entry.boundingClientRect.top, containerSize);
43+
} else if (entry.target === bottomSpacer) {
44+
component.invokeMethodAsync('OnBottomSpacerVisible', entry.boundingClientRect.bottom - entry.intersectionRect.bottom, containerSize);
45+
} else {
46+
throw new Error('Unknown intersection target');
47+
}
48+
});
49+
}
50+
}
51+
52+
export const Virtualize = {
53+
init,
54+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
using Microsoft.JSInterop;
4+
5+
namespace Microsoft.AspNetCore.Components.Virtualization
6+
{
7+
public class Virtualize<TItem> : VirtualizeBase<TItem>, IDisposable
8+
{
9+
private DotNetObjectReference<Virtualize<TItem>>? _selfReference;
10+
11+
[Inject]
12+
private IJSRuntime JSRuntime { get; set; } = default!;
13+
14+
protected override async Task OnAfterRenderAsync(bool firstRender)
15+
{
16+
if (firstRender)
17+
{
18+
_selfReference = DotNetObjectReference.Create(this);
19+
await JSRuntime.InvokeVoidAsync("Blazor._internal.Virtualize.init", _selfReference, TopSpacer, BottomSpacer);
20+
}
21+
}
22+
23+
[JSInvokable]
24+
public void OnTopSpacerVisible(float spacerSize, float containerSize)
25+
{
26+
UpdateTopSpacer(spacerSize, containerSize);
27+
}
28+
29+
[JSInvokable]
30+
public void OnBottomSpacerVisible(float spacerSize, float containerSize)
31+
{
32+
UpdateBottomSpacer(spacerSize, containerSize);
33+
}
34+
35+
public void Dispose()
36+
{
37+
_selfReference?.Dispose();
38+
}
39+
}
40+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
<option value="BasicTestApp.SvgWithChildComponent">SVG with child component</option>
8282
<option value="BasicTestApp.TextOnlyComponent">Plain text</option>
8383
<option value="BasicTestApp.TouchEventComponent">Touch events</option>
84+
<option value="BasicTestApp.VirtualizationComponent">Virtualization</option>
8485
<option value="BasicTestApp.SelectVariantsComponent">Select with component options</option>
8586
</select>
8687

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
@using Microsoft.AspNetCore.Components.Virtualization
2+
3+
<input type="number" @bind-value="itemSize" />
4+
5+
<div style="background-color: #eee; height: 500px; overflow-y: auto">
6+
<Virtualize Items="@Items" ItemSize="itemSize">
7+
<div @key="@context" style="height: @(itemSize)px; background-color: rgb(@((context % 2) * 255), @((1-(context % 2)) * 255), 255);">Item @context</div>
8+
</Virtualize>
9+
<Virtualize Items="@Items" ItemSize="itemSize">
10+
<div @key="@context" style="height: @(itemSize)px; background-color: rgb(0, @((context % 2) * 255), @((1-(context % 2)) * 255));">Item @context</div>
11+
</Virtualize>
12+
</div>
13+
14+
15+
@code {
16+
int itemSize = 100;
17+
Random rng = new Random();
18+
ICollection<int> Items = Enumerable.Range(1, 1000).ToList();
19+
}

0 commit comments

Comments
 (0)