共用方式為


ASP.NET 核心 Razor 元件轉譯

注意

這不是這篇文章的最新版本。 關於目前版本,請參閱 本文的 .NET 10 版本

警告

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

本文說明 Razor ASP.NET Core Blazor 應用程式中的元件轉譯,包括呼叫 StateHasChanged 以手動觸發元件轉譯的時機。

ComponentBase 的渲染慣例

當元件由父元件首次新增至元件階層時,這些元件必須渲染。 這是元件必須渲染的唯一時間。 元件可能會 根據自己的邏輯和慣例在其他時間渲染。

Razor 元件繼承自 ComponentBase 基底類別,其中包含在下列時間觸發重新轉譯的邏輯:

ComponentBase 繼承的元件在參數更新時會跳過重新轉譯,如果下列任一條件成立:

控制轉譯流程

在大部分情況下,ComponentBase 慣例會在事件發生後產生正確的元件子集重新轉譯。 開發人員通常不需要提供手動邏輯,以告訴架構應重新轉譯哪些元件,以及何時重新轉譯這些元件。 架構慣例的整體效果是,元件在接收到事件後會重新渲染自身,並以遞迴方式觸發參數值可能已改變的子系元件進行重新渲染。

如需架構慣例效能影響以及如何優化應用程式元件階層以進行轉譯的詳細資訊,請參閱 ASP.NET 核心 Blazor 轉譯效能最佳做法

串流渲染

使用串流轉譯搭配靜態伺服器端轉譯 (靜態 SSR) 或預先轉譯來串流回應資料流上的內容更新,並改善執行長時間執行非同步工作的元件使用者體驗,以便完整轉譯。

例如,考慮一個在頁面載入時進行長時間資料庫查詢或網絡 API 請求以生成資料的元件。 通常,在轉譯伺服器端元件的過程中執行的非同步工作必須在傳送轉譯的回應之前完成,這可能會延遲載入頁面。 轉譯頁面的任何重大延遲,都會損害使用者體驗。 為了改善使用者體驗,串流轉譯最初會在執行非同步作業的同時,以預留位置內容快速轉譯整個頁面。 作業完成之後,更新的內容會在相同的回應連線上傳送至用戶端,並修補到 DOM。

串流渲染要求伺服器避免緩存輸出。 當產生資料時,回應資料必須流向用戶端。 對於強制執行緩衝處理的主機,串流轉譯會正常降級,且頁面載入時不需要進行串流轉譯。

若要在使用靜態伺服器端轉譯 (static SSR) 或預先呈現時串流內容更新,請將 .NET 9 或更新版本中的 [StreamRendering] 屬性 套用至元件(在 .NET 8 中使用 [StreamRendering(true)])。 串流轉譯必須明確啟用,因為串流更新可能會導致頁面上的內容轉移。 如果父元件使用該功能,沒有該屬性的元件會自動採用串流轉譯。 將 false 傳遞至子元件中的屬性,以在該點及元件子樹的下方停用該功能。 當套用至 Razor 類別庫所提供的元件時,屬性會正常運作。

如果 增強式流覽 為使用中,串流轉譯會轉譯 「找不到」回應 ,而不會重載頁面。 當強化導航遭到封鎖時,架構會通過頁面重新整理將畫面導向至找不到內容的頁面。

串流渲染只能渲染具有路由的元件,例如 NotFoundPage 指派 (NotFoundPage="...") 或 狀態代碼頁重新執行中間件頁面指派 (UseStatusCodePagesWithReExecute)。 找不到的轉譯片段(<NotFound>...</NotFound>)和DefaultNotFound 404 內容(“Not found”純文字)沒有路由,因此在串流轉譯期間無法使用。

串流 NavigationManager.NotFound 內容渲染使用(依序):

  • NotFoundPage傳遞至Router元件,如果存在。
  • 如果已設定,狀態代碼頁會重新執行中間件頁面。
  • 如果上述兩種方法都未採用,則不採取任何動作。

非串流 NavigationManager.NotFound 內容呈現依序使用:

  • NotFoundPage傳遞至Router元件,如果存在。
  • 找不到轉譯片段內容,即使存在。 不建議在 .NET 10 或更新版本中使用。
  • DefaultNotFound 404 內容 ("Not found" 純文字)。

狀態代碼頁重新執行中間件 優先處理 UseStatusCodePagesWithReExecute 瀏覽器中與地址路由相關的問題,例如在瀏覽器的地址欄中輸入錯誤的URL,或選擇在應用程式中沒有正確端點的連結。

這個範例是以 Weather 建立的應用程式中的 Blazor Web App 元件為依據。 對 Task.Delay 的呼叫模擬了非同步方式擷取天氣資料。 元件最初會呈現預留位置內容(「Loading...」),而不需要等待非同步延遲完成。 當非同步延遲完成並產生天氣資料內容時,該內容會串流至回應並修補至氣象預報資料表中。

Weather.razor

@page "/weather"
@attribute [StreamRendering]

...

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        ...
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    ...

    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(500);

        ...

        forecasts = ...
    }
}

暫停 UI 重新整理 (ShouldRender)

每次元件轉譯時都會呼叫 ShouldRender。 覆寫 ShouldRender 以管理使用者介面的重新整理。 如果實作傳回 true,則會重新整理 UI。

即使覆寫 ShouldRender,該元件仍會始終先行渲染。

ControlRender.razor

@page "/control-render"

<PageTitle>Control Render</PageTitle>

<h1>Control Render Example</h1>

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender() => shouldRender;

    private void IncrementCount() => currentCount++;
}
@page "/control-render"

<PageTitle>Control Render</PageTitle>

<h1>Control Render Example</h1>

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender() => shouldRender;

    private void IncrementCount() => currentCount++;
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}

如需有關 ShouldRender之效能最佳做法的詳細資訊,請參閱 ASP.NET 核心 Blazor 轉譯效能最佳做法

StateHasChanged

呼叫 StateHasChanged 會在應用程式的主要執行緒空閒時,把重新渲染加入佇列。

元件會加入佇列以進行渲染,但如果已經有待處理的重新渲染,則不會重複加入佇列。 如果元件在迴圈中連續呼叫 StateHasChanged 五次,元件只會轉譯一次。 此行為是在 ComponentBase 中編碼,其會先檢查是否已將重新渲染排入佇列,然後才會再加入另一個佇列。

元件可在同一個週期中轉譯多次,這種情況通常發生在元件具有會彼此互動的子系時:

  • 父元件會渲染數個子元件。
  • 子元件渲染並觸發父組件的更新。
  • 父元件會根據新的狀態重新渲染。

此設計允許在必要時呼叫 StateHasChanged,而不會引入不必要的轉譯風險。 您始終可以在各個元件中掌控此行為,方法是直接實作 IComponent 並手動控制元件的渲染過程。

請考慮使用下列 IncrementCount 方法,以遞增計數、呼叫 StateHasChanged,以及再次遞增計數:

private void IncrementCount()
{
    currentCount++;
    StateHasChanged();
    currentCount++;
}

在偵錯工具中逐步執行程式碼時,您可能會認為,呼叫 currentCount++ 後,StateHasChanged 的第一次執行會立即在 UI 中更新計數。 不過,UI 不會在當時顯示更新的計數,因為執行此方法會進行同步處理。 在事件處理常式完成之前,轉譯器無法轉譯元件。 UI 顯示在單次渲染中這兩次currentCount++執行的增加情況

如果您在等候兩行 currentCount++ 之間的內容,等候的呼叫會讓轉譯器有機會轉譯。 這導致某些開發人員在其元件中延遲一毫秒後才呼叫Delay,以允許進行渲染,但我們不建議您任意放慢應用程式以將渲染加入佇列。

最好的方法是等候 Task.Yield,這會強制元件非同步處理程式碼,並在目前批次期間渲染。在生成的工作執行接續操作後,會以單獨的批次進行第二次渲染。

請考慮使用下列已修訂的 IncrementCount 方法,這會更新 UI 兩次,因為當透過呼叫 StateHasChanged 來產生工作時,會執行 Task.Yield 來將轉譯加入佇列:

private async Task IncrementCount()
{
    currentCount++;
    StateHasChanged();
    await Task.Yield();
    currentCount++;
}

請注意不要不必要地呼叫 StateHasChanged,這是造成不必要轉譯成本的常見錯誤。 在以下情況下,程式碼不需要呼叫 StateHasChanged

  • 以同步或非同步方式例行處理事件,因為 ComponentBase 會觸發大多數例行事件處理器的渲染。
  • 無論是同步或非同步實作一般生命週期邏輯,例如 OnInitializedOnParametersSetAsync,因為 ComponentBase 會觸發一般生命週期事件的轉譯。

不過,在本文下列各節所述的案例中,呼叫 StateHasChanged 可能很合理:

非同步處理器牽涉多個階段的非同步處理

