ASP.NET Core Razor 元件生命週期

注意

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

重要

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

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

本文說明 ASP.NET Core Razor 元件生命週期,以及如何使用生命週期事件。

週期事件

Razor 元件會在一組同步和非同步生命週期方法中處理 Razor 元件生命週期事件。 您可以在元件初始化和轉譯期間覆寫生命週期方法,以在元件初始化和轉譯期間執行元件中的其他作業。

本文簡化了元件生命週期事件處理,以便釐清複雜的架構邏輯,並不涵蓋多年來所做的每項變更。 您可能需要存取 ComponentBase 參考來源 ,以整合自訂事件處理與 Blazor 的生命週期事件處理。 參考來源中的程式碼批註包含生命週期事件處理的其他備註,且這些生命週期事件處理未出現在本文或 API 文件中。

注意

.NET 參考來源的文件連結通常會載入存放庫的預設分支,這表示下一版 .NET 的目前開發。 若要選取特定版本的標籤,請使用 [切換分支或標籤] 下拉式清單。 如需詳細資訊,請參閱如何選取 ASP.NET Core 原始程式碼 (dotnet/AspNetCore.Docs #26205) 的版本標籤

下列簡化圖表說明 Razor 元件生命週期事件處理。 與生命週期事件相關聯的 C# 方法會定義本文下列各節中的範例。

元件生命週期事件:

  1. 如果元件第一次在要求上轉譯:
    • 建立元件的執行個體。
    • 執行屬性插入。
    • 呼叫 OnInitialized{Async}。 如果傳回不完整的 Task,則 Task 會等候,然後重新轉譯元件。 同步方法是在非同步方法之前呼叫。
  2. 呼叫 OnParametersSet{Async}。 如果傳回不完整的 Task,則 Task 會等候,然後重新轉譯元件。 同步方法是在非同步方法之前呼叫。
  3. 轉譯所有同步工作並完成 Task

注意

在生命週期事件中執行的非同步動作在轉譯元件前可能尚未完成。 如需詳細資訊,請參閱本文稍後的處理轉譯時不完整的非同步動作一節。

父代元件會在其子系元件之前轉譯,因為轉譯是決定哪些子系存在的因素。 如果使用同步父代元件初始化,則保證父代初始化會先完成。 如果使用非同步父代元件初始化,則無法判斷父代和子系元件初始化的完成順序,因為該順序取決於執行中的初始化程式碼。

Blazor 中 Razor 元件的元件生命週期事件

DOM 事件處理:

  1. 事件處理常式正在執行。
  2. 如果傳回不完整的 Task,則 Task 會等候,然後重新轉譯元件。
  3. 轉譯所有同步工作並完成 Task

DOM 事件處理

Render 生命週期:

  1. 當下列兩個條件均符合時,避免在元件上進行進一步的轉譯作業:
  2. 組建轉譯樹狀結構差異,並轉譯元件。
  3. 等候 DOM 更新。
  4. 呼叫 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);

如果在開發人員程式碼中提供事件處理常式,請在處置時將其解除連結。 如需詳細資訊,請參閱使用 IDisposableIAsyncDisposable 處置元件一節。

在下列範例中,如果剖析 Param 的路由參數成功,則 ParameterView.TryGetValue 會將 Param 參數的值指派給 value。 當 value 不是 null 時,值會依元件顯示。

雖然路由參數比對不區分大小寫,但 TryGetValue 只會比對路由範本中的區分大小寫參數名稱。 下列範例需要使用路由範本中的 /{Param?},才能使用 TryGetValue 而非 /{param?} 取得值。 如果在此案例中使用 /{param?},則 TryGetValue 會傳回 falsemessage未設定為任一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?}"

<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})

OnInitializedOnInitializedAsync 專門用於在元件執行個體的整個存留期中初始化元件。 參數值和參數值變更不應影響在這些方法中執行的初始化。 例如,將靜態選項載入至下拉式清單中,其在該元件的存留期中不會改變,且不相依於參數值,會在下列其中一個生命週期方法中執行。 如果參數值或參數值變更會影響元件狀態,請改用 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"

<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 中的開發人員程式碼在預先轉譯時執行兩次,請參閱預先轉譯之後的具狀態重新連線一節。 本節內容著重於 BlazorWeb Apps 和具狀態的 SignalR重新連線。 若要在預先轉譯初始化程式碼執行期間保留狀態,請參閱預先轉譯 ASP.NET CoreRazor 元件

若要防止 OnInitializedAsync 中的開發人員程式碼在預先轉譯時執行兩次,請參閱預先轉譯之後的具狀態重新連線一節。 雖然本節內容著重於 Blazor Server 和具狀態的SignalR重新連線 ,但裝載 Blazor WebAssembly 解決方案 (WebAssemblyPrerendered) 預先轉譯的案例涉及類似條件和方法,以防止開發人員程式碼執行兩次。 若要在預先轉譯初始化程式碼執行期間保留狀態,請參閱預先轉譯並整合 ASP.NET CoreRazor 元件

雖然 Blazor 應用程式會預先轉譯,但無法進行呼叫 JavaScript (JS Interop) 等特定動作。 預先轉譯時,元件可能需要以不同的方式轉譯。 如需詳細資訊,請參閱使用 JavaScript Interop 預先轉譯一節。

如果在開發人員程式碼中提供事件處理常式,請在處置時將其解除連結。 如需詳細資訊,請參閱使用 IDisposableIAsyncDisposable 處置元件一節。

使用串流轉譯搭配靜態伺服器端轉譯 (靜態 SSR) 或預先轉譯,以改善在 OnInitializedAsync 中執行長時間執行非同步工作之元件的使用者體驗,以便完全轉譯。 如需詳細資訊,請參閱 ASP.NET Core Razor 元件轉譯

設定參數之後 (OnParametersSet{Async})

呼叫 OnParametersSetOnParametersSetAsync

  • OnInitializedOnInitializedAsync 中初始化元件之後。

  • 當父代元件重新轉譯並提供時:

    • 至少有一個參數變更時的已知或基本不可變類型。
    • 複雜型別參數。 架構無法得知複雜型別參數的值是否已在內部變動,因此當一或多個複雜型別參數存在時,架構一律會將參數集視為已變更。

    如需關於轉譯慣例的詳細資訊,請參閱 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}"

<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。 如需詳細資訊,請參閱基底類別生命週期方法一節。

如果在開發人員程式碼中提供事件處理常式,請在處置時將其解除連結。 如需詳細資訊,請參閱使用 IDisposableIAsyncDisposable 處置元件一節。

如需路由參數和限制式的詳細資訊,請參閱 ASP.NET Core Blazor 路由和導覽

如需手動實作 SetParametersAsync 以改善某些案例效能的範例,請參閱 ASP.NET Core Blazor 效能最佳做法

元件轉譯之後 (OnAfterRender{Async})

OnAfterRenderOnAfterRenderAsync 會在元件以互動方式轉譯且 UI 完成更新之後叫用 (例如,將元素新增至瀏覽器 DOM 之後)。 此時會填入元素和元件參考。 使用此階段來執行轉譯內容的其他初始化步驟,例如與轉譯 DOM 元素互動的 JS Interop 呼叫。 同步方法是在非同步方法之前呼叫。

這些方法不會在伺服器上於預先轉譯或靜態伺服器端轉譯 (靜態 SSR) 期間叫用,因為這些流程不會附加至即時瀏覽器 DOM,且已在更新 DOM 之前完成。

針對 OnAfterRenderAsync,元件不會在任何傳回的 Task 完成之後自動重新轉譯,以避免無限轉譯迴圈。

OnAfterRenderOnAfterRenderAsync 會在元件完成轉譯之後呼叫。 此時會填入元素和元件參考。 使用此階段來執行轉譯內容的其他初始化步驟,例如與轉譯 DOM 元素互動的 JS Interop 呼叫。 同步方法是在非同步方法之前呼叫。

這些方法不會在預先定義期間叫用,因為預先轉譯不會附加至即時瀏覽器 DOM,而且已在更新 DOM 之前完成。

針對 OnAfterRenderAsync,元件不會在任何傳回的 Task 完成之後自動重新轉譯,以避免無限轉譯迴圈。

