From 0d90cbb9d04d465e925ccc49d820f2210a8e80dd Mon Sep 17 00:00:00 2001
From: Mackinnon Buck
Date: Fri, 17 Jul 2020 18:56:44 -0700
Subject: [PATCH 01/21] Basic virtualization prototype.
---
AspNetCore.sln | 8 ++
.../src/Virtualization/VirtualizeBase.cs | 93 +++++++++++++++++++
src/Components/Web.JS/src/GlobalExports.ts | 2 +
src/Components/Web.JS/src/Virtualize.ts | 54 +++++++++++
.../Web/src/Virtualization/Virtualize.cs | 40 ++++++++
.../test/testassets/BasicTestApp/Index.razor | 1 +
.../VirtualizationComponent.razor | 19 ++++
7 files changed, 217 insertions(+)
create mode 100644 src/Components/Components/src/Virtualization/VirtualizeBase.cs
create mode 100644 src/Components/Web.JS/src/Virtualize.ts
create mode 100644 src/Components/Web/src/Virtualization/Virtualize.cs
create mode 100644 src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor
diff --git a/AspNetCore.sln b/AspNetCore.sln
index 50068277d886..3db9d70b808e 100644
--- a/AspNetCore.sln
+++ b/AspNetCore.sln
@@ -1430,12 +1430,20 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sdk", "Sdk", "{FED4267E-E5E4-49C5-98DB-8B3F203596EE}"
EndProject
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}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasicTestApp", "src\Components\test\testassets\BasicTestApp\BasicTestApp.csproj", "{46FB7E93-1294-4068-B80A-D4864F78277A}"
EndProject
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}"
EndProject
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}"
EndProject
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}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ComponentsApp.Server", "src\Components\test\testassets\ComponentsApp.Server\ComponentsApp.Server.csproj", "{19974360-4A63-425A-94DB-C2C940A3A97A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LazyTestContentPackage", "src\Components\test\testassets\LazyTestContentPackage\LazyTestContentPackage.csproj", "{ADF9C126-F322-4E34-AFD3-E626A4487206}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestContentPackage", "src\Components\test\testassets\TestContentPackage\TestContentPackage.csproj", "{3D3C7D9B-E356-4DC6-80B1-3F6D7F15EE31}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components.TestServer", "src\Components\test\testassets\TestServer\Components.TestServer.csproj", "{8A59AF88-4A82-46ED-977D-D909001F8107}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
diff --git a/src/Components/Components/src/Virtualization/VirtualizeBase.cs b/src/Components/Components/src/Virtualization/VirtualizeBase.cs
new file mode 100644
index 000000000000..1d92cb249d5e
--- /dev/null
+++ b/src/Components/Components/src/Virtualization/VirtualizeBase.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.AspNetCore.Components.Rendering;
+
+namespace Microsoft.AspNetCore.Components.Virtualization
+{
+ public abstract class VirtualizeBase : ComponentBase
+ {
+ private int _itemsAbove;
+
+ private int _itemsVisible;
+
+ private int _itemsBelow;
+
+ protected ElementReference TopSpacer { get; private set; }
+
+ protected ElementReference BottomSpacer { get; private set; }
+
+ [Parameter]
+ public ICollection Items { get; set; } = default!;
+
+ [Parameter]
+ public float ItemSize { get; set; }
+
+ [Parameter]
+ public RenderFragment? ChildContent { get; set; }
+
+ protected override void OnParametersSet()
+ {
+ if (Items == null)
+ {
+ throw new InvalidOperationException(
+ $"Parameter '{nameof(Items)}' must be specified and non-null.");
+ }
+
+ if (ItemSize <= 0f)
+ {
+ throw new InvalidOperationException(
+ $"Parameter '{nameof(ItemSize)}' must be specified and greater than zero.");
+ }
+
+ _itemsBelow = Items.Count;
+ }
+
+ protected void UpdateTopSpacer(float spacerSize, float containerSize)
+ => CalculateSpacerItemDistribution(spacerSize, containerSize, out _itemsAbove, out _itemsBelow);
+
+ protected void UpdateBottomSpacer(float spacerSize, float containerSize)
+ => CalculateSpacerItemDistribution(spacerSize, containerSize, out _itemsBelow, out _itemsAbove);
+
+ private void CalculateSpacerItemDistribution(float spacerSize, float containerSize, out int itemsInThisSpacer, out int itemsInOtherSpacer)
+ {
+ _itemsVisible = (int)(containerSize / ItemSize) + 1; // TODO: Custom number of "padding" elements?
+ itemsInThisSpacer = (int)(spacerSize / ItemSize);
+ itemsInOtherSpacer = Items.Count - itemsInThisSpacer - _itemsVisible;
+
+ StateHasChanged();
+ }
+
+ private string GetSpacerStyle(int itemsInSpacer)
+ {
+ return $"height: {itemsInSpacer * ItemSize}px;";
+ }
+
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.OpenElement(0, "div");
+ builder.AddAttribute(1, "key", "top-spacer");
+ builder.AddAttribute(2, "style", GetSpacerStyle(_itemsAbove));
+ builder.AddElementReferenceCapture(3, elementReference => TopSpacer = elementReference);
+ builder.CloseElement();
+
+ if (ChildContent != null)
+ {
+ builder.AddContent(4, new RenderFragment(builder =>
+ {
+ foreach (var item in Items.Skip(_itemsAbove).Take(_itemsVisible))
+ {
+ ChildContent(item)?.Invoke(builder);
+ }
+ }));
+ }
+
+ builder.OpenElement(5, "div");
+ builder.AddAttribute(6, "key", "bottom-spacer");
+ builder.AddAttribute(7, "style", GetSpacerStyle(_itemsBelow));
+ builder.AddElementReferenceCapture(8, elementReference =>
+ BottomSpacer = elementReference);
+ builder.CloseElement();
+ }
+ }
+}
diff --git a/src/Components/Web.JS/src/GlobalExports.ts b/src/Components/Web.JS/src/GlobalExports.ts
index 08f7557ba510..d09db420ae19 100644
--- a/src/Components/Web.JS/src/GlobalExports.ts
+++ b/src/Components/Web.JS/src/GlobalExports.ts
@@ -2,6 +2,7 @@ import { navigateTo, internalFunctions as navigationManagerInternalFunctions } f
import { attachRootComponentToElement } from './Rendering/Renderer';
import { domFunctions } from './DomWrapper';
import { setProfilingEnabled } from './Platform/Profiling';
+import { Virtualize } from './Virtualize';
// Make the following APIs available in global scope for invocation from JS
window['Blazor'] = {
@@ -12,5 +13,6 @@ window['Blazor'] = {
navigationManager: navigationManagerInternalFunctions,
domWrapper: domFunctions,
setProfilingEnabled: setProfilingEnabled,
+ Virtualize,
},
};
diff --git a/src/Components/Web.JS/src/Virtualize.ts b/src/Components/Web.JS/src/Virtualize.ts
new file mode 100644
index 000000000000..b4d11a771fd7
--- /dev/null
+++ b/src/Components/Web.JS/src/Virtualize.ts
@@ -0,0 +1,54 @@
+function findClosestScrollContainer(element: Element | null): Element | null {
+ if (!element) {
+ return null;
+ }
+
+ const style = getComputedStyle(element);
+
+ if (style.overflowY !== 'visible') {
+ return element;
+ }
+
+ return findClosestScrollContainer(element.parentElement);
+}
+
+function init(component: any, topSpacer: Element, bottomSpacer: Element, rootMargin = 50): void {
+ const intersectionObserver = new IntersectionObserver(intersectionCallback, {
+ root: findClosestScrollContainer(topSpacer),
+ rootMargin: `${rootMargin}px`,
+ });
+
+ intersectionObserver.observe(topSpacer);
+ intersectionObserver.observe(bottomSpacer);
+
+ const mutationObserver = new MutationObserver((): void => {
+ intersectionObserver.unobserve(topSpacer);
+ intersectionObserver.unobserve(bottomSpacer);
+ intersectionObserver.observe(topSpacer);
+ intersectionObserver.observe(bottomSpacer);
+ });
+
+ mutationObserver.observe(topSpacer, { attributes: true });
+
+ function intersectionCallback(entries: IntersectionObserverEntry[]): void {
+ entries.forEach((entry): void => {
+ if (!entry.isIntersecting) {
+ return;
+ }
+
+ const containerSize = entry.rootBounds?.height;
+
+ if (entry.target === topSpacer) {
+ component.invokeMethodAsync('OnTopSpacerVisible', entry.intersectionRect.top - entry.boundingClientRect.top, containerSize);
+ } else if (entry.target === bottomSpacer) {
+ component.invokeMethodAsync('OnBottomSpacerVisible', entry.boundingClientRect.bottom - entry.intersectionRect.bottom, containerSize);
+ } else {
+ throw new Error('Unknown intersection target');
+ }
+ });
+ }
+}
+
+export const Virtualize = {
+ init,
+};
diff --git a/src/Components/Web/src/Virtualization/Virtualize.cs b/src/Components/Web/src/Virtualization/Virtualize.cs
new file mode 100644
index 000000000000..04da2d8e7124
--- /dev/null
+++ b/src/Components/Web/src/Virtualization/Virtualize.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.JSInterop;
+
+namespace Microsoft.AspNetCore.Components.Virtualization
+{
+ public class Virtualize : VirtualizeBase, IDisposable
+ {
+ private DotNetObjectReference>? _selfReference;
+
+ [Inject]
+ private IJSRuntime JSRuntime { get; set; } = default!;
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender)
+ {
+ _selfReference = DotNetObjectReference.Create(this);
+ await JSRuntime.InvokeVoidAsync("Blazor._internal.Virtualize.init", _selfReference, TopSpacer, BottomSpacer);
+ }
+ }
+
+ [JSInvokable]
+ public void OnTopSpacerVisible(float spacerSize, float containerSize)
+ {
+ UpdateTopSpacer(spacerSize, containerSize);
+ }
+
+ [JSInvokable]
+ public void OnBottomSpacerVisible(float spacerSize, float containerSize)
+ {
+ UpdateBottomSpacer(spacerSize, containerSize);
+ }
+
+ public void Dispose()
+ {
+ _selfReference?.Dispose();
+ }
+ }
+}
diff --git a/src/Components/test/testassets/BasicTestApp/Index.razor b/src/Components/test/testassets/BasicTestApp/Index.razor
index aa87700d5b8e..c9e0ccb25096 100644
--- a/src/Components/test/testassets/BasicTestApp/Index.razor
+++ b/src/Components/test/testassets/BasicTestApp/Index.razor
@@ -81,6 +81,7 @@
+
diff --git a/src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor b/src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor
new file mode 100644
index 000000000000..dcf483dc15d4
--- /dev/null
+++ b/src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor
@@ -0,0 +1,19 @@
+@using Microsoft.AspNetCore.Components.Virtualization
+
+
+
+
+
+ Item @context
+
+
+ Item @context
+
+
+
+
+@code {
+ int itemSize = 100;
+ Random rng = new Random();
+ ICollection Items = Enumerable.Range(1, 1000).ToList();
+}
From 8d16b794213998c4022ad868ccca27e9ed6f48c7 Mon Sep 17 00:00:00 2001
From: Mackinnon Buck
Date: Mon, 20 Jul 2020 20:34:47 -0700
Subject: [PATCH 02/21] Started on asynchronous data fetching
---
.../src/Virtualization/VirtualizeBase.cs | 107 ++++++++++++++----
src/Components/Web.JS/src/Virtualize.ts | 21 ++++
.../Web/src/Virtualization/Virtualize.cs | 5 +-
.../VirtualizationComponent.razor | 40 ++++++-
4 files changed, 148 insertions(+), 25 deletions(-)
diff --git a/src/Components/Components/src/Virtualization/VirtualizeBase.cs b/src/Components/Components/src/Virtualization/VirtualizeBase.cs
index 1d92cb249d5e..09053366a639 100644
--- a/src/Components/Components/src/Virtualization/VirtualizeBase.cs
+++ b/src/Components/Components/src/Virtualization/VirtualizeBase.cs
@@ -1,18 +1,29 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
namespace Microsoft.AspNetCore.Components.Virtualization
{
public abstract class VirtualizeBase : ComponentBase
{
+ private readonly ConcurrentQueue _loadedItems = new ConcurrentQueue();
+
+ private readonly SemaphoreSlim _fetchSemaphore = new SemaphoreSlim(1);
+
private int _itemsAbove;
private int _itemsVisible;
private int _itemsBelow;
+ private IEnumerable LoadedItems => Items ?? (IEnumerable)_loadedItems;
+
+ private int ItemCount => Items?.Count ?? _loadedItems.Count;
+
protected ElementReference TopSpacer { get; private set; }
protected ElementReference BottomSpacer { get; private set; }
@@ -20,40 +31,70 @@ public abstract class VirtualizeBase : ComponentBase
[Parameter]
public ICollection Items { get; set; } = default!;
+ [Parameter]
+ public Func>> ItemsProvider { get; set; } = default!;
+
[Parameter]
public float ItemSize { get; set; }
+ [Parameter]
+ public int InitialItemsCount { get; set; }
+
[Parameter]
public RenderFragment? ChildContent { get; set; }
protected override void OnParametersSet()
{
- if (Items == null)
+ if (ItemSize <= 0f)
{
throw new InvalidOperationException(
- $"Parameter '{nameof(Items)}' must be specified and non-null.");
+ $"Parameter '{nameof(ItemSize)}' must be specified and greater than zero.");
}
- if (ItemSize <= 0f)
+ if (Items != null)
+ {
+ if (ItemsProvider != null)
+ {
+ throw new InvalidOperationException(
+ $"{GetType()} cannot have both '{nameof(Items)}' and '{nameof(ItemsProvider)}' parameters.");
+ }
+
+ _itemsBelow = Items.Count;
+ }
+ else if (ItemsProvider != null)
+ {
+ _itemsBelow = 0;
+ }
+ else
{
throw new InvalidOperationException(
- $"Parameter '{nameof(ItemSize)}' must be specified and greater than zero.");
+ $"{GetType()} requires either the '{nameof(Items)}' or '{nameof(ItemsProvider)}' parameter to " +
+ $"be specified and non-null.");
}
-
- _itemsBelow = Items.Count;
}
protected void UpdateTopSpacer(float spacerSize, float containerSize)
- => CalculateSpacerItemDistribution(spacerSize, containerSize, out _itemsAbove, out _itemsBelow);
+ {
+ CalculateSpacerItemDistribution(spacerSize, containerSize, out _itemsAbove, out _itemsBelow);
+ Console.WriteLine($"Above: {_itemsAbove}, Visible: {_itemsVisible}");
+ }
protected void UpdateBottomSpacer(float spacerSize, float containerSize)
- => CalculateSpacerItemDistribution(spacerSize, containerSize, out _itemsBelow, out _itemsAbove);
+ {
+ CalculateSpacerItemDistribution(spacerSize, containerSize, out _itemsBelow, out _itemsAbove);
+ Console.WriteLine($"Above: {_itemsAbove}, Visible: {_itemsVisible}");
+
+ if (ItemsProvider != null && _itemsAbove + _itemsVisible >= _loadedItems.Count)
+ {
+ FetchItems(_itemsAbove + _itemsVisible + InitialItemsCount);
+ }
+ }
private void CalculateSpacerItemDistribution(float spacerSize, float containerSize, out int itemsInThisSpacer, out int itemsInOtherSpacer)
{
- _itemsVisible = (int)(containerSize / ItemSize) + 1; // TODO: Custom number of "padding" elements?
- itemsInThisSpacer = (int)(spacerSize / ItemSize);
- itemsInOtherSpacer = Items.Count - itemsInThisSpacer - _itemsVisible;
+ _itemsVisible = Math.Max(0, (int)Math.Ceiling(containerSize / ItemSize) + 2);
+ itemsInThisSpacer = Math.Max(0, (int)Math.Floor(spacerSize / ItemSize) - 1);
+ itemsInOtherSpacer = Math.Max(0, ItemCount - itemsInThisSpacer - _itemsVisible);
StateHasChanged();
}
@@ -63,30 +104,58 @@ private string GetSpacerStyle(int itemsInSpacer)
return $"height: {itemsInSpacer * ItemSize}px;";
}
+ private void FetchItems(int newItemCount)
+ {
+ var currentScheduler = TaskScheduler.FromCurrentSynchronizationContext();
+
+ _fetchSemaphore.WaitAsync().ContinueWith(t =>
+ {
+ if (_loadedItems.Count >= newItemCount)
+ {
+ _fetchSemaphore.Release();
+ return;
+ }
+
+ ItemsProvider(_loadedItems.Count..newItemCount).ContinueWith(t =>
+ {
+ foreach (var item in t.Result)
+ {
+ _loadedItems.Enqueue(item);
+ }
+
+ StateHasChanged();
+
+ _fetchSemaphore.Release();
+ }, currentScheduler);
+ });
+ }
+
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
+ _itemsBelow = Math.Max(1, ItemCount - (_itemsVisible + _itemsAbove));
+
builder.OpenElement(0, "div");
builder.AddAttribute(1, "key", "top-spacer");
builder.AddAttribute(2, "style", GetSpacerStyle(_itemsAbove));
builder.AddElementReferenceCapture(3, elementReference => TopSpacer = elementReference);
builder.CloseElement();
+ builder.OpenRegion(4);
+
if (ChildContent != null)
{
- builder.AddContent(4, new RenderFragment(builder =>
+ foreach (var item in LoadedItems.Skip(_itemsAbove).Take(_itemsVisible))
{
- foreach (var item in Items.Skip(_itemsAbove).Take(_itemsVisible))
- {
- ChildContent(item)?.Invoke(builder);
- }
- }));
+ ChildContent(item)?.Invoke(builder);
+ }
}
+ builder.CloseRegion();
+
builder.OpenElement(5, "div");
builder.AddAttribute(6, "key", "bottom-spacer");
builder.AddAttribute(7, "style", GetSpacerStyle(_itemsBelow));
- builder.AddElementReferenceCapture(8, elementReference =>
- BottomSpacer = elementReference);
+ builder.AddElementReferenceCapture(8, elementReference => BottomSpacer = elementReference);
builder.CloseElement();
}
}
diff --git a/src/Components/Web.JS/src/Virtualize.ts b/src/Components/Web.JS/src/Virtualize.ts
index b4d11a771fd7..1a8863e8ed0e 100644
--- a/src/Components/Web.JS/src/Virtualize.ts
+++ b/src/Components/Web.JS/src/Virtualize.ts
@@ -1,3 +1,5 @@
+const observersByDotNetId = {};
+
function findClosestScrollContainer(element: Element | null): Element | null {
if (!element) {
return null;
@@ -30,6 +32,11 @@ function init(component: any, topSpacer: Element, bottomSpacer: Element, rootMar
mutationObserver.observe(topSpacer, { attributes: true });
+ observersByDotNetId[component._id] = {
+ intersection: intersectionObserver,
+ mutation: mutationObserver,
+ };
+
function intersectionCallback(entries: IntersectionObserverEntry[]): void {
entries.forEach((entry): void => {
if (!entry.isIntersecting) {
@@ -49,6 +56,20 @@ function init(component: any, topSpacer: Element, bottomSpacer: Element, rootMar
}
}
+function dispose(component: any): void {
+ const observers = observersByDotNetId[component._id];
+
+ if (observers) {
+ observers.intersection.disconnect();
+ observers.mutation.disconnect();
+
+ component.dispose();
+
+ delete observersByDotNetId[component.id];
+ }
+}
+
export const Virtualize = {
init,
+ dispose,
};
diff --git a/src/Components/Web/src/Virtualization/Virtualize.cs b/src/Components/Web/src/Virtualization/Virtualize.cs
index 04da2d8e7124..00080565323e 100644
--- a/src/Components/Web/src/Virtualization/Virtualize.cs
+++ b/src/Components/Web/src/Virtualization/Virtualize.cs
@@ -34,7 +34,10 @@ public void OnBottomSpacerVisible(float spacerSize, float containerSize)
public void Dispose()
{
- _selfReference?.Dispose();
+ if (_selfReference != null)
+ {
+ _ = JSRuntime.InvokeVoidAsync("Blazor._internal.Virtualize.dispose", _selfReference);
+ }
}
}
}
diff --git a/src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor b/src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor
index dcf483dc15d4..2534a4c673b0 100644
--- a/src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor
+++ b/src/Components/test/testassets/BasicTestApp/VirtualizationComponent.razor
@@ -1,19 +1,49 @@
@using Microsoft.AspNetCore.Components.Virtualization
-
+
+ Item size:
+
+
-
+
+ Scrollable div:
+
+
+ Item @context
+
+
+ Item @context
+
+
+
+
+
+ Async items:
+
+
+
+
+ Viewport as root:
Item @context
Item @context
-
-
+
@code {
- int itemSize = 100;
+ float itemSize = 100;
Random rng = new Random();
ICollection Items = Enumerable.Range(1, 1000).ToList();
+
+ Task> GetItems(Range range)
+ {
+ return Task.Delay(500).ContinueWith>(_ =>
+ Enumerable.Range(range.Start.Value, range.End.Value - range.Start.Value));
+ }
}
From 202bf618bcba3bd2eca8cbd590a3e12defc6cb8a Mon Sep 17 00:00:00 2001
From: Mackinnon Buck
Date: Tue, 21 Jul 2020 15:58:40 -0700
Subject: [PATCH 03/21] Restructured + infinite scroll
---
...rosoft.AspNetCore.Components.netcoreapp.cs | 646 ++++++++++++++++++
.../Virtualization/IVirtualizationHelper.cs | 32 +
.../Virtualization/IVirtualizationService.cs | 17 +
.../src/Virtualization/InfiniteScroll.cs | 84 +++
.../src/Virtualization/SpacerEventArgs.cs | 34 +
.../src/Virtualization/Virtualize.cs | 36 +
.../src/Virtualization/VirtualizeBase.cs | 182 +++--
.../ComponentServiceCollectionExtensions.cs | 2 +
src/Components/Web.JS/src/Virtualize.ts | 29 +-
...ft.AspNetCore.Components.Web.netcoreapp.cs | 498 ++++++++++++++
.../Web/src/Virtualization/Virtualize.cs | 43 --
.../Virtualization/WebVirtualizationHelper.cs | 71 ++
.../WebVirtualizationService.cs | 28 +
.../src/Hosting/WebAssemblyHostBuilder.cs | 2 +
.../VirtualizationComponent.razor | 15 +-
15 files changed, 1553 insertions(+), 166 deletions(-)
create mode 100644 src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp.cs
create mode 100644 src/Components/Components/src/Virtualization/IVirtualizationHelper.cs
create mode 100644 src/Components/Components/src/Virtualization/IVirtualizationService.cs
create mode 100644 src/Components/Components/src/Virtualization/InfiniteScroll.cs
create mode 100644 src/Components/Components/src/Virtualization/SpacerEventArgs.cs
create mode 100644 src/Components/Components/src/Virtualization/Virtualize.cs
create mode 100644 src/Components/Web/ref/Microsoft.AspNetCore.Components.Web.netcoreapp.cs
delete mode 100644 src/Components/Web/src/Virtualization/Virtualize.cs
create mode 100644 src/Components/Web/src/Virtualization/WebVirtualizationHelper.cs
create mode 100644 src/Components/Web/src/Virtualization/WebVirtualizationService.cs
diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp.cs
new file mode 100644
index 000000000000..eacc22699a97
--- /dev/null
+++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netcoreapp.cs
@@ -0,0 +1,646 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Components
+{
+ public static partial class BindConverter
+ {
+ public static bool FormatValue(bool value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string FormatValue(System.DateTime value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string FormatValue(System.DateTime value, string format, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string FormatValue(System.DateTimeOffset value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string FormatValue(System.DateTimeOffset value, string format, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string FormatValue(decimal value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string? FormatValue(double value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string FormatValue(short value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string? FormatValue(int value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string FormatValue(long value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static bool? FormatValue(bool? value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string? FormatValue(System.DateTimeOffset? value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string? FormatValue(System.DateTimeOffset? value, string format, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string? FormatValue(System.DateTime? value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string? FormatValue(System.DateTime? value, string? format, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string? FormatValue(decimal? value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string? FormatValue(double? value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string? FormatValue(short? value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string? FormatValue(int? value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string? FormatValue(long? value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string? FormatValue(float? value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string FormatValue(float value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static string? FormatValue(string? value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static object? FormatValue(T value, System.Globalization.CultureInfo? culture = null) { throw null; }
+ public static bool TryConvertToBool(object? obj, System.Globalization.CultureInfo? culture, out bool value) { throw null; }
+ public static bool TryConvertToDateTime(object? obj, System.Globalization.CultureInfo? culture, out System.DateTime value) { throw null; }
+ public static bool TryConvertToDateTime(object? obj, System.Globalization.CultureInfo? culture, string format, out System.DateTime value) { throw null; }
+ public static bool TryConvertToDateTimeOffset(object? obj, System.Globalization.CultureInfo? culture, out System.DateTimeOffset value) { throw null; }
+ public static bool TryConvertToDateTimeOffset(object? obj, System.Globalization.CultureInfo? culture, string format, out System.DateTimeOffset value) { throw null; }
+ public static bool TryConvertToDecimal(object? obj, System.Globalization.CultureInfo? culture, out decimal value) { throw null; }
+ public static bool TryConvertToDouble(object? obj, System.Globalization.CultureInfo? culture, out double value) { throw null; }
+ public static bool TryConvertToFloat(object? obj, System.Globalization.CultureInfo? culture, out float value) { throw null; }
+ public static bool TryConvertToInt(object? obj, System.Globalization.CultureInfo? culture, out int value) { throw null; }
+ public static bool TryConvertToLong(object? obj, System.Globalization.CultureInfo? culture, out long value) { throw null; }
+ public static bool TryConvertToNullableBool(object? obj, System.Globalization.CultureInfo? culture, out bool? value) { throw null; }
+ public static bool TryConvertToNullableDateTime(object? obj, System.Globalization.CultureInfo? culture, out System.DateTime? value) { throw null; }
+ public static bool TryConvertToNullableDateTime(object? obj, System.Globalization.CultureInfo? culture, string format, out System.DateTime? value) { throw null; }
+ public static bool TryConvertToNullableDateTimeOffset(object? obj, System.Globalization.CultureInfo? culture, out System.DateTimeOffset? value) { throw null; }
+ public static bool TryConvertToNullableDateTimeOffset(object? obj, System.Globalization.CultureInfo? culture, string format, out System.DateTimeOffset? value) { throw null; }
+ public static bool TryConvertToNullableDecimal(object? obj, System.Globalization.CultureInfo? culture, out decimal? value) { throw null; }
+ public static bool TryConvertToNullableDouble(object? obj, System.Globalization.CultureInfo? culture, out double? value) { throw null; }
+ public static bool TryConvertToNullableFloat(object? obj, System.Globalization.CultureInfo? culture, out float? value) { throw null; }
+ public static bool TryConvertToNullableInt(object? obj, System.Globalization.CultureInfo? culture, out int? value) { throw null; }
+ public static bool TryConvertToNullableLong(object? obj, System.Globalization.CultureInfo? culture, out long? value) { throw null; }
+ public static bool TryConvertToNullableShort(object? obj, System.Globalization.CultureInfo? culture, out short? value) { throw null; }
+ public static bool TryConvertToShort(object? obj, System.Globalization.CultureInfo? culture, out short value) { throw null; }
+ public static bool TryConvertToString(object? obj, System.Globalization.CultureInfo? culture, out string? value) { throw null; }
+ public static bool TryConvertTo(object? obj, System.Globalization.CultureInfo? culture, out T value) { throw null; }
+ }
+ [System.AttributeUsageAttribute(System.AttributeTargets.Class, AllowMultiple=true, Inherited=true)]
+ public sealed partial class BindElementAttribute : System.Attribute
+ {
+ public BindElementAttribute(string element, string? suffix, string valueAttribute, string changeAttribute) { }
+ public string ChangeAttribute { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
+ public string Element { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
+ public string? Suffix { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
+ public string ValueAttribute { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
+ }
+ [System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
+ public sealed partial class CascadingParameterAttribute : System.Attribute
+ {
+ public CascadingParameterAttribute() { }
+ public string? Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ }
+ public partial class CascadingValue : Microsoft.AspNetCore.Components.IComponent
+ {
+ public CascadingValue() { }
+ [Microsoft.AspNetCore.Components.ParameterAttribute]
+ public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ [Microsoft.AspNetCore.Components.ParameterAttribute]
+ public bool IsFixed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ [Microsoft.AspNetCore.Components.ParameterAttribute]
+ public string? Name { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ [Microsoft.AspNetCore.Components.ParameterAttribute]
+ public TValue Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public void Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
+ public System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
+ }
+ public partial class ChangeEventArgs : System.EventArgs
+ {
+ public ChangeEventArgs() { }
+ public object? Value { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ }
+ public abstract partial class ComponentBase : Microsoft.AspNetCore.Components.IComponent, Microsoft.AspNetCore.Components.IHandleAfterRender, Microsoft.AspNetCore.Components.IHandleEvent
+ {
+ public ComponentBase() { }
+ protected virtual void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder) { }
+ protected System.Threading.Tasks.Task InvokeAsync(System.Action workItem) { throw null; }
+ protected System.Threading.Tasks.Task InvokeAsync(System.Func workItem) { throw null; }
+ void Microsoft.AspNetCore.Components.IComponent.Attach(Microsoft.AspNetCore.Components.RenderHandle renderHandle) { }
+ System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleAfterRender.OnAfterRenderAsync() { throw null; }
+ System.Threading.Tasks.Task Microsoft.AspNetCore.Components.IHandleEvent.HandleEventAsync(Microsoft.AspNetCore.Components.EventCallbackWorkItem callback, object? arg) { throw null; }
+ protected virtual void OnAfterRender(bool firstRender) { }
+ protected virtual System.Threading.Tasks.Task OnAfterRenderAsync(bool firstRender) { throw null; }
+ protected virtual void OnInitialized() { }
+ protected virtual System.Threading.Tasks.Task OnInitializedAsync() { throw null; }
+ protected virtual void OnParametersSet() { }
+ protected virtual System.Threading.Tasks.Task OnParametersSetAsync() { throw null; }
+ public virtual System.Threading.Tasks.Task SetParametersAsync(Microsoft.AspNetCore.Components.ParameterView parameters) { throw null; }
+ protected virtual bool ShouldRender() { throw null; }
+ protected void StateHasChanged() { }
+ }
+ public abstract partial class Dispatcher
+ {
+ protected Dispatcher() { }
+ public void AssertAccess() { }
+ public abstract bool CheckAccess();
+ public static Microsoft.AspNetCore.Components.Dispatcher CreateDefault() { throw null; }
+ public abstract System.Threading.Tasks.Task InvokeAsync(System.Action workItem);
+ public abstract System.Threading.Tasks.Task InvokeAsync(System.Func workItem);
+ public abstract System.Threading.Tasks.Task InvokeAsync(System.Func> workItem);
+ public abstract System.Threading.Tasks.Task InvokeAsync(System.Func workItem);
+ protected void OnUnhandledException(System.UnhandledExceptionEventArgs e) { }
+ }
+ [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
+ public readonly partial struct ElementReference
+ {
+ private readonly object _dummy;
+ private readonly int _dummyPrimitive;
+ public ElementReference(string id) { throw null; }
+ public ElementReference(string id, Microsoft.AspNetCore.Components.ElementReferenceContext? context) { throw null; }
+ public Microsoft.AspNetCore.Components.ElementReferenceContext? Context { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
+ public string Id { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
+ }
+ public abstract partial class ElementReferenceContext
+ {
+ protected ElementReferenceContext() { }
+ }
+ [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
+ public readonly partial struct EventCallback
+ {
+ private readonly object _dummy;
+ private readonly int _dummyPrimitive;
+ public static readonly Microsoft.AspNetCore.Components.EventCallback Empty;
+ public static readonly Microsoft.AspNetCore.Components.EventCallbackFactory Factory;
+ public EventCallback(Microsoft.AspNetCore.Components.IHandleEvent? receiver, System.MulticastDelegate? @delegate) { throw null; }
+ public bool HasDelegate { get { throw null; } }
+ public System.Threading.Tasks.Task InvokeAsync() { throw null; }
+ public System.Threading.Tasks.Task InvokeAsync(object arg) { throw null; }
+ }
+ public sealed partial class EventCallbackFactory
+ {
+ public EventCallbackFactory() { }
+ [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
+ public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, Microsoft.AspNetCore.Components.EventCallback callback) { throw null; }
+ public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, System.Action callback) { throw null; }
+ public Microsoft.AspNetCore.Components.EventCallback Create(object receiver, System.Action