由於 .NET 中定義工作的方式,Task 的接收者只能觀察其最終完成情況,而不能觀察中繼非同步狀態。 因此,ComponentBase 只能在 Task 第一次傳回且 Task 最後完成時才能觸發重新轉譯。 架構無法知道在其他中繼點重新轉譯元件,例如當 IAsyncEnumerable<T>在一系列中繼 Task 中傳回資料時。 如果想要在中繼點重新轉譯,請在這些點上呼叫 StateHasChanged

請考慮下列 CounterState1 元件,每次執行 IncrementCount 方法時,它都會更新計數四次:

  • 自動渲染會發生在 currentCount 的第一次和最後一次遞增之後。
  • 當架構不會在 StateHasChanged 遞增的中繼處理點自動觸發重新轉譯時,會透過呼叫 currentCount 來觸發手動轉譯。

CounterState1.razor

@page "/counter-state-1"

<PageTitle>Counter State 1</PageTitle>

<h1>Counter State Example 1</h1>

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<PageTitle>Counter State 1</PageTitle>

<h1>Counter State Example 1</h1>

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}
@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}

接收來自 Blazor 轉譯和事件處理系統外部的呼叫

ComponentBase 僅知道自己的生命週期方法和 Blazor 觸發的事件。 ComponentBase 不知道程式碼中可能發生的其他事件。 例如,Blazor 無法識別自訂資料存放區引發的任何 C# 事件。 若要讓這類事件觸發重新轉譯以在 UI 中顯示更新的值,請呼叫 StateHasChanged

請考慮下列 CounterState2 元件,該元件使用 System.Timers.Timer 定期更新計數,並呼叫 StateHasChanged 來更新 UI:

  • OnTimerCallback 在任何 Blazor 受控轉譯流程或事件通知之外執行。 因此,OnTimerCallback 必須呼叫 StateHasChanged,因為 Blazor 不知道回呼中 currentCount 的變更。
  • 該元件會實作 IDisposable,當架構呼叫 Timer 方法時,Dispose 會被釋放。 如需詳細資訊,請參閱 ASP.NET Core Razor 元件處置

由於回呼是在 Blazor 同步處理內容外部叫用的,因此元件必須將 OnTimerCallback 的邏輯包裝在 ComponentBase.InvokeAsync 中,才能將其移至轉譯器的同步處理內容中。 這相當於在其他 UI 框架中將操作調度到 UI 執行緒。 StateHasChanged 只能從轉譯器的同步處理內容中呼叫,否則會擲回例外狀況:

System.InvalidOperationException:「目前的執行緒與 Dispatcher 沒有關聯。 在觸發呈現或元件狀態時,使用 InvokeAsync() 將執行流程切換至 Dispatcher。

CounterState2.razor

@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<PageTitle>Counter State 2</PageTitle>

<h1>Counter State Example 2</h1>

<p>
    This counter demonstrates <code>Timer</code> disposal.
</p>

<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();
}
@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<PageTitle>Counter State 2</PageTitle>

<h1>Counter State Example 2</h1>

<p>
    This counter demonstrates <code>Timer</code> disposal.
</p>

<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();
}
@page "/counter-state-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 = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-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 = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-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 = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}
@page "/counter-state-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 = 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();
}

要在由特定事件重新渲染的子樹之外渲染元件

使用者介面可能涉及:

  1. 將事件分派至一個元件。
  2. 變更一些狀態。
  3. 重繪一個完全不同的元件,且該元件不是接收事件的元件的子系。

處理這種情況的其中一種方法是提供狀態管理類別,通常作為相依性注入 (DI) 服務,注入到多個元件中。 當某個元件呼叫狀態管理員上的方法時,狀態管理員會引發 C# 事件,然後由獨立元件接收該事件。

如需管理狀態的方法,請參閱下列資源:

針對狀態管理員方法,C# 事件位於 Blazor 轉譯管線之外。 在您希望重新轉譯的其他元件上,呼叫 StateHasChanged 以回應狀態管理員的事件。

狀態管理員方法與此前段落中System.Timers.Timer的案例類似。 由於執行呼叫堆疊通常會保留在轉譯器的同步處理內容上,因此通常不需要呼叫 InvokeAsync。 只有在邏輯逸出同步化上下文時,才需要呼叫 InvokeAsync,例如在 ContinueWith 上呼叫 Task 或使用 Task 等候 ConfigureAwait(false)。 如需詳細資訊,請參閱接收來自 Blazor 轉譯和事件處理系統外部的呼叫一節。

WebAssembly 載入 Blazor Web App的進度指標

應用程式可以採用自訂程式碼來建立載入進度指標。 如需詳細資訊,請參閱 ASP.NET Core Blazor 啟動