ASP.NET Core Razor 元件虛擬化
注意
這不是這篇文章的最新版本。 如需目前的版本,請參閱 本文的 .NET 9 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支持原則。 如需目前的版本,請參閱 本文的 .NET 9 版本。
本文說明如何在 ASP.NET Core Blazor 應用程式中使用元件虛擬化。
虛擬化
將 Blazor 元件與 Virtualize<TItem> 架構的內建虛擬化支援搭配使用,以改善元件轉譯的感知效能。 虛擬化是將使用者介面轉譯限制為目前可見部分的技術。 例如,當應用程式必須轉譯一長串的清單項目且在指定時間僅必須顯示部分項目時,虛擬化將提供幫助。
在下列狀況使用 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> 元件僅會渲染目前可見的航班。使用
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>
- 根據容器高度和轉譯項目大小,計算要轉譯的項目數量。
- 當使用者捲動時,重新計算並渲染項目。
- 只有在使用
ItemsProvider
而不是Items
的情況下,才會從對應至目前可見區域並包括超額掃描(overscan)的外部 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
的員工(未顯示)。 欄位 totalEmployees
通常會藉由在同一個服務上呼叫一個方法(例如在其他地方EmployeesService.GetEmployeesCountAsync
),例如元件初始化期間進行設定。
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) =>
new(new ItemsProviderResult<DataRow>(
dataTable.Rows.OfType<DataRow>().Skip(request.StartIndex).Take(request.Count),
dataTable.Rows.Count));
}
Virtualize<TItem>.RefreshDataAsync 會指示元件從其 ItemsProvider 重新要求資料。 當外部資料變更時,這會提供幫助。 使用 RefreshDataAsync 時通常不需要呼叫 Items。
RefreshDataAsync 會更新 Virtualize<TItem> 元件的資料,而不會造成重新轉譯。 如果從 RefreshDataAsync 事件處理常式或元件生命週期方法叫用 Blazor,則由於轉譯會在事件處理常式或生命週期方法的結束時自動觸發,因此不需要觸發轉譯。 如果從背景工作或事件中獨立觸發 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();
});
});
}
在前述範例中:
- 先呼叫RefreshDataAsync,以取得 Virtualize<TItem> 元件的新資料。
-
StateHasChanged
被呼叫以重新渲染元件。
佔位符
由於從遠端資料來源請求項目可能需要一些時間,因此您可以選擇使用項目內容作為預留位置進行呈現:
- 使用 Placeholder (
<Placeholder>...</Placeholder>
) 以顯示內容,直到項目資料可用為止。 - 使用 Virtualize<TItem>.ItemContent 以設定清單的項目範本。
<Virtualize Context="employee" ItemsProvider="LoadEmployees">
<ItemContent>
<p>
@employee.FirstName @employee.LastName has the
job title of @employee.JobTitle.
</p>
</ItemContent>
<Placeholder>
<p>
Loading…
</p>
</Placeholder>
</Virtualize>
空白內容
當元件已載入且 EmptyContent 為空白或 Items 為零時,請使用 ItemsProviderResult<TItem>.TotalItemCount 參數提供內容。
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 ??= [];
}
@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 ??= [];
}
變更 OnInitialized
方法 Lambda 以查看元件顯示字串:
protected override void OnInitialized() =>
stringList ??= [ "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
值 (-1
、0
或其他值) 的意義,請參閱 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
元素。 這兩個用途如下:
- 若要在內容前後提供偏移量,使目前可見項目顯示在捲動範圍中的正確位置,而捲動範圍本身能代表所有內容的總大小。
- 若要偵測使用者何時捲動超出目前可見範圍,這表示必須轉譯不同內容。
注意
若要瞭解如何控制 spacer HTML 元素標籤,請參閱本篇文章稍後的控制 spacer 元素標籤名稱一節。
間隔元素在內部使用 Intersection Observer (交集觀察器),在可見時接收通知。
Virtualize
取決於接收這些事件。
Virtualize
適用於下列條件:
所有轉譯的內容項目 (包括 預留位置內容) 的高度均相同。 這可讓您計算哪些內容對應至指定的捲動位置,而不需要先擷取每個資料項目並將資料轉譯成 DOM 元素。
空白區塊和內容列會在單一垂直堆疊中渲染,所有項目都會填滿整個水平寬度。 在一般使用案例中,
Virtualize
元素 與div
元素共同運作。 如果您使用 CSS 來建立更進階的配置,請記住下列需求:- 具有下列任何值的
display
是捲動容器樣式所需的。-
block
(div
預設值)。 -
table-row-group
(tbody
預設值)。 -
flex
將flex-direction
設定為column
。 請確保 Virtualize<TItem> 元件的直接子系不會在 Flex 規則下縮小。 例如,新增.mycontainer > div { flex-shrink: 0 }
。
-
- 要使內容列樣式生效,需要
display
擁有下列任一值:-
block
(div
預設值)。 -
table-row
(tr
預設值)。
-
- 請勿使用 CSS 來干擾間隔元素的佈局。 空白區塊元素有
display
的block
值,除非父系是資料表列群組,在此情況下預設為table-row
。 不要嘗試影響定位元素的寬度或高度,包括讓它們有邊框或偽元素。
- 具有下列任何值的
任何阻止間隔元素與內容元素轉譯為單一垂直堆疊或導致內容元素高度不同的方法,均會影響 Virtualize<TItem> 元件的正確運作。
根層級虛擬化
Virtualize<TItem> 元件支援使用文件本身作為捲動根,也可以選擇使用其他具有 overflow-y: scroll
的元素。 在下列範例中,<html>
或 <body>
元素會在具有 overflow-y: scroll
樣式的元件中設定樣式:
<HeadContent>
<style>
html, body { overflow-y: scroll }
</style>
</HeadContent>
Virtualize<TItem> 元件支援使用文件本身作為捲動的根節點,作為使用附帶 overflow-y: scroll
的其他元素的替代方案。 當將文件設為捲動根節點時,請避免為 <body>
和 overflow-y: scroll
元素設定 <html>
這樣的樣式,因為這會導致 交叉觀察器 將頁面的完整可捲動高度視作可見區域,而不只是視窗檢視區。
您可透過建立大型虛擬化清單 (例如 100,000 個項目) 以重現此問題,並嘗試在頁面 CSS 樣式中透過 html { overflow-y: scroll }
使用文件作為捲動根。 即使在某些時候可能正常運作,但瀏覽器在渲染開始時嘗試至少渲染一次所有 100,000 個項目,這可能會導致瀏覽器分頁當機。
若要在 .NET 7 版本之前解決此問題,請避免使用 <html>
設定 /<body>
overflow-y: scroll
的樣式,或採用替代方法。 在下列範例中,<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 版本之前解決此問題,請避免使用 <html>
設定 /<body>
overflow-y: scroll
的樣式,或採用替代方法。 在下列範例中,<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();
}
在上述範例中,文件根作為捲動容器使用,因此 html
和 body
元素會以 overflow-y: scroll
來設定樣式。 如需詳細資訊,請參閱以下資源: