共用方式為


ASP.NET Core Razor 元件生命週期

注意

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

警告

不再支援此版本的 ASP.NET Core。 如需詳細資訊,請參閱 .NET 和 .NET Core 支持原則。 如需目前的版本,請參閱 本文的 .NET 9 版本。

重要

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

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

本文說明 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

注意

在生命週期事件中執行的異步動作可能會延遲元件轉譯或顯示數據。 如需詳細資訊,請參閱本文後面一節,標題為 呈現 未完成的非同步動作。

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

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

DOM 事件處理:

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

DOM 事件處理

Render 生命週期:

  1. 當下列兩個條件均符合時,避免在元件上進行進一步的轉譯作業:
  2. 組建轉譯樹狀結構差異,並轉譯元件。
  3. 等候 DOM 更新。
  4. 呼叫 OnAfterRender{Async}。 同步方法是在非同步方法之前呼叫。

轉譯生命週期

開發人員呼叫 StateHasChanged 會導致重新轉譯。 如需詳細資訊,請參閱 ASP.NET Core Razor 元件轉譯

預先呈現期間的靜止狀態

在伺服器端 Blazor 應用程式中,預渲染會等到 靜止狀態,這表示只有當渲染樹中的所有元件都完成渲染後,元件才會被渲染。 當元件在初始化和其他生命週期方法中進行長時間的工作時,因為缺乏活動而導致渲染明顯延遲,這會帶來不佳的用戶體驗。 如需詳細資訊,請參閱本文稍後一節的在轉譯時處理不完整的異步動作

設定參數時 (SetParametersAsync)

轉譯樹狀結構中元件父代或路由參數所提供的 SetParametersAsync 設定參數。

方法的 ParameterView 參數包含每次呼叫 時,元件的SetParametersAsync 值集。 藉由覆寫 SetParametersAsync 方法,開發人員程式碼可以直接與 ParameterView 的參數互動。

SetParametersAsync 的預設實作會使用 [Parameter] 中具有對應值的 [CascadingParameter]ParameterView,設定每個屬性的值。 在 ParameterView 中沒有對應值的參數會保持不變。

一般而言,覆寫 await base.SetParametersAsync(parameters); 時,您的程式碼應該呼叫基底類別方法 (SetParametersAsync)。 在進階案例中,開發人員程式碼可以透過不叫用基底類別方法所需的任何方式解譯傳入參數的值。 例如,不需要將傳入參數指派給類別的屬性。 不過,在建構程式碼而不呼叫基底類別方法時,您必須參考 ComponentBase 參考來源,因為其會呼叫其他生命週期方法,並以複雜的方式觸發轉譯。

注意

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

如果您想要依賴 ComponentBase.SetParametersAsync 的初始化和轉譯邏輯,但不處理傳入參數,您可以選擇將空白 ParameterView 傳遞至基底類別方法:

await base.SetParametersAsync(ParameterView.Empty);

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

在下列範例中,如果剖析 ParameterView.TryGetValue 的路由參數成功,則 Param 會將 value 參數的值指派給 Param。 當 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?}"

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

<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 Core Razor 元件與 MVC 或 Razor Pages

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

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

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

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

呼叫 OnParametersSetOnParametersSetAsync

  • OnInitializedOnInitializedAsync 中初始化元件之後。

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

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

    如需關於轉譯慣例的詳細資訊,請參閱 ASP.NET Core Razor 元件轉譯

同步方法是在非同步方法之前呼叫。

即使參數值尚未變更,也可以叫用方法。 這種行為強調開發人員需要在方法內實作其他邏輯,以檢查參數值是否確實在重新初始化資料或相依於這些參數的狀態之前已變更。

針對下列範例元件,瀏覽至 URL 上的元件頁面:

  • 具有在 StartDate 前收到的開始日期:/on-parameters-set/2021-03-19
  • 沒有開始日期,此情況下 StartDate 會指派目前當地時間的值:/on-parameters-set

注意

在元件路由中,您無法同時使用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。 如需詳細資訊,請參閱基底類別生命週期方法一節。

如果在開發人員程式碼中提供事件處理常式,請在處置時將其解除連結。 如需詳細資訊,請參閱使用 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 完成之後自動重新轉譯,以避免無限轉譯迴圈。

firstRenderOnAfterRenderOnAfterRenderAsync 參數:

  • 第一次轉譯元件執行個體時會設定為 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。 如需詳細資訊,請參閱基底類別生命週期方法一節。

即使您從 Task 傳回 OnAfterRenderAsync,架構也不會在完成該工作之後,為元件排程進一步的轉譯週期。 這是為了避免無限轉譯迴圈。 這與其他生命週期方法不同,其他生命週期方法會在傳回的 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"
@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,可在應用程式的主要執行緒可用時,將發生的重新轉譯加入佇列。