OnAfterRenderOnAfterRenderAsyncfirstRender 參數:

  • 第一次轉譯元件執行個體時會設定為 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("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"
@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 完成之後排程進一步轉譯週期。

OnAfterRenderOnAfterRenderAsync在伺服器預先轉譯流程期間不會呼叫。 當元件在預先轉譯之後以互動方式轉譯時,會呼叫方法。 當應用程式預先轉譯時:

  1. 元件會在伺服器上執行,以在 HTTP 回應中產生一些靜態 HTML 標記。 在此階段期間,不會呼叫 OnAfterRenderOnAfterRenderAsync
  2. 當 Blazor 指令碼 (blazor.{server|webassembly|web}.js) 在瀏覽器中啟動時,元件會以互動式轉譯模式重新啟動。 重新啟動元件之後,「會」呼叫 OnAfterRenderOnAfterRenderAsync,因為應用程式不再處於預先轉譯階段。

如果在開發人員程式碼中提供事件處理常式,請在處置時將其解除連結。 如需詳細資訊,請參閱使用 IDisposableIAsyncDisposable 處置元件一節。

基底類別生命週期方法

覆寫 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"
@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;
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)。 當 moviesnull 時,會向使用者顯示載入訊息。 由 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 &mdash; @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 檔案中,在服務集合上新增具有 AddMemoryCacheIMemoryCache 服務:

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;

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 指導

本節內容著重於 BlazorWeb Apps 和具狀態的 SignalR重新連線。 若要在預先轉譯初始化程式碼執行期間保留狀態,請參閱預先轉譯 ASP.NET CoreRazor 元件

雖然本節內容著重於 Blazor Server 和具狀態的SignalR重新連線 ,但裝載 Blazor WebAssembly 解決方案 (WebAssemblyPrerendered) 預先轉譯的案例涉及類似條件和方法,以防止開發人員程式碼執行兩次。 若要在預先轉譯初始化程式碼執行期間保留狀態,請參閱預先轉譯並整合 ASP.NET CoreRazor 元件

使用 JavaScript Interop 預先轉譯

本節適用於會預先轉譯 Razor 元件的伺服器端應用程式。 預先轉譯 ASP.NET Core Razor 元件中會有預先轉譯的說明。

注意

Blazor Web Apps 中互動式路由的內部瀏覽並不涉及向伺服器要求新的頁面內容。 因此,內部頁面要求不會發生預先轉譯。 如果應用程式採用互動式路由,請針對示範預先轉譯行為的元件範例執行完整頁面重載。 如需詳細資訊,請參閱預先轉譯 ASP.NET Core Razor 元件

本節適用於會預先轉譯 Razor 元件的伺服器端應用程式和所裝載的 Blazor WebAssembly 應用程式。 預先轉譯和整合 ASP.NET Core Razor 元件中會有預先轉譯的說明。

雖然應用程式會預先轉譯,但無法進行呼叫 JavaScript (JS) 等特定動作。

下列範例會使用 JSRuntimeExtensions.InvokeVoidAsync 來呼叫 setElementText1 函式,而且此函式不會傳回值。

注意

如需 JS 位置的一般指導和我們對於生產應用程式的建議,請參閱 ASP.NET Core Blazor 應用程式中的 JavaScript 位置

<script>
  window.setElementText1 = (element, text) => element.innerText = text;
</script>

警告

上述範例是為了示範才直接修改 DOM。 大部分情況下,不建議使用 JS 直接修改 DOM,因為 JS 可能會干擾 Blazor 的變更追蹤。 如需詳細資訊,請參閱 ASP.NET Core Blazor JavaScript 互通性 (JS Interop)

在伺服器上進行預先轉譯程序的期間,不會呼叫 OnAfterRender{Async} 生命週期事件。 請覆寫 OnAfterRender{Async} 方法,以將 JS Interop 呼叫延到預先轉譯後元件已在用戶端上完成轉譯並可以互動為止。

PrerenderedInterop1.razor

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<PageTitle>Prerendered Interop 1</PageTitle>

<h1>Prerendered Interop Example 1</h1>

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}

注意

上述範例會使用全域函式敗壞用戶端。 如需更適合生產應用程式的方法,請參閱 JavaScript 模組中的 JavaScript 隔離

範例:

export setElementText1 = (element, text) => element.innerText = text;

