ASP.NET Core Razor 元件虛擬化

注意

這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前版本,請參閱本文的 .NET 8 版本

本文說明如何在 ASP.NET Core Blazor 應用程式中使用元件虛擬化。

虛擬化

Virtualize<TItem> 元件與 Blazor 架構的內建虛擬化支援搭配使用,以改善元件轉譯的感知效能。 虛擬化是將使用者介面轉譯限制為目前可見部分的技術。 例如,當應用程式必須轉譯一長串的清單項目且在指定時間僅必須顯示部分項目時,虛擬化將提供幫助。

在下列狀況使用 Virtualize<TItem> 元件:

  • 在迴圈中轉譯資料項目集時。
  • 由於捲動而不會看見大部分項目時。
  • 轉譯項目大小相同時。

當使用者捲動至 Virtualize<TItem> 元件項目清單中的任一點時,元件會計算要顯示的可見項目。 不會轉譯未顯示的項目。

若未虛擬化,一般清單可能會使用 C# foreach 迴圈來轉譯清單中的每個項目。 在以下範例中:

  • allFlights 是飛機航班的集合。
  • FlightSummary 元件會顯示每個航班的詳細資料。
  • @key 指示詞屬性會透過航班的 FlightId 保留每個 FlightSummary 元件與其轉譯航班的關係。
<div style="height:500px;overflow-y:scroll">
    @foreach (var flight in allFlights)
    {
        <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
    }
</div>

如果集合包含數千個航班,則轉譯航班可能需要很長的時間且使用者會遇到明顯的使用者介面延遲。 由於大部分航班落在 <div> 元素高度之外,因此看不到這些航班。

請將上述範例中的 foreach 迴圈取代為 Virtualize<TItem> 元件,而不是一次轉譯整個航班清單:

  • allFlights 指定為 Virtualize<TItem>.Items 的固定項目來源。 Virtualize<TItem> 元件僅會轉譯目前可見航班。

    如果非泛型集合提供項目 (例如 DataRow 集合),請遵循 項目提供者委派 一節中的指導來提供項目。

  • 使用 Context 參數指定每個航班的內容。 在下列範例中,flight 會作為內容使用以提供每個航空成員的存取權。

<div style="height:500px;overflow-y:scroll">
    <Virtualize Items="allFlights" Context="flight">
        <FlightSummary @key="flight.FlightId" Details="@flight.Summary" />
    </Virtualize>
</div>

如果未使用 Context 內容指定內容,請使用項目內容範本中的 context 值以存取每個航班成員:

<div style="height:500px;overflow-y:scroll">
    <Virtualize Items="allFlights">
        <FlightSummary @key="context.FlightId" Details="@context.Summary" />
    </Virtualize>
</div>

Virtualize<TItem> 元件:

  • 根據容器高度和轉譯項目大小,計算要轉譯的項目數量。
  • 在使用者捲動時重新計算和轉譯項目。
  • 只有在使用 ItemsProvider 而不是 Items 時,才會從對應至目前可見區域的外部 API 擷取記錄的配量 (請參閱 項目提供者委派 一節)。

Virtualize<TItem> 元件的項目內容可包含:

  • 純 HRML 和 Razor 程式碼 (如上述範例所示)。
  • 一或多個 Razor 元件。
  • HTML/Razor 和 Razor 元件的混合。

項目提供者委派

如果您不想要將所有項目載入至記憶體或集合不是泛型 ICollection<T>,則您可對元件的 Virtualize<TItem>.ItemsProvider 參數指定項目提供者委派方法以依需求非同步擷取要求項目。 在下列範例中,LoadEmployees 方法會將項目提供給 Virtualize<TItem> 元件:

<Virtualize Context="employee" ItemsProvider="LoadEmployees">
    <p>
        @employee.FirstName @employee.LastName has the 
        job title of @employee.JobTitle.
    </p>
</Virtualize>

項目提供者會接收 ItemsProviderRequest,這會指定從特定開始索引開始的必要項目數量。 項目提供者接著會從資料庫或其他服務擷取要求項目,並以 ItemsProviderResult<TItem> 傳回這些項目和總計項目計數。 項目提供者依照每個要求選擇擷取項目,或快取這些項目以使其可供使用。