系統會自動針對 StateHasChanged 方法呼叫 EventCallback。 如需事件回撥的詳細資訊,請參閱 ASP.NET CoreBlazor 事件處理

如需關於元件轉譯和何時呼叫 StateHasChanged (包括何時使用 ComponentBase.InvokeAsync 叫用) 的詳細資訊,請參閱 ASP.NET Core Razor 元件轉譯

處理渲染時不完整的異步操作

在生命週期事件中執行的非同步動作,在轉譯元件前可能尚未完成。 當生命週期方法執行時,物件可能會 null 或不完整地填入資料。 提供轉譯邏輯,以確認物件已初始化。 轉譯預留位置 UI 元素 (例如載入訊息),而物件為 null

在下列 Slow 元件中,會覆寫 OnInitializedAsync 以異步方式執行長時間執行的工作。 當 isLoadingtrue時,載入訊息會顯示給使用者。 OnInitializedAsync 傳回的 Task 完成之後,元件會以更新的狀態重新呈現,並顯示 「Finished!」 訊息。

Slow.razor

@page "/slow"

<h2>Slow Component</h2>

@if (isLoading)
{
    <div><em>Loading...</em></div>
}
else
{
    <div>Finished!</div>
}

@code {
    private bool isLoading = true;

    protected override async Task OnInitializedAsync()
    {
        await LoadDataAsync();
        isLoading = false;
    }

    private Task LoadDataAsync()
    {
        return Task.Delay(10000);
    }
}

上述元件會使用 isLoading 變數來顯示載入訊息。 類似的方法用於將數據載入集合的元件,並檢查集合是否 null 來呈現載入訊息。 下列範例會檢查 movies 集合中的 null,以決定顯示載入訊息或顯示電影集合:

@if (movies == null)
{
    <p><em>Loading...</em></p>
}
else
{
    @* display movies *@
}

@code {
    private Movies[]? movies;

    protected override async Task OnInitializedAsync()
    {
        movies = await GetMovies();
    }
}

預先呈現會等候 靜止,這表示在渲染樹狀結構中的所有元件都完成渲染之前,該元件不會渲染。 這表示載入訊息不會在子元件的 OnInitializedAsync 方法在預先呈現期間執行長時間執行的工作時顯示。 若要示範此行為,請將上述 Slow 元件放入測試應用程式的 Home 元件:

@page "/"

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SlowComponent />

注意

雖然本節中的範例討論 OnInitializedAsync 生命週期方法,但在預先呈現期間執行的其他生命週期方法可能會延遲元件的最終轉譯。 在預先呈現期間,OnAfterRender{Async} 不會執行,並且不會因靜止狀態而受到延遲影響。

在預先呈現期間,Home 元件要等到 Slow 元件完成轉譯後才會呈現,而轉譯 Slow 元件需要十秒鐘。 UI 在這個十秒期間是空白的,而且沒有載入訊息。 預渲染之後,Home 元件會渲染,並顯示 Slow 元件的載入訊息。 超過十秒之後,Slow 元件最後會顯示已完成的訊息。

如上述示範所示,在預先呈現期間靜止會導致用戶體驗不佳。 若要改善用戶體驗,請從實施 串流轉譯 開始,以避免在預渲染的時候等待異步工作完成。

[StreamRendering] 屬性 新增至 Slow 元件(在 .NET 8 中使用 [StreamRendering(true)]):

@attribute [StreamRendering]

Home 元件預先呈現時,Slow 元件會快速渲染並顯示載入訊息。 Home 元件不會等候 10 秒,讓 Slow 元件完成轉譯。 不過,當預先呈現結束時,顯示的完成訊息會被載入訊息取代,然後元件開始最終渲染,這又造成了十秒鐘的延遲。 這是因為 Slow 元件會渲染兩次,LoadDataAsync 也會執行兩次。 當元件存取服務與資料庫等資源時,服務呼叫和資料庫查詢的雙重執行會在應用程式的資源上建立不想要的負載。

若要解決載入訊息被重複呈現以及服務和資料庫呼叫的重複執行問題,請將預先呈現的狀態保存到 PersistentComponentState,以供元件的最終渲染,如下列 Slow 元件的更新所示:

@page "/slow"
@attribute [StreamRendering]
@implements IDisposable
@inject PersistentComponentState ApplicationState

<h2>Slow Component</h2>

@if (data is null)
{
    <div><em>Loading...</em></div>
}
else
{
    <div>@data</div>
}

@code {
    private string? data;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        persistingSubscription =
            ApplicationState.RegisterOnPersisting(PersistData);

        if (!ApplicationState.TryTakeFromJson<string>("data", out var restored))
        {
            data = await LoadDataAsync();
        }
        else
        {
            data = restored!;
        }
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson("data", data);

        return Task.CompletedTask;
    }

    private async Task<string> LoadDataAsync()
    {
        await Task.Delay(10000);
        return "Finished!";
    }

    void IDisposable.Dispose()
    {
        persistingSubscription.Dispose();
    }
}

透過結合串流渲染與持續性元件狀態:

  • 服務和資料庫只需要單一呼叫元件初始化。
  • 元件會在長時間執行的工作期間快速渲染其UI,並顯示載入訊息,以獲得最佳用戶體驗。

如需詳細資訊,請參閱以下資源:

預先呈現期間的靜止會導致用戶體驗不佳。 延遲問題可以在以 .NET 8 或更新版本為目標的應用程式中透過名為 串流渲染的功能解決,通常會結合 在預渲染期間保存元件狀態 以避免等待異步任務完成。 在 .NET 的版本早於 8.0 時,執行 長時間運行的背景任務,以在最終渲染後載入數據,可能會因系統閒置而解決渲染的長時間延遲問題。

處理錯誤

如需關於在生命週期方法執行期間處理錯誤的資訊,請參閱處理 ASP.NET Core Blazor 應用程式中的錯誤

預先轉譯之後的具狀態重新連線

在伺服器上預先轉譯時,元件一開始會以靜態方式轉譯為頁面的一部分。 一旦瀏覽器建立與伺服器的 SignalR 連線,元件就會再次轉譯並可互動。 如果元件初始化的 OnInitialized{Async} 生命週期方法存在,則會執行方法兩次

  • 當元件以靜態方式預先轉譯時。
  • 建立伺服器連線之後。

這可能會導致最終轉譯元件時,UI 中顯示的資料有明顯變更。 若要避免此行為,請在預先轉譯期間傳入識別碼以快取狀態,並在預先轉譯之後擷取狀態。

下列程式碼示範 WeatherForecastService,其可避免因為預先轉譯而變更資料顯示。 等候的 Delay (await Task.Delay(...)) 會模擬短暫的延遲,再從 GetForecastAsync 方法傳回資料。

在應用程式的 IMemoryCache 檔案中,在服務集合上新增具有 AddMemoryCacheProgram 服務:

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 Core Razor 元件與 MVC 或 Razor Pages

使用 JavaScript Interop 預先轉譯

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

注意

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

本節適用於會預先轉譯 Blazor WebAssembly 元件的伺服器端應用程式和所裝載的 Razor 應用程式。 預先呈現涵蓋在 整合 ASP.NET Core Razor 元件與 MVC 或 Razor Pages 中。

在預先轉譯期間,無法電話撥接 JavaScript(JS)。 下列範例示範如何透過與預先轉譯相容的方式,將 JS Interop 作為元件初始化邏輯的一部分。

下列 scrollElementIntoView 函式:

window.scrollElementIntoView = (element) => {
  element.scrollIntoView();
  return element.getBoundingClientRect().top;
}

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

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

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

使用 IDisposableIAsyncDisposable 的元件處置

如果元件實作 IDisposableIAsyncDisposable,則從 UI 移除元件時,架構會呼叫資源處置。 請勿依賴執行這些方法的確切時機。 例如,IAsyncDisposable可以在呼叫或完成Task中等待的非同步OnInitalizedAsync之前或之後觸發。 此外,物件處置程式碼不應該假設初始化期間建立的物件或其他生命週期方法存在。

元件不需要同時實作 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 錯誤處理指導,請參閱Blazor中 JavaScript Interop 一節。

同步 IDisposable

針對同步處置工作,請使用 IDisposable.Dispose

下列元件:

  • 使用 IDisposable@implements 指示詞實作 Razor。
  • 處置 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}),則會在呼叫 null 之前先檢查 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();
}

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

呼叫 nullDispose/ 之後,通常不需要指派 DisposeAsync 給已處置的物件。 指派 null 的罕見案例包括:

  • 如果物件類型實作不佳,且不容許對 Dispose/DisposeAsync 重複呼叫,請在處置之後指派 null,以順利略過對 Dispose/DisposeAsync 的進一步呼叫。
  • 如果長時間存留的流程繼續保存已處置物件的參考,則指派 null 可讓記憶體回收行程釋放物件,儘管長期存留的流程仍保存該物件參考。

這些是不尋常的案例。 對於正確實作且正常運作的物件,將 null 指派給已處置的物件沒有意義。 在必須將 null 指派給物件的罕見情況下,建議您記錄原因,並尋求不須指派 null 的解決方案。

StateHasChanged

注意

不支援在 StateHasChangedDispose 中呼叫 DisposeAsyncStateHasChanged 可能會在卸載轉譯器時叫用,因此不支援在該時間點要求 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 方法的主題。

Interop 期間的 JS 處置

在遺失JSDisconnectedExceptionBlazor線路時發生陷阱SignalR,JS導致 Interop 呼叫,並導致未處理的例外狀況。

如需詳細資訊,請參閱以下資源:

可取消的背景工作

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

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

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

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

在以下範例中:

  • await Task.Delay(10000, 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 指導

其他資源

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