下列元件示範如何透過與預先轉譯相容的方式,將 JS Interop 作為元件初始化邏輯的一部分。 此元件顯示我們可以從 OnAfterRenderAsync 內部觸發轉譯更新。 開發人員必須小心,以免在這個情況下建立無限迴圈。

下列範例會使用 IJSRuntime.InvokeAsync 來呼叫 setElementText2 函式,而且此函式會傳回值。

注意

如需 JS 位置的一般指導和我們對於生產應用程式的建議,請參閱 ASP.NET Core Blazor 應用程式中的 JavaScript 位置

<script>
  window.setElementText2 = (element, text) => {
    element.innerText = text;
    return text;
  };
</script>

警告

上述範例是為了示範才直接修改 DOM。 大部分情況下,不建議使用 JS 直接修改 DOM,因為 JS 可能會干擾 Blazor 的變更追蹤。 如需詳細資訊,請參閱 ASP.NET Core Blazor JavaScript 互通性 (JS Interop)

在呼叫了 JSRuntime.InvokeAsync 時,ElementReference 只會在 OnAfterRenderAsync 中使用,而不會在任何先前的生命週期方法中使用,因為要等到元件轉譯後才會有 HTML DOM 元素。

為了使用從 JS Interop 呼叫所取得的新狀態來重新轉譯元件 (如需詳細資訊,請參閱 ASP.NET Core Razor 元件轉譯),因此會呼叫 StateHasChanged。 此程式碼不會建立無限迴圈,因為只有在 datanull 時才會呼叫 StateHasChanged

PrerenderedInterop2.razor

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<PageTitle>Prerendered Interop 2</PageTitle>

<h1>Prerendered Interop Example 2</h1>

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(infoFromJs ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call: 
    <strong id="val-set-by-interop" @ref="divElement"></strong>
</p>



@code {
    private string? infoFromJs;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && infoFromJs == null)
        {
            infoFromJs = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}

注意

上述範例會使用全域函式敗壞用戶端。 如需更適合生產應用程式的方法,請參閱 JavaScript 模組中的 JavaScript 隔離

範例:

export setElementText2 = (element, text) => {
  element.innerText = text;
  return text;
};

使用 IDisposableIAsyncDisposable 的元件處置

如果元件實作 IDisposableIAsyncDisposable 或兩者,則從 UI 移除元件時,架構會呼叫資源處置。 您可以隨時進行處置,包括元件初始化期間。

元件不需要同時實作 IDisposableIAsyncDisposable。 如果兩者皆實作,則架構只會執行非同步多載。

開發人員程式碼必須確保 IAsyncDisposable 實作不需花費過長的時間才能完成。

JavaScript Interop 物件參考的處置

JavaScript (JS) Interop 文章中的範例會示範典型物件處置模式:

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

下列元件:

  • 使用 @implementsRazor 指示詞實作 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();
}

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();
}

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 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

注意

不支援在 DisposeDisposeAsync 中呼叫 StateHasChangedStateHasChanged 可能會在卸載轉譯器時叫用,因此不支援在該時間點要求 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;
        }
    }
    

如需詳細資訊,請參閱使用 IDisposableIAsyncDisposable 處置元件一節。

如需 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 表單驗證一文中。

如需詳細資訊,請參閱清除非受控資源及其後續關於實作 DisposeDisposeAsync 方法的主題。

可取消的背景工作

元件通常會執行長時間執行的背景工作,例如進行網路呼叫 (HttpClient) 並與資料庫互動。 建議您在數種情況下停止背景工作,以節省系統資源。 例如,當使用者瀏覽離開元件時,背景非同步作業不會自動停止。

背景工作項目可能需要取消的其他原因包括:

  • 執行中的背景工作是以錯誤輸入資料或處理參數啟動。
  • 目前執行背景工作項目的集合必須取代為一組新工作項目。
  • 目前執行中工作的優先順序必須變更。
  • 應用程式必須關閉,才能重新部署伺服器。
  • 伺服器資源變得有限,需要重新排程背景工作項目。

若要在元件中實作可取消的背景工作模式:

在以下範例中:

  • 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"
@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 指導

其他資源

處理在 Razor 元件生命週期外攔截到的例外狀況