From decd621d4be0079261e9aa90cf747a384f4b5988 Mon Sep 17 00:00:00 2001 From: Luke Latham Date: Fri, 2 Oct 2020 04:07:07 -0500 Subject: [PATCH] Add Virtualize component example to Blazor doc (#20084) --- .../blazor/components/virtualization.md | 211 +++++++++++++++++- aspnetcore/data/ef-rp/sort-filter-page.md | 2 +- 2 files changed, 208 insertions(+), 5 deletions(-) diff --git a/aspnetcore/blazor/components/virtualization.md b/aspnetcore/blazor/components/virtualization.md index 62d6e47f24..ca03881ab0 100644 --- a/aspnetcore/blazor/components/virtualization.md +++ b/aspnetcore/blazor/components/virtualization.md @@ -5,7 +5,7 @@ description: Learn how to use component virtualization in ASP.NET Core Blazor ap monikerRange: '>= aspnetcore-3.1' ms.author: riande ms.custom: mvc -ms.date: 09/22/2020 +ms.date: 10/02/2020 no-loc: ["ASP.NET Core Identity", cookie, Cookie, Blazor, "Blazor Server", "Blazor WebAssembly", "Identity", "Let's Encrypt", Razor, SignalR] uid: blazor/components/virtualization --- @@ -134,10 +134,213 @@ The size of each item in pixels can be set with `ItemSize` (default: 50px): ::: moniker range="< aspnetcore-5.0" -For example, a grid or list that renders hundreds of rows containing components is processor intensive to render. Consider virtualizing a grid or list layout so that only a subset of the components is rendered at any given time. For an example of component subset rendering, see the following components in the [`Virtualization` sample app (aspnet/samples GitHub repository)](https://github.com/aspnet/samples/tree/master/samples/aspnetcore/blazor/Virtualization): +For example, a grid or list that renders hundreds of rows containing components is processor intensive to render. Consider virtualizing a grid or list layout so that only a subset of the components is rendered at any given time. -* `Virtualize` component ([`Shared/Virtualize.razor`](https://github.com/aspnet/samples/blob/master/samples/aspnetcore/blazor/Virtualization/Shared/Virtualize.cs)): A component written in C# that implements to render a set of weather data rows based on user scrolling. -* `FetchData` component ([`Pages/FetchData.razor`](https://github.com/aspnet/samples/blob/master/samples/aspnetcore/blazor/Virtualization/Pages/FetchData.razor)): Uses the `Virtualize` component to display 25 rows of weather data at a time. +The following `Virtualize` component (`Virtualize.cs`) implements to render child content based on user scrolling: + +```csharp +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.JSInterop; + +public class Virtualize : ComponentBase +{ + [Parameter] + public string TagName { get; set; } = "div"; + + [Parameter] + public RenderFragment ChildContent { get; set; } + + [Parameter] + public ICollection Items { get; set; } + + [Parameter] + public double ItemHeight { get; set; } + + [Parameter(CaptureUnmatchedValues = true)] + public Dictionary Attributes { get; set; } + + [Inject] + IJSRuntime JS { get; set; } + + ElementReference contentElement; + int numItemsToSkipBefore; + int numItemsToShow; + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + // Render actual content + builder.OpenElement(0, TagName); + builder.AddMultipleAttributes(1, Attributes); + + var translateY = numItemsToSkipBefore * ItemHeight; + builder.AddAttribute(2, "style", $"transform: translateY({ translateY }px);"); + builder.AddAttribute(2, "data-translateY", translateY); + builder.AddElementReferenceCapture(3, @ref => { contentElement = @ref; }); + + // As an important optimization, *don't* use builder.AddContent(seq, ChildContent, item) + // because that implicitly wraps a new region around each item, which in turn means that + // @key does nothing (because keys are scoped to regions). Instead, create a single + // container region and then invoke the fragments directly. + + builder.OpenRegion(4); + + foreach (var item in Items.Skip(numItemsToSkipBefore).Take(numItemsToShow)) + { + ChildContent(item)(builder); + } + + builder.CloseRegion(); + + builder.CloseElement(); + + // Also emit a spacer that causes the total vertical height to add up to + // Items.Count()*numItems + + builder.OpenElement(5, "div"); + var numHiddenItems = Items.Count - numItemsToShow; + builder.AddAttribute(6, "style", + $"width: 1px; height: { numHiddenItems * ItemHeight }px;"); + builder.CloseElement(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + var objectRef = DotNetObjectReference.Create(this); + var initResult = await JS.InvokeAsync( + "VirtualizedComponent._initialize", objectRef, contentElement); + OnScroll(initResult); + } + } + + [JSInvokable] + public void OnScroll(ScrollEventArgs args) + { + var relativeTop = args.ContainerRect.Top - args.ContentRect.Top; + numItemsToSkipBefore = Math.Max(0, (int)(relativeTop / ItemHeight)); + + var visibleHeight = args.ContainerRect.Bottom - + (args.ContentRect.Top + numItemsToSkipBefore * ItemHeight); + numItemsToShow = (int)Math.Ceiling(visibleHeight / ItemHeight) + 3; + + StateHasChanged(); + } + + public class ScrollEventArgs + { + public DOMRect ContainerRect { get; set; } + public DOMRect ContentRect { get; set; } + } + + public class DOMRect + { + public double Top { get; set; } + public double Bottom { get; set; } + public double Left { get; set; } + public double Right { get; set; } + public double Width { get; set; } + public double Height { get; set; } + } +} +``` + +The following `FetchData` component (`FetchData.razor`) uses the preceding `Virtualize` component to display 25 rows of weather data at a time: + +```razor +@page "/" +@page "/fetchdata" +@inject HttpClient Http + +

Weather forecast

+ +

This component demonstrates fetching data from a service.

+ +@if (forecasts == null) +{ +

Loading...

+} +else +{ +

Using table-layout: fixed

+
+ + + @context.Date.ToShortDateString() + @context.TemperatureC + @context.TemperatureF + @context.Summary + + +
+ +

Using position: sticky

+
+ + + + + + + + + + + + + + + + + + +
DateTemperature (C)Temperature (F)Summary
@context.Date.ToShortDateString()@context.TemperatureC@context.TemperatureF@context.Summary
+
+ + +} + +@code { + private WeatherForecast[] forecasts; + + protected override async Task OnInitializedAsync() + { + forecasts = await Http.GetFromJsonAsync( + "sample-data/weather.json"); + } + + public class WeatherForecast + { + public DateTime Date { get; set; } + + public int TemperatureC { get; set; } + + public string Summary { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + } +} +``` + +In the preceding example, the approach is about avoiding absolute positioning for each individual item. Absolute positioning would have some advantages (we can be sure the items do take up the specified amount of Y space) but has the bad disadvantage that you lose the normal widths and can't get table columns to line up across rows/header when based on content sizing. + +The concept behind the design of the `Virtualize` component is that the component doesn't change how the items are laid out within the DOM. There are no added wrapper elements, besides the single one whose `TagName` you specify. + +The best approach is to avoid even the `TagName` wrapper element. Have the `Virtualize` component emit no elements of its own. All the component does is render however many of the template instances are required to fill up any remaining visible space in the closest scrollable ancestor, plus add a following spacer element that makes the scrollable ancestor have the right scrolling range. As far as the layout is concerned, it's the same as if the full range of children were physically in the DOM. It does require you to specify an accurate `ItemHeight` though. If you get it wrong (for example, because you're confused and think it's valid to specify `style.height` on a ``), then the component ends up rendering the wrong number of template instances and either not fill up the UI or inefficiently render too many. Additionally, the scroll range on the parent won't be correct. ::: moniker-end diff --git a/aspnetcore/data/ef-rp/sort-filter-page.md b/aspnetcore/data/ef-rp/sort-filter-page.md index 7a0c75bed8..eee9c16a4b 100644 --- a/aspnetcore/data/ef-rp/sort-filter-page.md +++ b/aspnetcore/data/ef-rp/sort-filter-page.md @@ -176,7 +176,7 @@ The preceding code: * Saves the sort order in the `CurrentSort` property. * Resets page index to 1 when there's a new search string. * Uses the `PaginatedList` class to get Student entities. -* Sets `pageSize` to 3. A real app would use [Configuration](xref:fundamentals/configuration) to set the page size value. +* Sets `pageSize` to 3. A real app would use [Configuration](xref:fundamentals/configuration/index) to set the page size value. All the parameters that `OnGetAsync` receives are null when: