ASP.NET 核心 Razor 元件轉譯

注意

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

重要

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

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

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

ComponentBase 的轉譯慣例

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

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

如果下列任一項成立,則從 ComponentBase 繼承的元件會因為參數更新而跳過重新轉譯:

控制轉譯流程

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

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

串流轉譯

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

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

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

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

下列範例是基於從 Blazor Web App 專案範本建立的應用程式中的 Weather 元件。 對 Task.Delay 的呼叫模擬了非同步方式擷取天氣資料。 元件最初會轉譯預留位置內容 (「Loading...」),而不等待非同步延遲完成。 當非同步延遲完成並產生天氣資料內容時,該內容會串流至回應並修補至氣象預報資料表中。

Weather.razor

@page "/weather"
@attribute [StreamRendering(true)]

...

@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 以管理 UI 重新整理。 如果實作傳回 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()
    {
        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++;
    }
}
@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 Core Blazor 效能最佳做法

呼叫 StateHasChanged 的時機

呼叫 StateHasChanged 可讓您隨時觸發轉譯。 但是,請注意不要不必要地呼叫 StateHasChanged,這是造成不必要轉譯成本的常見錯誤。

在以下情況下,程式碼不需要呼叫 StateHasChanged

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

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

非同步處理常式牽涉到多個非同步階段

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

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

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

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"

<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,其中當架構呼叫 Dispose 方法時,Timer 會被處置。 如需詳細資訊,請參閱 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

<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,例如在 Task 上呼叫 ContinueWith 或使用 ConfigureAwait(false) 等候 Task。 如需詳細資訊,請參閱接收來自 Blazor 轉譯和事件處理系統外部的呼叫一節。

Blazor Web Apps 的 WebAssembly 載入進度指標

載入進度指標不存在於從 Blazor Web 應用程式專案範本建立的應用程式中。 已針對未來的 .NET 版本規劃新的載入進度指標功能。 同時,應用程式可以採用自訂程式碼來建立載入進度指標。 如需詳細資訊,請參閱 ASP.NET Core Blazor 啟動