Virtualize<TItem> 元件僅可接受其參數的一個項目來源,因此請勿嘗試同步使用項目提供者和將集和指派給 Items。 如果同時指派兩者,則在執行階段設定元件的參數時,便會擲回 InvalidOperationException

下列範例會從 EmployeeService 載入員工 (不會顯示):

private async ValueTask<ItemsProviderResult<Employee>> LoadEmployees(
    ItemsProviderRequest request)
{
    var numEmployees = Math.Min(request.Count, totalEmployees - request.StartIndex);
    var employees = await EmployeesService.GetEmployeesAsync(request.StartIndex, 
        numEmployees, request.CancellationToken);

    return new ItemsProviderResult<Employee>(employees, totalEmployees);
}

在下列範例中,DataRow 集合為非泛型集合,因此項目提供者委派會用於虛擬化:

<Virtualize Context="row" ItemsProvider="GetRows">
    ...
</Virtualize>

@code{
    ...

    private ValueTask<ItemsProviderResult<DataRow>> GetRows(ItemsProviderRequest request)
    {
        return new(new ItemsProviderResult<DataRow>(
            dataTable.Rows.OfType<DataRow>().Skip(request.StartIndex).Take(request.Count),
            dataTable.Rows.Count));
    }
}

Virtualize<TItem>.RefreshDataAsync 會指示元件從其 ItemsProvider 重新要求資料。 當外部資料變更時,這會提供幫助。 使用 Items 時通常不需要呼叫 RefreshDataAsync

RefreshDataAsync 會更新 Virtualize<TItem> 元件的資料,而不會造成重新轉譯。 如果從 Blazor 事件處理常式或元件生命週期方法叫用 RefreshDataAsync,則由於轉譯會在事件處理常式或生命週期方法的結束時自動觸發,因此不需要觸發轉譯。 如果從背景工作或事件個別觸發 RefreshDataAsync (例如在下列 ForecastUpdated 委派中),請呼叫 StateHasChanged 以在背景工作或事件結束時更新使用者介面:

<Virtualize ... @ref="virtualizeComponent">
    ...
</Virtualize>

...

private Virtualize<FetchData>? virtualizeComponent;

protected override void OnInitialized()
{
    WeatherForecastSource.ForecastUpdated += async () => 
    {
        await InvokeAsync(async () =>
        {
            await virtualizeComponent?.RefreshDataAsync();
            StateHasChanged();
        });
    });
}

在前述範例中:

預留位置

由於從遠端資料來源請求項目時可能需要一些時間,因此您可選擇使用項目內容轉譯預留位置:

<Virtualize Context="employee" ItemsProvider="LoadEmployees">
    <ItemContent>
        <p>
            @employee.FirstName @employee.LastName has the 
            job title of @employee.JobTitle.
        </p>
    </ItemContent>
    <Placeholder>
        <p>
            Loading&hellip;
        </p>
    </Placeholder>
</Virtualize>

空白內容

當元件已載入且 Items 為空白或 ItemsProviderResult<TItem>.TotalItemCount 為零時,請使用 EmptyContent 參數提供內容。

EmptyContent.razor

@page "/empty-content"

<PageTitle>Empty Content</PageTitle>

<h1>Empty Content Example</h1>

<Virtualize Items="@stringList">
    <ItemContent>
        <p>
            @context
        </p>
    </ItemContent>
    <EmptyContent>
        <p>
            There are no strings to display.
        </p>
    </EmptyContent>
</Virtualize>

@code {
    private List<string>? stringList;

    protected override void OnInitialized() => stringList ??= new();
}

變更 OnInitialized 方法 Lambda 以查看元件顯示字串:

protected override void OnInitialized() =>
    stringList ??= new() { "Here's a string!", "Here's another string!" };

項目大小

每個項目的高度 (像素) 可設為 Virtualize<TItem>.ItemSize (預設:50)。 下列範例會將每個項目高度從預設 50 像素變更為 25 像素:

<Virtualize Context="employee" Items="employees" ItemSize="25">
    ...
</Virtualize>

依預設,Virtualize<TItem> 元件會在發生初始轉譯後,測量個別項目的轉譯大小 (高度)。 使用 ItemSize 來預先提供確切項目大小以協助精確初始轉譯執行,並確保頁面重新載入的正確捲動位置。 如果預設 ItemSize 會造成某些項目在目前可見檢視之外轉譯,則會觸發第二次重新轉譯。 若要正確維護瀏覽器在虛擬化清單的捲動位置,初始轉譯必須正確。 如果沒有,使用者可能會檢視錯誤的項目。

