ASP.NET Core Razor 元件生命週期
注意
這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本。
警告
不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支援原則。 如需目前版本,請參閱本文的 .NET 8 版本。
本文說明 ASP.NET Core Razor 元件生命週期,以及如何使用生命週期事件。
週期事件
Razor 元件會在一組同步和非同步生命週期方法中處理 Razor 元件生命週期事件。 您可以在元件初始化和轉譯期間覆寫生命週期方法,以在元件初始化和轉譯期間執行元件中的其他作業。
本文簡化了元件生命週期事件處理,以便釐清複雜的架構邏輯,並不涵蓋多年來所做的每項變更。 您可能需要存取 ComponentBase
參考來源 ,以整合自訂事件處理與 Blazor 的生命週期事件處理。 參考來源中的程式碼批註包含生命週期事件處理的其他備註,且這些生命週期事件處理未出現在本文或 API 文件中。
注意
.NET 參考來源的文件連結通常會載入存放庫的預設分支,這表示下一版 .NET 的目前開發。 若要選取特定版本的標籤,請使用 [切換分支或標籤] 下拉式清單。 如需詳細資訊,請參閱如何選取 ASP.NET Core 原始程式碼 (dotnet/AspNetCore.Docs #26205) 的版本標籤。
下列簡化圖表說明 Razor 元件生命週期事件處理。 與生命週期事件相關聯的 C# 方法會定義本文下列各節中的範例。
元件生命週期事件:
- 如果元件第一次在要求上轉譯:
- 建立元件的執行個體。
- 執行屬性插入。
- 呼叫
OnInitialized{Async}
。 如果傳回不完整的 Task,則 Task 會等候,然後重新轉譯元件。 同步方法是在非同步方法之前呼叫。
- 呼叫
OnParametersSet{Async}
。 如果傳回不完整的 Task,則 Task 會等候,然後重新轉譯元件。 同步方法是在非同步方法之前呼叫。 - 轉譯所有同步工作並完成 Task。
注意
在生命週期事件中執行的非同步動作在轉譯元件前可能尚未完成。 如需詳細資訊,請參閱本文稍後的處理轉譯時不完整的非同步動作一節。
父代元件會在其子系元件之前轉譯,因為轉譯是決定哪些子系存在的因素。 如果使用同步父代元件初始化,則保證父代初始化會先完成。 如果使用非同步父代元件初始化,則無法判斷父代和子系元件初始化的完成順序,因為該順序取決於執行中的初始化程式碼。
DOM 事件處理:
Render
生命週期:
- 當下列兩個條件均符合時,避免在元件上進行進一步的轉譯作業:
- 這不是第一項轉譯。
ShouldRender
傳回false
。
- 組建轉譯樹狀結構差異,並轉譯元件。
- 等候 DOM 更新。
- 呼叫
OnAfterRender{Async}
。 同步方法是在非同步方法之前呼叫。
開發人員呼叫 StateHasChanged
會導致重新轉譯。 如需詳細資訊,請參閱 ASP.NET Core Razor 元件轉譯。
設定參數時 (SetParametersAsync
)
轉譯樹狀結構中元件父代或路由參數所提供的 SetParametersAsync 設定參數。
方法的 ParameterView 參數包含每次呼叫 SetParametersAsync 時,元件的元件參數 值集。 藉由覆寫 SetParametersAsync 方法,開發人員程式碼可以直接與 ParameterView 的參數互動。
SetParametersAsync 的預設實作會使用 ParameterView 中具有對應值的 [Parameter]
或 [CascadingParameter]
屬性,設定每個屬性的值。 在 ParameterView 中沒有對應值的參數會保持不變。
一般而言,覆寫 SetParametersAsync 時,您的程式碼應該呼叫基底類別方法 (await base.SetParametersAsync(parameters);
)。 在進階案例中,開發人員程式碼可以透過不叫用基底類別方法所需的任何方式解譯傳入參數的值。 例如,不需要將傳入參數指派給類別的屬性。 不過,在建構程式碼而不呼叫基底類別方法時,您必須參考 ComponentBase
參考來源,因為其會呼叫其他生命週期方法,並以複雜的方式觸發轉譯。
注意
.NET 參考來源的文件連結通常會載入存放庫的預設分支,這表示下一版 .NET 的目前開發。 若要選取特定版本的標籤,請使用 [切換分支或標籤] 下拉式清單。 如需詳細資訊,請參閱如何選取 ASP.NET Core 原始程式碼 (dotnet/AspNetCore.Docs #26205) 的版本標籤。
如果您想要依賴 ComponentBase.SetParametersAsync 的初始化和轉譯邏輯,但不處理傳入參數,您可以選擇將空白 ParameterView 傳遞至基底類別方法:
await base.SetParametersAsync(ParameterView.Empty);
如果在開發人員程式碼中提供事件處理常式,請在處置時將其解除連結。 如需詳細資訊,請參閱使用 IDisposable
和 IAsyncDisposable
處置元件一節。
在下列範例中,如果剖析 Param
的路由參數成功,則 ParameterView.TryGetValue 會將 Param
參數的值指派給 value
。 當 value
不是 null
時,值會依元件顯示。
雖然路由參數比對不區分大小寫,但 TryGetValue 只會比對路由範本中的區分大小寫參數名稱。 下列範例需要使用路由範本中的 /{Param?}
,才能使用 TryGetValue 而非 /{param?}
取得值。 如果在此案例中使用 /{param?}
,則 TryGetValue 會傳回 false
且message
未設定為任一message
字串。
SetParamsAsync.razor
:
@page "/set-params-async/{Param?}"
<PageTitle>Set Parameters Async</PageTitle>
<h1>Set Parameters Async Example</h1>
<p>@message</p>
@code {
private string message = "Not set";
[Parameter]
public string? Param { get; set; }
public override async Task SetParametersAsync(ParameterView parameters)
{
if (parameters.TryGetValue<string>(nameof(Param), out var value))
{
if (value is null)
{
message = "The value of 'Param' is null.";
}
else
{
message = $"The value of 'Param' is {value}.";
}
}
await base.SetParametersAsync(parameters);
}
}
@page "/set-params-async/{Param?}"
<PageTitle>Set Parameters Async</PageTitle>
<h1>Set Parameters Async Example</h1>
<p>@message</p>
@code {
private string message = "Not set";
[Parameter]
public string? Param { get; set; }
public override async Task SetParametersAsync(ParameterView parameters)
{
if (parameters.TryGetValue<string>(nameof(Param), out var value))
{
if (value is null)
{
message = "The value of 'Param' is null.";
}
else
{
message = $"The value of 'Param' is {value}.";
}
}
await base.SetParametersAsync(parameters);
}
}
@page "/set-params-async/{Param?}"
<p>@message</p>
@code {
private string message = "Not set";
[Parameter]
public string? Param { get; set; }
public override async Task SetParametersAsync(ParameterView parameters)
{
if (parameters.TryGetValue<string>(nameof(Param), out var value))
{
if (value is null)
{
message = "The value of 'Param' is null.";
}
else
{
message = $"The value of 'Param' is {value}.";
}
}
await base.SetParametersAsync(parameters);
}
}
@page "/set-params-async/{Param?}"
<p>@message</p>
@code {
private string message = "Not set";
[Parameter]
public string? Param { get; set; }
public override async Task SetParametersAsync(ParameterView parameters)
{
if (parameters.TryGetValue<string>(nameof(Param), out var value))
{
if (value is null)
{
message = "The value of 'Param' is null.";
}
else
{
message = $"The value of 'Param' is {value}.";
}
}
await base.SetParametersAsync(parameters);
}
}
@page "/set-params-async/{Param?}"
<p>@message</p>
@code {
private string message = "Not set";
[Parameter]
public string Param { get; set; }
public override async Task SetParametersAsync(ParameterView parameters)
{
if (parameters.TryGetValue<string>(nameof(Param), out var value))
{
if (value is null)
{
message = "The value of 'Param' is null.";
}
else
{
message = $"The value of 'Param' is {value}.";
}
}
await base.SetParametersAsync(parameters);
}
}
@page "/set-params-async"
@page "/set-params-async/{Param}"
<p>@message</p>
@code {
private string message = "Not set";
[Parameter]
public string Param { get; set; }
public override async Task SetParametersAsync(ParameterView parameters)
{
if (parameters.TryGetValue<string>(nameof(Param), out var value))
{
if (value is null)
{
message = "The value of 'Param' is null.";
}
else
{
message = $"The value of 'Param' is {value}.";
}
}
await base.SetParametersAsync(parameters);
}
}
元件初始化 (OnInitialized{Async}
)
OnInitialized 和 OnInitializedAsync 專門用於在元件執行個體的整個存留期中初始化元件。 參數值和參數值變更不應影響在這些方法中執行的初始化。 例如,將靜態選項載入至下拉式清單中,其在該元件的存留期中不會改變,且不相依於參數值,會在下列其中一個生命週期方法中執行。 如果參數值或參數值變更會影響元件狀態,請改用 OnParametersSet{Async}
。
當元件在 SetParametersAsync 中收到其初始參數之後,就會叫用這些方法。 同步方法是在非同步方法之前呼叫。
如果使用同步父代元件初始化,則保證父代初始化會在子元件初始化之前完成。 如果使用非同步父代元件初始化,則無法判斷父代和子系元件初始化的完成順序,因為該順序取決於執行中的初始化程式碼。
若為同步作業,請覆寫 OnInitialized:
OnInit.razor
:
@page "/on-init"
<PageTitle>On Initialized</PageTitle>
<h1>On Initialized Example</h1>
<p>@message</p>
@code {
private string? message;
protected override void OnInitialized() =>
message = $"Initialized at {DateTime.Now}";
}
@page "/on-init"
<PageTitle>On Initialized</PageTitle>
<h1>On Initialized Example</h1>
<p>@message</p>
@code {
private string? message;
protected override void OnInitialized() =>
message = $"Initialized at {DateTime.Now}";
}
@page "/on-init"
<p>@message</p>
@code {
private string? message;
protected override void OnInitialized()
{
message = $"Initialized at {DateTime.Now}";
}
}
@page "/on-init"
<p>@message</p>
@code {
private string? message;
protected override void OnInitialized()
{
message = $"Initialized at {DateTime.Now}";
}
}
@page "/on-init"
<p>@message</p>
@code {
private string message;
protected override void OnInitialized()
{
message = $"Initialized at {DateTime.Now}";
}
}
@page "/on-init"
<p>@message</p>
@code {
private string message;
protected override void OnInitialized()
{
message = $"Initialized at {DateTime.Now}";
}
}
若要執行非同步作業,請覆寫 OnInitializedAsync 並使用 await
運算子:
protected override async Task OnInitializedAsync()
{
await ...
}
如果自訂基底類別搭配自訂初始化邏輯使用,請在基底類別上呼叫 OnInitializedAsync:
protected override async Task OnInitializedAsync()
{
await ...
await base.OnInitializedAsync();
}
除非自訂基底類別搭配自訂邏輯使用,否則不需要呼叫 ComponentBase.OnInitializedAsync。 如需詳細資訊,請參閱基底類別生命週期方法一節。
在伺服器上預先轉譯其內容的 Blazor 應用程式會呼叫 OnInitializedAsync 兩次:
- 第一次是在元件開始靜態轉譯為頁面的一部分時。
- 第二次是在瀏覽器轉譯元件時。
若要防止 OnInitializedAsync 中的開發人員程式碼在預先轉譯時執行兩次,請參閱預先轉譯之後的具狀態重新連線一節。 本節內容著重於 Blazor Web App和具狀態的 SignalR重新連線。 若要在預先轉譯初始化程式碼執行期間保留狀態,請參閱預先轉譯 ASP.NET CoreRazor 元件。
若要防止 OnInitializedAsync 中的開發人員程式碼在預先轉譯時執行兩次,請參閱預先轉譯之後的具狀態重新連線一節。 雖然本節內容著重於 Blazor Server 和具狀態的SignalR重新連線 ,但裝載 Blazor WebAssembly 解決方案 (WebAssemblyPrerendered) 預先轉譯的案例涉及類似條件和方法,以防止開發人員程式碼執行兩次。 若要在預先轉譯初始化程式碼執行期間保留狀態,請參閱預先轉譯並整合 ASP.NET CoreRazor 元件。
雖然 Blazor 應用程式會預先轉譯,但無法進行呼叫 JavaScript (JS Interop) 等特定動作。 預先轉譯時,元件可能需要以不同的方式轉譯。 如需詳細資訊,請參閱使用 JavaScript Interop 預先轉譯一節。
如果在開發人員程式碼中提供事件處理常式,請在處置時將其解除連結。 如需詳細資訊,請參閱使用 IDisposable
IAsyncDisposable
處置元件一節。
使用串流轉譯搭配靜態伺服器端轉譯 (靜態 SSR) 或預先轉譯,以改善在 OnInitializedAsync 中執行長時間執行非同步工作之元件的使用者體驗,以便完全轉譯。 如需詳細資訊,請參閱 ASP.NET Core Razor 元件轉譯。
設定參數之後 (OnParametersSet{Async}
)
呼叫 OnParametersSet 或 OnParametersSetAsync:
在 OnInitialized 或 OnInitializedAsync 中初始化元件之後。
當父代元件重新轉譯並提供時:
- 至少有一個參數變更時的已知或基本不可變類型。
- 複雜型別參數。 架構無法得知複雜型別參數的值是否已在內部變動,因此當一或多個複雜型別參數存在時,架構一律會將參數集視為已變更。
如需關於轉譯慣例的詳細資訊,請參閱 ASP.NET Core Razor 元件轉譯。
同步方法是在非同步方法之前呼叫。
即使參數值尚未變更,也可以叫用方法。 這種行為強調開發人員需要在方法內實作其他邏輯,以檢查參數值是否確實在重新初始化資料或相依於這些參數的狀態之前已變更。
針對下列範例元件,瀏覽至 URL 上的元件頁面:
- 具有在
StartDate
前收到的開始日期:/on-parameters-set/2021-03-19
- 沒有開始日期,此情況下
StartDate
會指派目前當地時間的值:/on-parameters-set
注意
在元件路由中,您無法同時使用路由限制式datetime
來限制 DateTime 參數,並將參數設為選擇性。 因此,下列 OnParamsSet
元件會使用兩個 @page
指示詞,來處理 URL 中提供與未提供日期區段的路由。
OnParamsSet.razor
:
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"
<PageTitle>On Parameters Set</PageTitle>
<h1>On Parameters Set Example</h1>
<p>
Pass a datetime in the URI of the browser's address bar.
For example, add <code>/1-1-2024</code> to the address.
</p>
<p>@message</p>
@code {
private string? message;
[Parameter]
public DateTime StartDate { get; set; }
protected override void OnParametersSet()
{
if (StartDate == default)
{
StartDate = DateTime.Now;
message = $"No start date in URL. Default value applied " +
$"(StartDate: {StartDate}).";
}
else
{
message = $"The start date in the URL was used " +
$"(StartDate: {StartDate}).";
}
}
}
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"
<PageTitle>On Parameters Set</PageTitle>
<h1>On Parameters Set Example</h1>
<p>
Pass a datetime in the URI of the browser's address bar.
For example, add <code>/1-1-2024</code> to the address.
</p>
<p>@message</p>
@code {
private string? message;
[Parameter]
public DateTime StartDate { get; set; }
protected override void OnParametersSet()
{
if (StartDate == default)
{
StartDate = DateTime.Now;
message = $"No start date in URL. Default value applied " +
$"(StartDate: {StartDate}).";
}
else
{
message = $"The start date in the URL was used " +
$"(StartDate: {StartDate}).";
}
}
}
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"
<p>@message</p>
@code {
private string? message;
[Parameter]
public DateTime StartDate { get; set; }
protected override void OnParametersSet()
{
if (StartDate == default)
{
StartDate = DateTime.Now;
message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
}
else
{
message = $"The start date in the URL was used (StartDate: {StartDate}).";
}
}
}
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"
<p>@message</p>
@code {
private string? message;
[Parameter]
public DateTime StartDate { get; set; }
protected override void OnParametersSet()
{
if (StartDate == default)
{
StartDate = DateTime.Now;
message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
}
else
{
message = $"The start date in the URL was used (StartDate: {StartDate}).";
}
}
}
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"
<p>@message</p>
@code {
private string message;
[Parameter]
public DateTime StartDate { get; set; }
protected override void OnParametersSet()
{
if (StartDate == default)
{
StartDate = DateTime.Now;
message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
}
else
{
message = $"The start date in the URL was used (StartDate: {StartDate}).";
}
}
}
@page "/on-params-set"
@page "/on-params-set/{StartDate:datetime}"
<p>@message</p>
@code {
private string message;
[Parameter]
public DateTime StartDate { get; set; }
protected override void OnParametersSet()
{
if (StartDate == default)
{
StartDate = DateTime.Now;
message = $"No start date in URL. Default value applied (StartDate: {StartDate}).";
}
else
{
message = $"The start date in the URL was used (StartDate: {StartDate}).";
}
}
}
套用參數和屬性值時,必須在 OnParametersSetAsync 生命週期事件期間進行非同步工作:
protected override async Task OnParametersSetAsync()
{
await ...
}
如果自訂基底類別搭配自訂初始化邏輯使用,請在基底類別上呼叫 OnParametersSetAsync:
protected override async Task OnParametersSetAsync()
{
await ...
await base.OnParametersSetAsync();
}
除非自訂基底類別搭配自訂邏輯使用,否則不需要呼叫 ComponentBase.OnParametersSetAsync。 如需詳細資訊,請參閱基底類別生命週期方法一節。
如果在開發人員程式碼中提供事件處理常式,請在處置時將其解除連結。 如需詳細資訊,請參閱使用 IDisposable
IAsyncDisposable
處置元件一節。
如需路由參數和限制式的詳細資訊,請參閱 ASP.NET Core Blazor 路由和導覽。
如需手動實作 SetParametersAsync
以改善某些案例效能的範例,請參閱 ASP.NET Core Blazor 效能最佳做法。
元件轉譯之後 (OnAfterRender{Async}
)
OnAfterRender 和 OnAfterRenderAsync 會在元件以互動方式轉譯且 UI 完成更新之後叫用 (例如,將元素新增至瀏覽器 DOM 之後)。 此時會填入元素和元件參考。 使用此階段來執行轉譯內容的其他初始化步驟,例如與轉譯 DOM 元素互動的 JS Interop 呼叫。 同步方法是在非同步方法之前呼叫。
這些方法不會在伺服器上於預先轉譯或靜態伺服器端轉譯 (靜態 SSR) 期間叫用,因為這些流程不會附加至即時瀏覽器 DOM,且已在更新 DOM 之前完成。
針對 OnAfterRenderAsync,元件不會在任何傳回的 Task
完成之後自動重新轉譯,以避免無限轉譯迴圈。
OnAfterRender 和 OnAfterRenderAsync 會在元件完成轉譯之後呼叫。 此時會填入元素和元件參考。 使用此階段來執行轉譯內容的其他初始化步驟,例如與轉譯 DOM 元素互動的 JS Interop 呼叫。 同步方法是在非同步方法之前呼叫。
這些方法不會在預先定義期間叫用,因為預先轉譯不會附加至即時瀏覽器 DOM,而且已在更新 DOM 之前完成。
針對 OnAfterRenderAsync,元件不會在任何傳回的 Task
完成之後自動重新轉譯,以避免無限轉譯迴圈。
OnAfterRender 和 OnAfterRenderAsync 的 firstRender
參數:
- 第一次轉譯元件執行個體時會設定為
true
。 - 可以用來確保初始化工作只會執行一次。
AfterRender.razor
:
@page "/after-render"
@inject ILogger<AfterRender> Logger
<PageTitle>After Render</PageTitle>
<h1>After Render Example</h1>
<p>
<button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>
<p>Study logged messages in the console.</p>
@code {
protected override void OnAfterRender(bool firstRender) =>
Logger.LogInformation("firstRender = {FirstRender}", firstRender);
private void HandleClick() => Logger.LogInformation("HandleClick called");
}
@page "/after-render"
@inject ILogger<AfterRender> Logger
<PageTitle>After Render</PageTitle>
<h1>After Render Example</h1>
<p>
<button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>
<p>Study logged messages in the console.</p>
@code {
protected override void OnAfterRender(bool firstRender) =>
Logger.LogInformation("firstRender = {FirstRender}", firstRender);
private void HandleClick() => Logger.LogInformation("HandleClick called");
}
@page "/after-render"
@inject ILogger<AfterRender> Logger
<PageTitle>After Render</PageTitle>
<h1>After Render Example</h1>
<p>
<button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>
<p>Study logged messages in the console.</p>
@code {
protected override void OnAfterRender(bool firstRender)
{
Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
}
private void HandleClick()
{
Logger.LogInformation("HandleClick called");
}
}
@page "/after-render"
@inject ILogger<AfterRender> Logger
<PageTitle>After Render</PageTitle>
<h1>After Render Example</h1>
<p>
<button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>
<p>Study logged messages in the console.</p>
@code {
protected override void OnAfterRender(bool firstRender)
{
Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
}
private void HandleClick()
{
Logger.LogInformation("HandleClick called");
}
}
@page "/after-render"
@using Microsoft.Extensions.Logging
@inject ILogger<AfterRender> Logger
<h1>After Render Example</h1>
<p>
<button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>
<p>Study logged messages in the console.</p>
@code {
protected override void OnAfterRender(bool firstRender)
{
Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
}
private void HandleClick()
{
Logger.LogInformation("HandleClick called");
}
}
@page "/after-render"
@using Microsoft.Extensions.Logging
@inject ILogger<AfterRender> Logger
<h1>After Render Example</h1>
<p>
<button @onclick="HandleClick">Log information (and trigger a render)</button>
</p>
<p>Study logged messages in the console.</p>
@code {
protected override void OnAfterRender(bool firstRender)
{
Logger.LogInformation("OnAfterRender: firstRender = {FirstRender}", firstRender);
}
private void HandleClick()
{
Logger.LogInformation("HandleClick called");
}
}
載入頁面並選取按鈕時,AfterRender.razor
範例會產生下列輸出至主控台:
OnAfterRender: firstRender = True
HandleClick called
OnAfterRender: firstRender = False
在轉譯之後立即進行的非同步工作,必須在 OnAfterRenderAsync 生命週期事件期間發生:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
...
}
如果自訂基底類別搭配自訂初始化邏輯使用,請在基底類別上呼叫 OnAfterRenderAsync:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
...
await base.OnAfterRenderAsync(firstRender);
}
除非自訂基底類別搭配自訂邏輯使用,否則不需要呼叫 ComponentBase.OnAfterRenderAsync。 如需詳細資訊,請參閱基底類別生命週期方法一節。
即使您從 OnAfterRenderAsync 傳回 Task,架構也不會在完成該工作之後,為元件排程進一步的轉譯週期。 這是為了避免無限轉譯迴圈。 這與其他生命週期方法不同,其他生命週期方法會在傳回的 Task 完成之後排程進一步轉譯週期。
OnAfterRender 和 OnAfterRenderAsync 在伺服器預先轉譯流程期間不會呼叫。 當元件在預先轉譯之後以互動方式轉譯時,會呼叫方法。 當應用程式預先轉譯時:
- 元件會在伺服器上執行,以在 HTTP 回應中產生一些靜態 HTML 標記。 在此階段期間,不會呼叫 OnAfterRender 和 OnAfterRenderAsync。
- 當 Blazor 指令碼 (
blazor.{server|webassembly|web}.js
) 在瀏覽器中啟動時,元件會以互動式轉譯模式重新啟動。 重新啟動元件之後,「會」呼叫 OnAfterRender 和 OnAfterRenderAsync ,因為應用程式不再處於預先轉譯階段。
如果在開發人員程式碼中提供事件處理常式,請在處置時將其解除連結。 如需詳細資訊,請參閱使用 IDisposable
IAsyncDisposable
處置元件一節。
基底類別生命週期方法
覆寫 Blazor 的生命週期方法時,不需要呼叫 ComponentBase 的基底類別生命週期方法。 不過,在下列情況下,元件應該呼叫覆寫的基底類別生命週期方法:
- 覆寫 ComponentBase.SetParametersAsync 時,通常會叫用
await base.SetParametersAsync(parameters);
,因為基底類別方法會呼叫其他生命週期方法,並以複雜的方式觸發轉譯。 如需詳細資訊,請參閱何時設定參數 (SetParametersAsync
) 一節。 - 如果基底類別方法包含必須執行的邏輯。 程式庫取用者通常會在繼承基底類別時呼叫基底類別生命週期方法,因為程式庫基底類別通常有要執行的自訂生命週期邏輯。 如果應用程式使用程式庫中的基底類別,請參閱程式庫的文件以取得指引。
在下列範例中,會呼叫 base.OnInitialized();
,以確保執行基底類別的 OnInitialized
方法。 若沒有呼叫,BlazorRocksBase2.OnInitialized
就不會執行。
BlazorRocks2.razor
:
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger
<PageTitle>Blazor Rocks!</PageTitle>
<h1>Blazor Rocks! Example 2</h1>
<p>
@BlazorRocksText
</p>
@code {
protected override void OnInitialized()
{
Logger.LogInformation("Initialization code of BlazorRocks2 executed!");
base.OnInitialized();
}
}
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger
<PageTitle>Blazor Rocks!</PageTitle>
<h1>Blazor Rocks! Example 2</h1>
<p>
@BlazorRocksText
</p>
@code {
protected override void OnInitialized()
{
Logger.LogInformation("Initialization code of BlazorRocks2 executed!");
base.OnInitialized();
}
}
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger
<PageTitle>Blazor Rocks!</PageTitle>
<h1>Blazor Rocks! Example 2</h1>
<p>
@BlazorRocksText
</p>
@code {
protected override void OnInitialized()
{
Logger.LogInformation("Initialization code of BlazorRocks2 executed!");
base.OnInitialized();
}
}
@page "/blazor-rocks-2"
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger
<PageTitle>Blazor Rocks!</PageTitle>
<h1>Blazor Rocks! Example 2</h1>
<p>
@BlazorRocksText
</p>
@code {
protected override void OnInitialized()
{
Logger.LogInformation("Initialization code of BlazorRocks2 executed!");
base.OnInitialized();
}
}
@page "/blazor-rocks-2"
@using Microsoft.Extensions.Logging
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger
<h1>Blazor Rocks! Example 2</h1>
<p>
@BlazorRocksText
</p>
@code {
protected override void OnInitialized()
{
Logger.LogInformation("Initialization code of BlazorRocks2 executed!");
base.OnInitialized();
}
}
@page "/blazor-rocks-2"
@using Microsoft.Extensions.Logging
@inherits BlazorRocksBase2
@inject ILogger<BlazorRocks2> Logger
<h1>Blazor Rocks! Example 2</h1>
<p>
@BlazorRocksText
</p>
@code {
protected override void OnInitialized()
{
Logger.LogInformation("Initialization code of BlazorRocks2 executed!");
base.OnInitialized();
}
}
BlazorRocksBase2.cs
:
using Microsoft.AspNetCore.Components;
namespace BlazorSample;
public class BlazorRocksBase2 : ComponentBase
{
[Inject]
private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;
public string BlazorRocksText { get; set; } = "Blazor rocks the browser!";
protected override void OnInitialized() =>
Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
}
using Microsoft.AspNetCore.Components;
namespace BlazorSample;
public class BlazorRocksBase2 : ComponentBase
{
[Inject]
private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;
public string BlazorRocksText { get; set; } = "Blazor rocks the browser!";
protected override void OnInitialized() =>
Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
}
using Microsoft.AspNetCore.Components;
namespace BlazorSample;
public class BlazorRocksBase2 : ComponentBase
{
[Inject]
private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;
public string BlazorRocksText { get; set; } =
"Blazor rocks the browser!";
protected override void OnInitialized()
{
Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
}
}
using Microsoft.AspNetCore.Components;
namespace BlazorSample;
public class BlazorRocksBase2 : ComponentBase
{
[Inject]
private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;
public string BlazorRocksText { get; set; } =
"Blazor rocks the browser!";
protected override void OnInitialized()
{
Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
}
}
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;
namespace BlazorSample;
public class BlazorRocksBase2 : ComponentBase
{
[Inject]
private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;
public string BlazorRocksText { get; set; } =
"Blazor rocks the browser!";
protected override void OnInitialized()
{
Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
}
}
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;
namespace BlazorSample;
public class BlazorRocksBase2 : ComponentBase
{
[Inject]
private ILogger<BlazorRocksBase2> Logger { get; set; } = default!;
public string BlazorRocksText { get; set; } =
"Blazor rocks the browser!";
protected override void OnInitialized()
{
Logger.LogInformation("Initialization code of BlazorRocksBase2 executed!");
}
}
狀態變更 (StateHasChanged
)
StateHasChanged 通知元件其狀態已變更。 適用時,呼叫 StateHasChanged,可在應用程式的主要執行緒可用時,將發生的重新轉譯加入佇列。
系統會自動針對 EventCallback 方法呼叫 StateHasChanged。 如需事件回撥的詳細資訊,請參閱 ASP.NET CoreBlazor 事件處理。
如需關於元件轉譯和何時呼叫 StateHasChanged (包括何時使用 ComponentBase.InvokeAsync 叫用) 的詳細資訊,請參閱 ASP.NET Core Razor 元件轉譯 。
處理轉譯時不完整的非同步動作
在生命週期事件中執行的非同步動作,在轉譯元件前可能尚未完成。 當生命週期方法執行時,物件可能會 null
或不完整地填入資料。 提供轉譯邏輯,以確認物件已初始化。 轉譯預留位置 UI 元素 (例如載入訊息),而物件為 null
。
在下列元件中,會覆寫 OnInitializedAsync 以非同步提供電影分級資料 (movies
)。 當 movies
為 null
時,會向使用者顯示載入訊息。 由 OnInitializedAsync 傳回 Task
完成之後,元件會以更新的狀態重新轉譯。
<h1>Sci-Fi Movie Ratings</h1>
@if (movies == null)
{
<p><em>Loading...</em></p>
}
else
{
<ul>
@foreach (var movie in movies)
{
<li>@movie.Title — @movie.Rating</li>
}
</ul>
}
@code {
private Movies[]? movies;
protected override async Task OnInitializedAsync()
{
movies = await GetMovieRatings(DateTime.Now);
}
}
處理錯誤
如需關於在生命週期方法執行期間處理錯誤的資訊,請參閱處理 ASP.NET Core Blazor 應用程式中的錯誤。
預先轉譯之後的具狀態重新連線
在伺服器上預先轉譯時,元件一開始會以靜態方式轉譯為頁面的一部分。 一旦瀏覽器建立與伺服器的 SignalR 連線,元件就會再次轉譯並可互動。 如果元件初始化的 OnInitialized{Async}
生命週期方法存在,則會執行方法兩次:
- 當元件以靜態方式預先轉譯時。
- 建立伺服器連線之後。
這可能會導致最終轉譯元件時,UI 中顯示的資料有明顯變更。 若要避免此行為,請在預先轉譯期間傳入識別碼以快取狀態,並在預先轉譯之後擷取狀態。
下列程式碼示範 WeatherForecastService
,其可避免因為預先轉譯而變更資料顯示。 等候的 Delay (await Task.Delay(...)
) 會模擬短暫的延遲,再從 GetForecastAsync
方法傳回資料。
在應用程式的 Program
檔案中,在服務集合上新增具有 AddMemoryCache 的 IMemoryCache 服務:
builder.Services.AddMemoryCache();
WeatherForecastService.cs
:
using Microsoft.Extensions.Caching.Memory;
namespace BlazorSample;
public class WeatherForecastService(IMemoryCache memoryCache)
{
private static readonly string[] summaries =
[
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];
public IMemoryCache MemoryCache { get; } = memoryCache;
public Task<WeatherForecast[]?> GetForecastAsync(DateOnly startDate)
{
return MemoryCache.GetOrCreateAsync(startDate, async e =>
{
e.SetOptions(new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromSeconds(30)
});
await Task.Delay(TimeSpan.FromSeconds(10));
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
});
}
}
using Microsoft.Extensions.Caching.Memory;
namespace BlazorSample;
public class WeatherForecastService(IMemoryCache memoryCache)
{
private static readonly string[] summaries =
[
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];
public IMemoryCache MemoryCache { get; } = memoryCache;
public Task<WeatherForecast[]?> GetForecastAsync(DateOnly startDate)
{
return MemoryCache.GetOrCreateAsync(startDate, async e =>
{
e.SetOptions(new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromSeconds(30)
});
await Task.Delay(TimeSpan.FromSeconds(10));
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
});
}
}
using Microsoft.Extensions.Caching.Memory;
public class WeatherForecastService
{
private static readonly string[] summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public WeatherForecastService(IMemoryCache memoryCache)
{
MemoryCache = memoryCache;
}
public IMemoryCache MemoryCache { get; }
public Task<WeatherForecast[]?> GetForecastAsync(DateTime startDate)
{
return MemoryCache.GetOrCreateAsync(startDate, async e =>
{
e.SetOptions(new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromSeconds(30)
});
await Task.Delay(TimeSpan.FromSeconds(10));
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
});
}
}
using Microsoft.Extensions.Caching.Memory;
public class WeatherForecastService
{
private static readonly string[] summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public WeatherForecastService(IMemoryCache memoryCache)
{
MemoryCache = memoryCache;
}
public IMemoryCache MemoryCache { get; }
public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
{
return MemoryCache.GetOrCreateAsync(startDate, async e =>
{
e.SetOptions(new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromSeconds(30)
});
await Task.Delay(TimeSpan.FromSeconds(10));
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
});
}
}
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using BlazorSample.Shared;
public class WeatherForecastService
{
private static readonly string[] summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public WeatherForecastService(IMemoryCache memoryCache)
{
MemoryCache = memoryCache;
}
public IMemoryCache MemoryCache { get; }
public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
{
return MemoryCache.GetOrCreateAsync(startDate, async e =>
{
e.SetOptions(new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromSeconds(30)
});
var rng = new Random();
await Task.Delay(TimeSpan.FromSeconds(10));
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = summaries[rng.Next(summaries.Length)]
}).ToArray();
});
}
}
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using BlazorSample.Shared;
public class WeatherForecastService
{
private static readonly string[] summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
public WeatherForecastService(IMemoryCache memoryCache)
{
MemoryCache = memoryCache;
}
public IMemoryCache MemoryCache { get; }
public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate)
{
return MemoryCache.GetOrCreateAsync(startDate, async e =>
{
e.SetOptions(new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromSeconds(30)
});
var rng = new Random();
await Task.Delay(TimeSpan.FromSeconds(10));
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = summaries[rng.Next(summaries.Length)]
}).ToArray();
});
}
}
如需關於 RenderMode 的詳細資訊,請參閱 ASP.NET Core BlazorSignalR 指導。
本節內容著重於 Blazor Web App和具狀態的 SignalR重新連線。 若要在預先轉譯初始化程式碼執行期間保留狀態,請參閱預先轉譯 ASP.NET CoreRazor 元件。
雖然本節內容著重於 Blazor Server 和具狀態的SignalR重新連線 ,但裝載 Blazor WebAssembly 解決方案 (WebAssemblyPrerendered) 預先轉譯的案例涉及類似條件和方法,以防止開發人員程式碼執行兩次。 若要在預先轉譯初始化程式碼執行期間保留狀態,請參閱預先轉譯並整合 ASP.NET CoreRazor 元件。
使用 JavaScript Interop 預先轉譯
本節適用於會預先轉譯 Razor 元件的伺服器端應用程式。 預先轉譯 ASP.NET Core Razor 元件中會有預先轉譯的說明。
注意
在 Blazor Web App 中互動式路由的內部瀏覽並不涉及向伺服器要求新頁面內容。 因此,內部頁面要求不會發生預先轉譯。 如果應用程式採用互動式路由,請針對示範預先轉譯行為的元件範例執行完整頁面重載。 如需詳細資訊,請參閱預先轉譯 ASP.NET Core Razor 元件。
本節適用於會預先轉譯 Razor 元件的伺服器端應用程式和所裝載的 Blazor WebAssembly 應用程式。 預先轉譯和整合 ASP.NET Core Razor 元件中會有預先轉譯的說明。
在預先轉譯期間,無法電話撥接 JavaScript(JS)。 下列範例示範如何透過與預先轉譯相容的方式,將 JS Interop 作為元件初始化邏輯的一部分。
下列 scrollElementIntoView
函式:
- 使用
scrollIntoView
捲動至傳遞的元素。 - 從
getBoundingClientRect
方法傳回元素的top
屬性值。
window.scrollElementIntoView = (element) => {
element.scrollIntoView();
return element.getBoundingClientRect().top;
}
當 IJSRuntime.InvokeAsync 在元件程式碼呼叫 JS 函式時, ElementReference 僅在 OnAfterRenderAsync 使用,而不在任何先前的生命週期方法中使用,因為要等到元件轉譯後才會有 HTML DOM 元素。
呼叫 StateHasChanged
(參考來源) 以使用從 JS Interop 呼叫所取得的新狀態,將元件的重新轉譯加入佇列 (如需詳細資訊,請參閱 ASP.NET Core Razor 元件轉譯)。 此程式碼不會無限迴圈,因為只有在 scrollPosition
是 null
時才會呼叫 StateHasChanged 。
PrerenderedInterop.razor
:
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS
<PageTitle>Prerendered Interop</PageTitle>
<h1>Prerendered Interop Example</h1>
<div @ref="divElement" style="margin-top:2000px">
Set value via JS interop call: <strong>@scrollPosition</strong>
</div>
@code {
private ElementReference divElement;
private double? scrollPosition;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && scrollPosition is null)
{
scrollPosition = await JS.InvokeAsync<double>(
"scrollElementIntoView", divElement);
StateHasChanged();
}
}
}
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS
<h1>Prerendered Interop Example</h1>
<div @ref="divElement" style="margin-top:2000px">
Set value via JS interop call: <strong>@scrollPosition</strong>
</div>
@code {
private ElementReference divElement;
private double? scrollPosition;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && scrollPosition is null)
{
scrollPosition = await JS.InvokeAsync<double>(
"scrollElementIntoView", divElement);
StateHasChanged();
}
}
}
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS
<h1>Prerendered Interop Example</h1>
<div @ref="divElement" style="margin-top:2000px">
Set value via JS interop call: <strong>@scrollPosition</strong>
</div>
@code {
private ElementReference divElement;
private double? scrollPosition;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && scrollPosition is null)
{
scrollPosition = await JS.InvokeAsync<double>(
"scrollElementIntoView", divElement);
StateHasChanged();
}
}
}
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS
<h1>Prerendered Interop Example</h1>
<div @ref="divElement" style="margin-top:2000px">
Set value via JS interop call: <strong>@scrollPosition</strong>
</div>
@code {
private ElementReference divElement;
private double? scrollPosition;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && scrollPosition is null)
{
scrollPosition = await JS.InvokeAsync<double>(
"scrollElementIntoView", divElement);
StateHasChanged();
}
}
}
@page "/prerendered-interop"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS
<h1>Prerendered Interop Example</h1>
<div @ref="divElement" style="margin-top:2000px">
Set value via JS interop call: <strong>@scrollPosition</strong>
</div>
@code {
private ElementReference divElement;
private double? scrollPosition;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && scrollPosition is null)
{
scrollPosition = await JS.InvokeAsync<double>(
"scrollElementIntoView", divElement);
StateHasChanged();
}
}
}
上述範例會使用全域函式敗壞用戶端。 如需更適合生產應用程式的方法,請參閱 JavaScript 模組中的 JavaScript 隔離。
使用 IDisposable
和 IAsyncDisposable
的元件處置
如果元件實作 IDisposable 或 IAsyncDisposable,則從 UI 移除元件時,架構會呼叫資源處置。 請勿依賴執行這些方法的確切時機。 例如,IAsyncDisposable可以在呼叫或完成OnInitalizedAsync
中等待的非同步Task之前或之後觸發。 此外,物件處置程式碼不應該假設初始化期間建立的物件或其他生命週期方法存在。
元件不需要同時實作 IDisposable 和 IAsyncDisposable。 如果兩者皆實作,則架構只會執行非同步多載。
開發人員程式碼必須確保 IAsyncDisposable 實作不需花費過長的時間才能完成。
JavaScript Interop 物件參考的處置
JavaScript (JS) Interop 文章中的範例會示範典型物件處置模式:
從 .NET 呼叫 JS 時,如從 ASP.NET Core 中的 .NET 方法呼叫 JavaScript 函式 Blazor中所述,請處置從 .NET 或從 JS 所建立的任何 IJSObjectReference/IJSInProcessObjectReference/JSObjectReference,以免流失 JS 記憶體。
如在 ASP.NET Core Blazor 中從 JavaScript 函式呼叫 .NET 方法中所述從 JS 呼叫 .NET 時,請處置從 .NET 或從 JS 建立的 DotNetObjectReference,以免流失 .NET 記憶體。
JS Interop 物件參考會實作為對應,且會由建立參考的 JS Interop 呼叫端上的識別碼建立索引鍵。 從 .NET 或 JS 端起始物件處置時,Blazor 會從對應中移除該項目,只要物件沒有其他強式參考存在,就可以對物件進行記憶體回收。
至少,請一律處置在 .NET 端建立的物件,以免 .NET 受控記憶體流失。
元件處置期間的 DOM 清除工作
如需詳細資訊,請參閱 ASP.NET Core Blazor JavaScript 互通性 (JS Interop)。
如需線路中斷連線時 JSDisconnectedException 的指導,請參閱 ASP.NET CoreBlazor JavaScript 互通性 (JS Interop)。 如需一般 JavaScript Interop 錯誤處理指導,請參閱處理 ASP.NET Core Blazor應用程式中的錯誤中 JavaScript Interop 一節。
同步 IDisposable
針對同步處置工作,請使用 IDisposable.Dispose。
下列元件:
- 使用
@implements
Razor 指示詞實作 IDisposable。 - 處置
obj
,這是實作 IDisposable 的類型。 - 因為生命週期方法中已建立
obj
,因此會執行 null 檢查 (未顯示)。
@implements IDisposable
...
@code {
...
public void Dispose()
{
obj?.Dispose();
}
}
如果單一物件需要處置,則呼叫 Dispose 時,可以使用 Lambda 來處置物件。 下列範例會出現在 ASP.NET CoreRazor 元件轉譯一文中,並示範如何使用 Lambda 運算式來處置 Timer。
TimerDisposal1.razor
:
@page "/timer-disposal-1"
@using System.Timers
@implements IDisposable
<PageTitle>Timer Disposal 1</PageTitle>
<h1>Timer Disposal Example 1</h1>
<p>Current count: @currentCount</p>
@code {
private int currentCount = 0;
private Timer timer = new(1000);
protected override void OnInitialized()
{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
public void Dispose() => timer.Dispose();
}
TimerDisposal1.razor
:
@page "/timer-disposal-1"
@using System.Timers
@implements IDisposable
<PageTitle>Timer Disposal 1</PageTitle>
<h1>Timer Disposal Example 1</h1>
<p>Current count: @currentCount</p>
@code {
private int currentCount = 0;
private Timer timer = new(1000);
protected override void OnInitialized()
{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
public void Dispose() => timer.Dispose();
}
CounterWithTimerDisposal1.razor
:
@page "/counter-with-timer-disposal-1"
@using System.Timers
@implements IDisposable
<h1>Counter with <code>Timer</code> disposal</h1>
<p>Current count: @currentCount</p>
@code {
private int currentCount = 0;
private Timer timer = new(1000);
protected override void OnInitialized()
{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
public void Dispose() => timer.Dispose();
}
CounterWithTimerDisposal1.razor
:
@page "/counter-with-timer-disposal-1"
@using System.Timers
@implements IDisposable
<h1>Counter with <code>Timer</code> disposal</h1>
<p>Current count: @currentCount</p>
@code {
private int currentCount = 0;
private Timer timer = new(1000);
protected override void OnInitialized()
{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
public void Dispose() => timer.Dispose();
}
CounterWithTimerDisposal1.razor
:
@page "/counter-with-timer-disposal-1"
@using System.Timers
@implements IDisposable
<h1>Counter with <code>Timer</code> disposal</h1>
<p>Current count: @currentCount</p>
@code {
private int currentCount = 0;
private Timer timer = new(1000);
protected override void OnInitialized()
{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
public void Dispose() => timer.Dispose();
}
CounterWithTimerDisposal1.razor
:
@page "/counter-with-timer-disposal-1"
@using System.Timers
@implements IDisposable
<h1>Counter with <code>Timer</code> disposal</h1>
<p>Current count: @currentCount</p>
@code {
private int currentCount = 0;
private Timer timer = new Timer(1000);
protected override void OnInitialized()
{
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
public void Dispose() => timer.Dispose();
}
注意
在上述範例中,對 StateHasChanged 的呼叫會由對 ComponentBase.InvokeAsync 的呼叫包裝,因為回撥是在 Blazor 的同步處理內容外部叫用。 如需詳細資訊,請參閱 ASP.NET Core Razor 元件轉譯。
如果在生命週期方法中建立物件 (例如 OnInitialized{Async}
),則會在呼叫 Dispose
之前先檢查 null
。
TimerDisposal2.razor
:
@page "/timer-disposal-2"
@using System.Timers
@implements IDisposable
<PageTitle>Timer Disposal 2</PageTitle>
<h1>Timer Disposal Example 2</h1>
<p>Current count: @currentCount</p>
@code {
private int currentCount = 0;
private Timer? timer;
protected override void OnInitialized()
{
timer = new Timer(1000);
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
public void Dispose() => timer?.Dispose();
}
TimerDisposal2.razor
:
@page "/timer-disposal-2"
@using System.Timers
@implements IDisposable
<PageTitle>Timer Disposal 2</PageTitle>
<h1>Timer Disposal Example 2</h1>
<p>Current count: @currentCount</p>
@code {
private int currentCount = 0;
private Timer? timer;
protected override void OnInitialized()
{
timer = new Timer(1000);
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
public void Dispose() => timer?.Dispose();
}
CounterWithTimerDisposal2.razor
:
@page "/counter-with-timer-disposal-2"
@using System.Timers
@implements IDisposable
<h1>Counter with <code>Timer</code> disposal</h1>
<p>Current count: @currentCount</p>
@code {
private int currentCount = 0;
private Timer? timer;
protected override void OnInitialized()
{
timer = new Timer(1000);
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
public void Dispose() => timer?.Dispose();
}
CounterWithTimerDisposal2.razor
:
@page "/counter-with-timer-disposal-2"
@using System.Timers
@implements IDisposable
<h1>Counter with <code>Timer</code> disposal</h1>
<p>Current count: @currentCount</p>
@code {
private int currentCount = 0;
private Timer? timer;
protected override void OnInitialized()
{
timer = new Timer(1000);
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
public void Dispose() => timer?.Dispose();
}
CounterWithTimerDisposal2.razor
:
@page "/counter-with-timer-disposal-2"
@using System.Timers
@implements IDisposable
<h1>Counter with <code>Timer</code> disposal</h1>
<p>Current count: @currentCount</p>
@code {
private int currentCount = 0;
private Timer timer;
protected override void OnInitialized()
{
timer = new Timer(1000);
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
public void Dispose() => timer?.Dispose();
}
CounterWithTimerDisposal2.razor
:
@page "/counter-with-timer-disposal-2"
@using System.Timers
@implements IDisposable
<h1>Counter with <code>Timer</code> disposal</h1>
<p>Current count: @currentCount</p>
@code {
private int currentCount = 0;
private Timer timer;
protected override void OnInitialized()
{
timer = new Timer(1000);
timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
timer.Start();
}
private void OnTimerCallback()
{
_ = InvokeAsync(() =>
{
currentCount++;
StateHasChanged();
});
}
public void Dispose() => timer?.Dispose();
}
如需詳細資訊,請參閱
非同步 IAsyncDisposable
針對非同步處置工作,請使用 IAsyncDisposable.DisposeAsync。
下列元件:
- 使用
@implements
Razor 指示詞實作 IAsyncDisposable。 - 處置
obj
,這是實作 IAsyncDisposable 的非受控類型。 - 因為生命週期方法中已建立
obj
,因此會執行 null 檢查 (未顯示)。
@implements IAsyncDisposable
...
@code {
...
public async ValueTask DisposeAsync()
{
if (obj is not null)
{
await obj.DisposeAsync();
}
}
}
如需詳細資訊,請參閱
已處置物件的 null
指派
呼叫 Dispose/DisposeAsync 之後,通常不需要指派 null
給已處置的物件。 指派 null
的罕見案例包括:
- 如果物件類型實作不佳,且不容許對 Dispose/DisposeAsync 重複呼叫,請在處置之後指派
null
,以順利略過對 Dispose/DisposeAsync 的進一步呼叫。 - 如果長時間存留的流程繼續保存已處置物件的參考,則指派
null
可讓記憶體回收行程釋放物件,儘管長期存留的流程仍保存該物件參考。
這些是不尋常的案例。 對於正確實作且正常運作的物件,將 null
指派給已處置的物件沒有意義。 在必須將 null
指派給物件的罕見情況下,建議您記錄原因,並尋求不須指派 null
的解決方案。
StateHasChanged
注意
不支援在 Dispose
和 DisposeAsync
中呼叫 StateHasChanged。 StateHasChanged 可能會在卸載轉譯器時叫用,因此不支援在該時間點要求 UI 更新。
事件處理常式
請一律從 .NET 事件取消訂閱事件處理常式。 下列 Blazor 表單範例示範如何在 Dispose
方法中取消訂閱事件處理常式:
私人欄位和 Lambda 方法
@implements IDisposable <EditForm ... EditContext="editContext" ...> ... <button type="submit" disabled="@formInvalid">Submit</button> </EditForm> @code { ... private EventHandler<FieldChangedEventArgs>? fieldChanged; protected override void OnInitialized() { editContext = new(model); fieldChanged = (_, __) => { ... }; editContext.OnFieldChanged += fieldChanged; } public void Dispose() { editContext.OnFieldChanged -= fieldChanged; } }
私人方法
@implements IDisposable <EditForm ... EditContext="editContext" ...> ... <button type="submit" disabled="@formInvalid">Submit</button> </EditForm> @code { ... protected override void OnInitialized() { editContext = new(model); editContext.OnFieldChanged += HandleFieldChanged; } private void HandleFieldChanged(object sender, FieldChangedEventArgs e) { ... } public void Dispose() { editContext.OnFieldChanged -= HandleFieldChanged; } }
如需詳細資訊,請參閱使用 IDisposable
和 IAsyncDisposable
處置元件一節。
如需 EditForm 元件和表單的詳細資訊,請參閱 ASP.NET Core Blazor 表單概觀和 Forms 節點中的其他表單文章。
匿名函式、方法和運算式
使用匿名函式、方法或運算式時,不需要實作 IDisposable 和取消訂閱委派。 不過,當公開事件物件超過註冊委派的元件存留期時,無法取消訂閱委派是個問題。 發生此情況時,會產生記憶體流失,因為已註冊的委派讓原始物件保持運作。 因此,請只有在您知道事件委派會快速處置時,才使用下列方法。 當懷疑需要處置的物件存留期時,請訂閱委派方法並適當處置委派,如先前範例所示。
匿名 Lambda 方法 (不需要明確處置):
private void HandleFieldChanged(object sender, FieldChangedEventArgs e) { formInvalid = !editContext.Validate(); StateHasChanged(); } protected override void OnInitialized() { editContext = new(starship); editContext.OnFieldChanged += (s, e) => HandleFieldChanged((editContext)s, e); }
匿名 Lambda 運算式方法 (不需要明確處置):
private ValidationMessageStore? messageStore; [CascadingParameter] private EditContext? CurrentEditContext { get; set; } protected override void OnInitialized() { ... messageStore = new(CurrentEditContext); CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear(); CurrentEditContext.OnFieldChanged += (s, e) => messageStore.Clear(e.FieldIdentifier); }
具有匿名 Lambda 運算式的上述程式碼完整範例會出現在 ASP.NET Core Blazor 表單驗證一文中。
如需詳細資訊,請參閱清除非受控資源及其後續關於實作 Dispose
和 DisposeAsync
方法的主題。
可取消的背景工作
元件通常會執行長時間執行的背景工作,例如進行網路呼叫 (HttpClient) 並與資料庫互動。 建議您在數種情況下停止背景工作,以節省系統資源。 例如,當使用者瀏覽離開元件時,背景非同步作業不會自動停止。
背景工作項目可能需要取消的其他原因包括:
- 執行中的背景工作是以錯誤輸入資料或處理參數啟動。
- 目前執行背景工作項目的集合必須取代為一組新工作項目。
- 目前執行中工作的優先順序必須變更。
- 應用程式必須關閉,才能重新部署伺服器。
- 伺服器資源變得有限,需要重新排程背景工作項目。
若要在元件中實作可取消的背景工作模式:
- 使用 CancellationTokenSource 和 CancellationToken。
- 在處理元件和在任何需要手動取消權杖的時間點時,請呼叫
CancellationTokenSource.Cancel
以表示應該取消背景工作。 - 非同步呼叫傳回之後,請在權杖上呼叫 ThrowIfCancellationRequested。
在以下範例中:
await Task.Delay(5000, cts.Token);
代表長時間執行的非同步背景工作。BackgroundResourceMethod
代表長時間執行的背景方法,如果在呼叫方法之前處置Resource
,則不應該啟動此方法。
BackgroundWork.razor
:
@page "/background-work"
@implements IDisposable
@inject ILogger<BackgroundWork> Logger
<PageTitle>Background Work</PageTitle>
<h1>Background Work Example</h1>
<p>
<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>
</p>
<p>Study logged messages in the console.</p>
<p>
If you trigger disposal within 10 seconds of page load, the
<code>BackgroundResourceMethod</code> isn't executed.
</p>
<p>
If disposal occurs after <code>BackgroundResourceMethod</code> is called but
before action is taken on the resource, an <code>ObjectDisposedException</code>
is thrown by <code>BackgroundResourceMethod</code>, and the resource isn't
processed.
</p>
@code {
private Resource resource = new();
private CancellationTokenSource cts = new();
private IList<string> messages = [];
protected async Task LongRunningWork()
{
Logger.LogInformation("Long running work started");
await Task.Delay(10000, cts.Token);
cts.Token.ThrowIfCancellationRequested();
resource.BackgroundResourceMethod(Logger);
}
public void Dispose()
{
Logger.LogInformation("Executing Dispose");
if (!cts.IsCancellationRequested)
{
cts.Cancel();
}
cts?.Dispose();
resource?.Dispose();
}
private class Resource : IDisposable
{
private bool disposed;
public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
{
logger.LogInformation("BackgroundResourceMethod: Start method");
if (disposed)
{
logger.LogInformation("BackgroundResourceMethod: Disposed");
throw new ObjectDisposedException(nameof(Resource));
}
// Take action on the Resource
logger.LogInformation("BackgroundResourceMethod: Action on Resource");
}
public void Dispose() => disposed = true;
}
}
@page "/background-work"
@implements IDisposable
@inject ILogger<BackgroundWork> Logger
<PageTitle>Background Work</PageTitle>
<h1>Background Work Example</h1>
<p>
<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>
</p>
<p>Study logged messages in the console.</p>
<p>
If you trigger disposal within 10 seconds of page load, the
<code>BackgroundResourceMethod</code> isn't executed.
</p>
<p>
If disposal occurs after <code>BackgroundResourceMethod</code> is called but
before action is taken on the resource, an <code>ObjectDisposedException</code>
is thrown by <code>BackgroundResourceMethod</code>, and the resource isn't
processed.
</p>
@code {
private Resource resource = new();
private CancellationTokenSource cts = new();
private IList<string> messages = [];
protected async Task LongRunningWork()
{
Logger.LogInformation("Long running work started");
await Task.Delay(10000, cts.Token);
cts.Token.ThrowIfCancellationRequested();
resource.BackgroundResourceMethod(Logger);
}
public void Dispose()
{
Logger.LogInformation("Executing Dispose");
if (!cts.IsCancellationRequested)
{
cts.Cancel();
}
cts?.Dispose();
resource?.Dispose();
}
private class Resource : IDisposable
{
private bool disposed;
public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
{
logger.LogInformation("BackgroundResourceMethod: Start method");
if (disposed)
{
logger.LogInformation("BackgroundResourceMethod: Disposed");
throw new ObjectDisposedException(nameof(Resource));
}
// Take action on the Resource
logger.LogInformation("BackgroundResourceMethod: Action on Resource");
}
public void Dispose() => disposed = true;
}
}
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger
<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>
@code {
private Resource resource = new();
private CancellationTokenSource cts = new();
protected async Task LongRunningWork()
{
Logger.LogInformation("Long running work started");
await Task.Delay(5000, cts.Token);
cts.Token.ThrowIfCancellationRequested();
resource.BackgroundResourceMethod(Logger);
}
public void Dispose()
{
Logger.LogInformation("Executing Dispose");
cts.Cancel();
cts.Dispose();
resource?.Dispose();
}
private class Resource : IDisposable
{
private bool disposed;
public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
{
logger.LogInformation("BackgroundResourceMethod: Start method");
if (disposed)
{
logger.LogInformation("BackgroundResourceMethod: Disposed");
throw new ObjectDisposedException(nameof(Resource));
}
// Take action on the Resource
logger.LogInformation("BackgroundResourceMethod: Action on Resource");
}
public void Dispose()
{
disposed = true;
}
}
}
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger
<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>
@code {
private Resource resource = new();
private CancellationTokenSource cts = new();
protected async Task LongRunningWork()
{
Logger.LogInformation("Long running work started");
await Task.Delay(5000, cts.Token);
cts.Token.ThrowIfCancellationRequested();
resource.BackgroundResourceMethod(Logger);
}
public void Dispose()
{
Logger.LogInformation("Executing Dispose");
cts.Cancel();
cts.Dispose();
resource?.Dispose();
}
private class Resource : IDisposable
{
private bool disposed;
public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
{
logger.LogInformation("BackgroundResourceMethod: Start method");
if (disposed)
{
logger.LogInformation("BackgroundResourceMethod: Disposed");
throw new ObjectDisposedException(nameof(Resource));
}
// Take action on the Resource
logger.LogInformation("BackgroundResourceMethod: Action on Resource");
}
public void Dispose()
{
disposed = true;
}
}
}
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger
<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>
@code {
private Resource resource = new();
private CancellationTokenSource cts = new();
protected async Task LongRunningWork()
{
Logger.LogInformation("Long running work started");
await Task.Delay(5000, cts.Token);
cts.Token.ThrowIfCancellationRequested();
resource.BackgroundResourceMethod(Logger);
}
public void Dispose()
{
Logger.LogInformation("Executing Dispose");
cts.Cancel();
cts.Dispose();
resource?.Dispose();
}
private class Resource : IDisposable
{
private bool disposed;
public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
{
logger.LogInformation("BackgroundResourceMethod: Start method");
if (disposed)
{
logger.LogInformation("BackgroundResourceMethod: Disposed");
throw new ObjectDisposedException(nameof(Resource));
}
// Take action on the Resource
logger.LogInformation("BackgroundResourceMethod: Action on Resource");
}
public void Dispose()
{
disposed = true;
}
}
}
@page "/background-work"
@using System.Threading
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger<BackgroundWork> Logger
<button @onclick="LongRunningWork">Trigger long running work</button>
<button @onclick="Dispose">Trigger Disposal</button>
@code {
private Resource resource = new Resource();
private CancellationTokenSource cts = new CancellationTokenSource();
protected async Task LongRunningWork()
{
Logger.LogInformation("Long running work started");
await Task.Delay(5000, cts.Token);
cts.Token.ThrowIfCancellationRequested();
resource.BackgroundResourceMethod(Logger);
}
public void Dispose()
{
Logger.LogInformation("Executing Dispose");
cts.Cancel();
cts.Dispose();
resource?.Dispose();
}
private class Resource : IDisposable
{
private bool disposed;
public void BackgroundResourceMethod(ILogger<BackgroundWork> logger)
{
logger.LogInformation("BackgroundResourceMethod: Start method");
if (disposed)
{
logger.LogInformation("BackgroundResourceMethod: Disposed");
throw new ObjectDisposedException(nameof(Resource));
}
// Take action on the Resource
logger.LogInformation("BackgroundResourceMethod: Action on Resource");
}
public void Dispose()
{
disposed = true;
}
}
}
Blazor Server 重新連線事件
本文所涵蓋的元件生命週期事件會與伺服器端重新連線事件處理常式分開運作。 當用戶端 SignalR 連線遺失時,只會中斷 UI 更新。 重新建立連線時,會繼續 UI 更新。 如需線路處理常式事件和設定的詳細資訊,請參閱 ASP.NET Core BlazorSignalR 指導。