Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Virtualization support #24179

Merged
merged 24 commits into from
Aug 4, 2020
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0d90cbb
Basic virtualization prototype.
MackinnonBuck Jul 18, 2020
8d16b79
Started on asynchronous data fetching
MackinnonBuck Jul 21, 2020
202bf61
Restructured + infinite scroll
MackinnonBuck Jul 21, 2020
9d668c5
Fixed rebase issues.
MackinnonBuck Jul 22, 2020
bed2a6b
Implemented alternate approach suggested in CR.
MackinnonBuck Jul 24, 2020
1b76cff
Improved design which now supports fixed lists of fetched items
MackinnonBuck Jul 25, 2020
dbd87e7
Support for bubbling exceptions thrown in ItemsProvider.
MackinnonBuck Jul 27, 2020
25059f9
Small improvements
MackinnonBuck Jul 27, 2020
93fdd29
Merge branch 'release/5.0-preview8' of https://github.com/dotnet/aspn…
MackinnonBuck Jul 27, 2020
03d97a4
Added CancellationToken parameter to ItemsProviderDelegate.
MackinnonBuck Jul 28, 2020
6c2b79d
Combined VirtualizeDeferred and VirtualizeFixed, addressed CR feedback
MackinnonBuck Jul 29, 2020
8d0a884
Updated Virtualize to not cache items.
MackinnonBuck Jul 30, 2020
1aedbe0
Updated exception message.
MackinnonBuck Jul 30, 2020
efec547
Fixed inaccurate comments.
MackinnonBuck Jul 30, 2020
978562f
CR feedback.
MackinnonBuck Jul 30, 2020
02a03f4
Added E2E tests.
MackinnonBuck Jul 31, 2020
532c22c
Started on unit tests.
MackinnonBuck Jul 31, 2020
f2791a0
Added unit tests.
MackinnonBuck Jul 31, 2020
2bd0acd
Merge branch 'master' of https://github.com/dotnet/aspnetcore into t-…
MackinnonBuck Jul 31, 2020
3548a98
Fixed async tests.
MackinnonBuck Jul 31, 2020
99964b9
Merge branch 'master' of https://github.com/dotnet/aspnetcore into t-…
MackinnonBuck Jul 31, 2020
98b6aa6
Addressed API review feedback.
MackinnonBuck Aug 3, 2020
57d8416
Update VirtualizationComponent.razor
MackinnonBuck Aug 3, 2020
c091b1e
Changed the item template to require a key
MackinnonBuck Aug 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webassembly.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/Components/Web.JS/src/GlobalExports.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { navigateTo, internalFunctions as navigationManagerInternalFunctions } from './Services/NavigationManager';
import { attachRootComponentToElement } from './Rendering/Renderer';
import { domFunctions } from './DomWrapper';
import { Virtualize } from './Virtualize';

// Make the following APIs available in global scope for invocation from JS
window['Blazor'] = {
Expand All @@ -10,5 +11,6 @@ window['Blazor'] = {
attachRootComponentToElement,
navigationManager: navigationManagerInternalFunctions,
domWrapper: domFunctions,
Virtualize,
},
};
86 changes: 86 additions & 0 deletions src/Components/Web.JS/src/Virtualize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
export const Virtualize = {
init,
dispose,
};

const observersByDotNetId = {};

function findClosestScrollContainer(element: Element | null): Element | null {
if (!element) {
return null;
}

const style = getComputedStyle(element);

if (style.overflowY !== 'visible') {
MackinnonBuck marked this conversation as resolved.
Show resolved Hide resolved
return element;
}

return findClosestScrollContainer(element.parentElement);
}

function init(dotNetHelper: any, spacerBefore: HTMLElement, spacerAfter: HTMLElement, rootMargin = 50): void {
const intersectionObserver = new IntersectionObserver(intersectionCallback, {
root: findClosestScrollContainer(spacerBefore),
rootMargin: `${rootMargin}px`,
});

intersectionObserver.observe(spacerBefore);
intersectionObserver.observe(spacerAfter);

const mutationObserverBefore = createSpacerMutationObserver(spacerBefore);
const mutationObserverAfter = createSpacerMutationObserver(spacerAfter);

observersByDotNetId[dotNetHelper._id] = {
SteveSandersonMS marked this conversation as resolved.
Show resolved Hide resolved
intersectionObserver,
mutationObserverBefore,
mutationObserverAfter,
};

function createSpacerMutationObserver(spacer: HTMLElement): MutationObserver {
// Without the use of thresholds, IntersectionObserver only detects binary changes in visibility,
// so if a spacer gets resized but remains visible, no additional callbacks will occur. By unobserving
// and reobserving spacers when they get resized, the intersection callback will re-run if they remain visible.
MackinnonBuck marked this conversation as resolved.
Show resolved Hide resolved
const mutationObserver = new MutationObserver((): void => {
intersectionObserver.unobserve(spacer);
intersectionObserver.observe(spacer);
});

mutationObserver.observe(spacer, { attributes: true });

return mutationObserver;
}

function intersectionCallback(entries: IntersectionObserverEntry[]): void {
entries.forEach((entry): void => {
if (!entry.isIntersecting) {
return;
}

const containerSize = entry.rootBounds?.height;

if (entry.target === spacerBefore) {
dotNetHelper.invokeMethodAsync('OnSpacerBeforeVisible', entry.intersectionRect.top - entry.boundingClientRect.top, containerSize);
} else if (entry.target === spacerAfter && spacerAfter.offsetHeight > 0) {
// When we first start up, both the "before" and "after" spacers will be visible, but it's only relevant to raise a
// single event to load the initial data. To avoid raising two events, skip the one for the "after" spacer if we know
// it's meaningless to talk about any overlap into it.
dotNetHelper.invokeMethodAsync('OnSpacerAfterVisible', entry.boundingClientRect.bottom - entry.intersectionRect.bottom, containerSize);
}
});
}
}

function dispose(dotNetHelper: any): void {
const observers = observersByDotNetId[dotNetHelper._id];

if (observers) {
observers.intersectionObserver.disconnect();
observers.mutationObserverBefore.disconnect();
observers.mutationObserverAfter.disconnect();

dotNetHelper.dispose();

delete observersByDotNetId[dotNetHelper._id];
}
}
11 changes: 11 additions & 0 deletions src/Components/Web/src/Virtualization/IVirtualizeJsCallbacks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// 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.Web.Virtualization
{
internal interface IVirtualizeJsCallbacks
{
void OnBeforeSpacerVisible(float spacerSize, float containerSize);
void OnAfterSpacerVisible(float spacerSize, float containerSize);
}
}
15 changes: 15 additions & 0 deletions src/Components/Web/src/Virtualization/ItemsProviderDelegate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// 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.

using System.Threading.Tasks;

namespace Microsoft.AspNetCore.Components.Web.Virtualization
{
/// <summary>
/// A function that provides items to a virtualized source.
/// </summary>
/// <typeparam name="TItem">The type of the context for each item in the list.</typeparam>
/// <param name="request">The <see cref="ItemsProviderRequest"/> defining the request details.</param>
/// <returns>A <see cref="ValueTask"/> whose result is a <see cref="ItemsProviderResult{TItem}"/> upon successful completion.</returns>
public delegate ValueTask<ItemsProviderResult<TItem>> ItemsProviderDelegate<TItem>(ItemsProviderRequest request);
}
44 changes: 44 additions & 0 deletions src/Components/Web/src/Virtualization/ItemsProviderRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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.

using System.Threading;

namespace Microsoft.AspNetCore.Components.Web.Virtualization
{
/// <summary>
/// Represents a request to an <see cref="ItemsProviderDelegate{TItem}"/>.
/// </summary>
public readonly struct ItemsProviderRequest
{
/// <summary>
/// The start index of the data segment requested.
/// </summary>
public int StartIndex { get; }

/// <summary>
/// The requested number of items to be provided. The actual number of provided items does not need to match
/// this value.
/// </summary>
public int Count { get; }

/// <summary>
/// The <see cref="System.Threading.CancellationToken"/> used to relay cancellation of the request.
/// </summary>
public CancellationToken CancellationToken { get; }

/// <summary>
/// Constructs a new <see cref="ItemsProviderRequest"/> instance.
/// </summary>
/// <param name="startIndex">The start index of the data segment requested.</param>
/// <param name="count">The requested number of items to be provided.</param>
/// <param name="cancellationToken">
/// The <see cref="System.Threading.CancellationToken"/> used to relay cancellation of the request.
/// </param>
public ItemsProviderRequest(int startIndex, int count, CancellationToken cancellationToken)
{
StartIndex = startIndex;
Count = count;
CancellationToken = cancellationToken;
}
}
}
35 changes: 35 additions & 0 deletions src/Components/Web/src/Virtualization/ItemsProviderResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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.

using System.Collections.Generic;

namespace Microsoft.AspNetCore.Components.Web.Virtualization
{
/// <summary>
/// Represents the result of a <see cref="ItemsProviderDelegate{TItem}"/>.
/// </summary>
/// <typeparam name="TItem">The type of the context for each item in the list.</typeparam>
public readonly struct ItemsProviderResult<TItem>
{
/// <summary>
/// The items to provide.
/// </summary>
public IEnumerable<TItem> Items { get; }

/// <summary>
/// The total item count in the source generating the items provided.
/// </summary>
public int TotalItemCount { get; }

/// <summary>
/// Instantiates a new <see cref="ItemsProviderResult{TItem}"/> instance.
/// </summary>
/// <param name="items">The items to provide.</param>
/// <param name="totalItemCount">The total item count in the source generating the items provided.</param>
public ItemsProviderResult(IEnumerable<TItem> items, int totalItemCount)
{
Items = items;
TotalItemCount = totalItemCount;
}
}
}
25 changes: 25 additions & 0 deletions src/Components/Web/src/Virtualization/PlaceholderContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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.Web.Virtualization
{
/// <summary>
/// Contains context for a placeholder in a virtualized list.
/// </summary>
public readonly struct PlaceholderContext
{
/// <summary>
/// The item index of the placeholder.
/// </summary>
public int Index { get; }

/// <summary>
/// Constructs a new <see cref="PlaceholderContext"/> instance.
/// </summary>
/// <param name="index">The item index of the placeholder.</param>
public PlaceholderContext(int index)
{
Index = index;
}
}
}
Loading