過度掃描計數

Virtualize<TItem>.OverscanCount 會決定可見區域前後轉譯其他項目的數量。 此設定可協助您減少捲動期間轉譯頻率。 不過,較高的值會導致在頁面中轉譯多個元素 (預設:3)。 下列範例會將過度掃描計數從預設三個項目變更為四個項目:

<Virtualize Context="employee" Items="employees" OverscanCount="4">
    ...
</Virtualize>

狀態變更

當對 Virtualize<TItem> 元件轉譯的項目進行變更時,請呼叫 StateHasChanged 以強制執行重新評估及重新轉譯元件。 如需詳細資訊,請參閱 ASP.NET Core Razor 元件轉譯

鍵盤捲動支援

若要允許使用者使用鍵盤捲動虛擬化內容,請確保虛擬化元素或捲動容器本身可設定焦點。 如果您無法執行此步驟,鍵盤捲動無法在以 Chromium 為基礎的瀏覽器中運作。

例如,您可在捲動容器上使用 tabindex 屬性:

<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <Virtualize Items="allFlights">
        <div class="flight-info">...</div>
    </Virtualize>
</div>

若要深入瞭解 tabindex 值 (-10 或其他值) 的意義,請參閱 tabindex (MDN 文件)

進階樣式和捲動偵測

Virtualize<TItem> 元件設計目的僅為支援特定元素配置機制。 若要瞭解哪些元件配置可正常運作,下列說明 Virtualize 如何可偵測那些元素可見並顯示在正確位置。

如果原始程式碼如下所示:

<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <Virtualize Items="allFlights" ItemSize="100">
        <div class="flight-info">Flight @context.Id</div>
    </Virtualize>
</div>

在執行階段,Virtualize<TItem> 元素會轉譯與下列範例相似的 DOM 結構:

<div style="height:500px; overflow-y:scroll" tabindex="-1">
    <div style="height:1100px"></div>
    <div class="flight-info">Flight 12</div>
    <div class="flight-info">Flight 13</div>
    <div class="flight-info">Flight 14</div>
    <div class="flight-info">Flight 15</div>
    <div class="flight-info">Flight 16</div>
    <div style="height:3400px"></div>
</div>

轉譯的實際列數目和空白字元大小會根據您的樣式和 Items 集合大小而有所不同。 不過,請注意,在內容前後已插入空白字元 div 元素。 這兩個用途如下:

  • 若要提供內容前後的位移,使目前可見項目顯示在捲動範圍中的正確位置,而捲動範圍本視可表示所有內容的總計大小。
  • 若要偵測使用者何時捲動超出目前可見範圍,這表示必須轉譯不同內容。

注意

若要瞭解如何控制空白字元 HTML 元素標籤,請參閱本文稍後的控制空白字元元素標籤名稱一節。

空白字元元素會在內部使用 Intersection Observer (交集觀察程式),在可見時接收通知。 Virtualize 取決於接收這些事件。

Virtualize 適用於下列條間:

  • 所有轉譯的內容項目 (包括 預留位置內容) 的高度均相同。 這可讓您計算哪些內容對應至指定的捲動位置,而不需要先擷取每個資料項目並將資料轉譯成 DOM 元素。

  • 空白字元和內容列會在單一垂直堆疊中轉譯,每個項目都會填滿整個水平寬度。 這通常是預設值。 在 div 元素的一般案例中,Virtualize 依預設正常運作。 如果您使用 CSS 來建立更進階的配置,請記住下列需求:

    • 捲動容器樣式需要含下列任何值的 display
      • block (div 預設值)。
      • table-row-group (tbody 預設值)。
      • flex-direction 設定 columnflex。 請確保 Virtualize<TItem> 元件的直接子系不會在 Flex 規則下縮小。 例如,新增 .mycontainer > div { flex-shrink: 0 }
    • 內容列樣式需要含下列任一值的 display
      • block (div 預設值)。
      • table-row (tr 預設值)。
    • 請勿使用 CSS,因為會干擾空間字元元素的配置。 依預設,空白字元元素有 blockdisplay 值,除非父系是資料表列群組,在此情況下預設為 table-row。 請勿嘗試影響空白字元元素寬度或高度,包含造成空白字元元素有框線或 content 虛擬元素。

任何防止空白字元和內容元素轉譯為單一垂直堆疊的方法或使內容項目高度不同的方法,都會防止 Virtualize<TItem> 元件正確運作。

根層級虛擬化

Virtualize<TItem> 元件支援使用文件本身作為捲動根,這是使用 overflow-y: scroll 的其他元素替代方案。 在下列範例中,會使用 overflow-y: scroll 設定 <html><body> 的樣式:

<HeadContent>
    <style>
        html, body { overflow-y: scroll }
    </style>
</HeadContent>

Virtualize<TItem> 元件支援使用文件本身作為捲動根,這是使用 overflow-y: scroll 的其他元素替代方案。 當使用文件作為捲動根,請避免使用 overflow-y: scroll 設定 <html><body> 的樣式時,因為這會導致 Intersection Observer (交集觀察程式) 將頁面的完整可捲動高度視為可見區域,而不是僅視窗檢視區。

您可透過建立大型虛擬化清單 (例如 100,000 個項目) 以重現此問題,並嘗試在頁面 CSS 樣式中透過 html { overflow-y: scroll } 使用文件作為捲動根。 即使目前可正常運作,但瀏覽器嘗試在轉譯開始時至少轉譯一次所有 100,000 個項目時,這可能會導致瀏覽器索引標籤鎖定。

若要在 .NET 7 版本之前解決此問題,請避免使用 overflow-y: scroll 設定 <html>/<body> 的樣式,或採用替代方法。 在下列範例中,<html> 元素高度會設定為檢視區高度的 100# 以上:

<HeadContent>
    <style>
        html { min-height: calc(100vh + 0.3px) }
    </style>
</HeadContent>

Virtualize<TItem> 元件支援使用文件本身作為捲動根,這是使用 overflow-y: scroll 的其他元素替代方案。 當使用文件作為捲動根,請避免使用 overflow-y: scroll 設定 <html><body> 的樣式時,因為這會導致頁面的完整可捲動高度視為可見區域,而不是僅視窗檢視區。

您可透過建立大型虛擬化清單 (例如 100,000 個項目) 以重現此問題,並嘗試在頁面 CSS 樣式中透過 html { overflow-y: scroll } 使用文件作為捲動根。 即使目前可正常運作,但瀏覽器嘗試在轉譯開始時至少轉譯一次所有 100,000 個項目時,這可能會導致瀏覽器索引標籤鎖定。

若要在 .NET 7 版本之前解決此問題,請避免使用 overflow-y: scroll 設定 <html>/<body> 的樣式,或採用替代方法。 在下列範例中,<html> 元素高度會設定為檢視區高度的 100# 以上:

<style>
    html { min-height: calc(100vh + 0.3px) }
</style>

控制空白字元元素標籤名稱

如果 Virtualize<TItem> 元件放置於需要特定子標籤名稱的元素內,SpacerElement 可讓您取得或設定虛擬化空白字元標籤名稱。 預設值是 div。 針對下列範例,Virtualize<TItem> 元件會在資料表主體元素 (tbody) 內轉譯,因此資料表列 (tr) 的適當子項目會設定為空白字元。

VirtualizedTable.razor

@page "/virtualized-table"

<PageTitle>Virtualized Table</PageTitle>

<HeadContent>
    <style>
        html, body {
            overflow-y: scroll
        }
    </style>
</HeadContent>

<h1>Virtualized Table Example</h1>

<table id="virtualized-table">
    <thead style="position: sticky; top: 0; background-color: silver">
        <tr>
            <th>Item</th>
            <th>Another column</th>
        </tr>
    </thead>
    <tbody>
        <Virtualize Items="fixedItems" ItemSize="30" SpacerElement="tr">
            <tr @key="context" style="height: 30px;" id="row-@context">
                <td>Item @context</td>
                <td>Another value</td>
            </tr>
        </Virtualize>
    </tbody>
</table>

@code {
    private List<int> fixedItems = Enumerable.Range(0, 1000).ToList();
}

在上述範例中,文件根會作為捲動容器使用,因此會使用 overflow-y: scroll 設定 htmlbody 元素ㄉ的樣式。 如需詳細資訊,請參閱以下資源: