共用方式為


ASP.NET Core Blazor 狀態管理

注意

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

警告

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

重要

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

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

本文說明在使用者使用應用程式和跨瀏覽器工作階段時,維護使用者資料 (狀態) 的常見方法。

注意

本文中的程式碼範例採用了 可空性參考型別 (NRT) 和 .NET 編譯器的 Null 狀態靜態分析,這些功能在 .NET 6 或更高版本的 ASP.NET Core 中受到了支援。 以 ASP.NET Core 5.0 或更早版本為目標時,請從本文範例的類型中移除 Null 類型指定 (?)。

維護使用者狀態

伺服器端 Blazor 是具狀態的應用程式架構。 大部分時候,應用程式會維護與伺服器的連線。 使用者的狀態會保留在線路的伺服器記憶體中。

在線路中保留的使用者狀態範例包括:

  • 元件執行個體的階層結構及其在已渲染的 UI 中最新的渲染輸出。
  • 元件執行個體中的欄位和屬性值。
  • 資料存放在相依性插入 (DI) 服務執行個體中,且範圍限定於線路。

您也可以透過 JavaScript Interop 呼叫,在瀏覽器記憶體集的 JavaScript 變數中找到使用者狀態。

如果使用者遇到暫時的網路連線遺失,Blazor 會嘗試以原始狀態將使用者重新連線到其原始線路。 不過,將使用者重新連線到其在伺服器記憶體中的原始線路並不一定可行:

  • 伺服器無法永遠保留中斷連線的線路。 伺服器在逾時或伺服器承受記憶體不足的壓力時,必須釋放已斷開的連線。
  • 在多伺服器、負載平衡的部署環境中,當不再需要處理整體要求量時,個別伺服器可能會失敗或自動移除。 當使用者嘗試重新連線時,使用者的原始伺服器處理要求可能會變成無法使用。
  • 使用者可能會關閉並重新開啟其瀏覽器或重新載入頁面,這會移除瀏覽器記憶體中保留的任何狀態。 例如,透過 JavaScript Interop 呼叫設定的 JavaScript 變數值會遺失。

當使用者無法重新連線到其原始線路時,使用者會收到具有空白狀態的新線路。 這相當於關閉並重新開啟桌面應用程式。

保存跨線路狀態

一般而言,會在使用者積極創建資料的電路之間維護狀態,而不僅僅是讀取已經存在的資料。

若要跨線路保留狀態,應用程式必須將資料保存到伺服器記憶體以外的其他儲存位置。 狀態持續性並非自動。 開發應用程式以實作具狀態資料持續性時,您必須採取一些步驟。

通常只有使用者花費大量精力建立的高價值狀態才需要資料持續性。 在下列範例中,保存狀態可節省時間,或協助商務工作:

  • 多階段 Web 表單:如果使用者的狀態遺失,必須重新輸入已完成的數個階段的資料,對使用者而言相當耗時。 如果使用者離開表單並在稍後返回,則在此案例中就會失去狀態。
  • 購物車:可以維護應用程式中任何具有商業重要性並能代表潛在營收的元件。 失去狀態的使用者連同他們的購物車,在稍後返回網站時,可能會購買較少的產品或服務。

應用程式只能保存應用程式狀態。 UI 無法被儲存,例如元件實例及其渲染樹。 元件和渲染樹通常無法序列化。 若要保存 UI 狀態 (例如樹狀檢視控制項的展開節點),應用程式必須使用自訂程式碼,將 UI 狀態的行為模型化為可序列化的應用程式狀態。

保存狀態的位置

常用的保存狀態位置有:

伺服器端儲存體

針對跨越多個使用者和裝置的永久資料持續性,應用程式可以使用伺服器端儲存體。 這些選項包括:

  • Blob 儲存體
  • 鍵值儲存
  • 關聯式資料庫
  • 表格儲存體

儲存資料之後,會保留使用者的狀態,並可在任何新的線路中使用。

如需 Azure 資料儲存體選項的詳細資訊,請參閱下列各項:

URL

若為代表瀏覽狀態的暫時性資料,請將資料模型化為 URL 的一部分。 在 URL 中建立模型的使用者狀態範例包括:

  • 已檢視實體的識別碼。
  • 分頁方格中的目前頁碼。

會保留瀏覽器網址列的內容:

  • 如果使用者手動重新載入頁面。
  • 如果網頁伺服器無法使用,且使用者被迫重新載入頁面,才能連線到不同的伺服器。

如需使用 @page 指示詞定義 URL 模式的資訊,請參閱 ASP.NET Core Blazor 路由和導覽

瀏覽器儲存體

針對使用者正在主動建立的暫時性資料,常用的儲存位置是瀏覽器的 localStoragesessionStorage 集合:

  • localStorage 的範圍限定於瀏覽器實例。 如果使用者重新載入頁面或關閉並重新開啟瀏覽器,會保存狀態。 如果使用者開啟多個瀏覽器索引標籤,狀態會跨索引標籤共用。 資料會持續存在 localStorage 中,直到明確清除為止。 當最後一個「私人」分頁關閉時,載入於「私人瀏覽」或「無痕」會話的文件的 localStorage 數據會被清除。
  • sessionStorage 範圍設定為瀏覽器索引標籤。如果使用者重新載入索引標籤,則狀態會持續。 如果使用者關閉索引標籤或瀏覽器,狀態就會遺失。 如果使用者開啟多個瀏覽器索引標籤,則每個索引標籤都有自己的獨立資料版本。

一般而言,sessionStorage 使用上更安全。 sessionStorage 可避免使用者開啟多個索引標籤並遇到下列風險:

  • 標籤頁的狀態儲存錯誤。
  • 當索引標籤覆寫其他索引標籤的狀態時,造成混淆的行為。

如果應用程式必須在關閉並重新開啟瀏覽器時保存狀態,則 localStorage 為較佳的選擇。

使用瀏覽器儲存體的注意事項:

  • 類似於使用伺服器端資料庫,載入及儲存資料為非同步。
  • 由於在預先轉譯期間,要求的頁面不存在於瀏覽器中,因此預先轉譯期間不能使用本地存儲空間。
  • 對於伺服器端 Blazor 應用程式,儲存幾個 KB 的數據是合理的。 除了幾 KB 之外,您還必須考量效能影響,因為資料會透過網路載入並儲存。
  • 使用者可以檢視或竄改資料。 ASP.NET Core Data Protection 可以降低風險。 例如,ASP.NET Core Protected Browser Storage 會使用 ASP.NET Core Data Protection。

第三方 NuGet 套件提供用於 localStoragesessionStorage 的 API。 建議您考慮選擇以透明方式使用 ASP.NET Core Data Protection 的套件。 資料保護會加密儲存的資料,並降低竄改已儲存資料的潛在風險。 如果 JSON 序列化資料以純文字儲存,則使用者可以使用瀏覽器開發人員工具查看資料,也可以修改儲存的資料。 保護一般數據不是問題。 例如,讀取或修改 UI 元素的預存色彩,對使用者或組織來說並不具有重大的安全性風險。 避免允許使用者檢查或竄改敏感性資料

ASP.NET Core 受保護的瀏覽器儲存

ASP.NET Core Protected Browser Storage 利用 ASP.NET Core Data Protection 來對 localStoragesessionStorage 進行保護。

注意

受保護的瀏覽器儲存體會仰賴 ASP.NET Core Data Protection,且僅支援伺服器端 Blazor 應用程式。

警告

Microsoft.AspNetCore.ProtectedBrowserStorage 是不受支援的實驗性套件,不適合用於生產環境。

套件僅適用於 ASP.NET Core 3.1 應用程式。

組態

  1. 將套件參考新增至 Microsoft.AspNetCore.ProtectedBrowserStorage

    注意

    如需將套件新增至 .NET 應用程式的指引,請參閱在套件取用工作流程 (NuGet 文件)安裝及管理套件底下的文章。 在 NuGet.org 確認正確的套件版本。

  2. _Host.cshtml 檔案中,在結尾 </body> 標籤內新增下列指令碼:

    <script src="_content/Microsoft.AspNetCore.ProtectedBrowserStorage/protectedBrowserStorage.js"></script>
    
  3. Startup.ConfigureServices 中,呼叫 AddProtectedBrowserStorage 以將 localStoragesessionStorage 服務新增至服務集合:

    services.AddProtectedBrowserStorage();
    

儲存及載入元件中的資料

在任何需要將資料載入或儲存至瀏覽器儲存體的元件中,使用 @inject 指示詞插入下列任一項的執行個體:

  • ProtectedLocalStorage
  • ProtectedSessionStorage

選擇取決於您想要使用的瀏覽器儲存位置。 在以下範例中,已使用 sessionStorage

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

@using 指示詞可以放在應用程式的 _Imports.razor 檔案中,而不是放在元件中。 使用 _Imports.razor 檔案會使命名空間可供應用程式的較大區段或整個應用程式使用。

若要在以 Blazor 專案範本為基礎的應用程式中,使 currentCount 值於 Counter 元件中持續存在,請修改 IncrementCount 方法來使用 ProtectedSessionStore.SetAsync

private async Task IncrementCount()
{
    currentCount++;
    await ProtectedSessionStore.SetAsync("count", currentCount);
}

在較大型、更現實的應用程式中,個別欄位的儲存是不太可能發生的案例。 應用程式更可能儲存包含複雜狀態的整個模型物件。 ProtectedSessionStore 會自動序列化及還原序列化 JSON 資料,以儲存複雜的狀態物件。

在上述程式碼範例中,currentCount 資料會在使用者的瀏覽器中儲存為 sessionStorage['count']。 資料不會以純文字儲存,而是使用 ASP.NET Core Data Protection 來保護資料。 若在瀏覽器的開發人員主控台中評估 sessionStorage['count'],則可以查看加密的資料。

若要在稍後使用者返回 currentCount 元件時復原 Counter 資料,包括使用者位於新的線路上,請使用 ProtectedSessionStore.GetAsync

protected override async Task OnInitializedAsync()
{
    var result = await ProtectedSessionStore.GetAsync<int>("count");
    currentCount = result.Success ? result.Value : 0;
}
protected override async Task OnInitializedAsync()
{
    currentCount = await ProtectedSessionStore.GetAsync<int>("count");
}

如果元件的參數包含瀏覽狀態,請呼叫 ProtectedSessionStore.GetAsync 並將非 null 的結果指派給 OnParametersSetAsync,而不是 OnInitializedAsync。 僅在第一次具現化元件時,才會呼叫 OnInitializedAsync 一次。 如果使用者在相同頁面上繼續瀏覽至不同的 URL,則稍後不會再次呼叫 OnInitializedAsync。 如需詳細資訊,請參閱 ASP.NET Core Razor 元件生命週期

警告

本節中的範例只有在伺服器未啟用預先轉譯時才能運作。 啟用預先轉譯時,會產生錯誤,說明無法發出 JavaScript Interop 呼叫,因為元件已預先轉譯。

停用預先轉譯或新增其他程式碼以使用預先轉譯。 若要深入了解撰寫可與預先轉譯搭配運作的程式碼,請參閱處理預先轉譯一節。

處理載入狀態

由於瀏覽器儲存體是透過網路連線以非同步方式存取,因此在載入資料並提供給元件之前,一律會有一段時間。 為了獲得最佳結果,在載入進行時轉譯訊息,而不是顯示空白或預設資料。

其中一種方法是追蹤資料是否為 null,這表示資料仍在載入中。 在預設 Counter 元件中,計數會保留在 int 中。 透過在類型 (int) 上添加問號 (?) 來使 currentCount 可以為空值

private int? currentCount;

只有在確認資料已載入後,才會顯示計數和 Increment 按鈕,而不是無條件顯示這些元素 HasValue

@if (currentCount.HasValue)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

處理預渲染

在預渲染期間:

  • 使用者瀏覽器中不存在互動式連線。
  • 瀏覽器還沒有可以執行 JavaScript 程式碼的頁面。

localStoragesessionStorage 無法在預先轉譯期間使用。 如果元件嘗試與儲存體互動,就會產生錯誤,說明無法發出 JavaScript Interop 呼叫,因為元件已預先轉譯。

解決錯誤的其中一種方法是停用預先轉譯。 如果應用程式大量使用瀏覽器型儲存體,這通常是最佳選擇。 預先轉譯會增加複雜度,且不會使應用程式受益,因為應用程式在 localStoragesessionStorage 可用之前,無法預先轉譯任何有用的內容。

若要停用預先轉譯,請指出轉譯模式,並在應用程式元件階層的最高層元件 (不是根元件) 中將 prerender 參數設定為 false

注意

不支援讓根元件成為互動式,例如 App 元件。 因此,App 元件無法直接停用預先轉譯。

針對以 Blazor Web App 專案範本為基礎的應用程式,通常會停用預先轉譯,會在 App 元件中使用 Routes 元件 (Components/App.razor):

<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />

此外,停用該元件的 HeadOutlet 預先轉譯:

<HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />

如需詳細資訊,請參閱 ASP.NET Core Blazor 轉譯模式

若要停用預先轉譯,請開啟 _Host.cshtml 檔案,並將 Component Tag Helperrender-mode 屬性變更為 Server

<component type="typeof(App)" render-mode="Server" />

當預先轉譯被停用時,<head>內容的預先轉譯會被停用

預先轉譯對於不使用 localStoragesessionStorage 的其他頁面可能很有用。 若要保留預先轉譯,請延遲載入作業,直到瀏覽器連線到線路為止。 以下是儲存計數器值的範例:

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore

@if (isConnected)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

@code {
    private int currentCount;
    private bool isConnected;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isConnected = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        var result = await ProtectedLocalStore.GetAsync<int>("count");
        currentCount = result.Success ? result.Value : 0;
    }

    private async Task IncrementCount()
    {
        currentCount++;
        await ProtectedLocalStore.SetAsync("count", currentCount);
    }
}
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedLocalStorage ProtectedLocalStore

@if (isConnected)
{
    <p>Current count: <strong>@currentCount</strong></p>
    <button @onclick="IncrementCount">Increment</button>
}
else
{
    <p>Loading...</p>
}

@code {
    private int currentCount = 0;
    private bool isConnected = false;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isConnected = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        currentCount = await ProtectedLocalStore.GetAsync<int>("count");
    }

    private async Task IncrementCount()
    {
        currentCount++;
        await ProtectedLocalStore.SetAsync("count", currentCount);
    }
}

將狀態提取到共用提供者

如果許多元件仰賴瀏覽器型儲存體,則實作狀態提供者程式碼在大多數情況下會建立程式碼重複。 避免程式碼重複的其中一個選項是建立封裝狀態提供者邏輯的狀態提供者父元件。 子元件可以處理持續化的資料,而不必考慮狀態保存機制。

在下列 CounterStateProvider 元件範例中,計數器資料會儲存至 sessionStorage,而且在狀態載入完成之前,暫不渲染其子內容以處理載入階段。

CounterStateProvider 元件處理預先呈現的方式是直到組件在 OnAfterRenderAsync 生命週期方法渲染後才載入狀態,而該方法在預先呈現期間不會執行。

本節中的方法無法觸發相同頁面上多個訂閱元件的重新呈現。 如果某個訂閱的元件變更狀態,它會重新渲染並且可以顯示更新後的狀態,但在相同頁面中顯示該狀態的其他元件則會顯示過時的資料,直到它自己下一次重新渲染為止。 因此,本節所述的方法最適合在頁面上的單一元件中使用狀態。

CounterStateProvider.razor

@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

@if (isLoaded)
{
    <CascadingValue Value="this">
        @ChildContent
    </CascadingValue>
}
else
{
    <p>Loading...</p>
}

@code {
    private bool isLoaded;

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    public int CurrentCount { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isLoaded = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        var result = await ProtectedSessionStore.GetAsync<int>("count");
        CurrentCount = result.Success ? result.Value : 0;
        isLoaded = true;
    }

    public async Task IncrementCount()
    {
        CurrentCount++;
        await ProtectedSessionStore.SetAsync("count", CurrentCount);
    }
}
@using Microsoft.AspNetCore.ProtectedBrowserStorage
@inject ProtectedSessionStorage ProtectedSessionStore

@if (isLoaded)
{
    <CascadingValue Value="this">
        @ChildContent
    </CascadingValue>
}
else
{
    <p>Loading...</p>
}

@code {
    private bool isLoaded;

    [Parameter]
    public RenderFragment ChildContent { get; set; }

    public int CurrentCount { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            isLoaded = true;
            await LoadStateAsync();
            StateHasChanged();
        }
    }

    private async Task LoadStateAsync()
    {
        CurrentCount = await ProtectedSessionStore.GetAsync<int>("count");
        isLoaded = true;
    }

    public async Task IncrementCount()
    {
        CurrentCount++;
        await ProtectedSessionStore.SetAsync("count", CurrentCount);
    }
}

注意

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

為了讓應用程式中的所有元件都能存取狀態,請使用全域互動式伺服器端渲染(互動式 SSR),將 CounterStateProvider 元件包裹在 Routes 元件中的 Router<Router>...</Router>)周圍。

App 元件 (Components/App.razor) 中:

<Routes @rendermode="InteractiveServer" />

Routes 元件 (Components/Routes.razor) 中:

若要使用 CounterStateProvider 元件,請將其實例包裹在需要存取計數器狀態的其他任何元件周圍。 若要讓應用程式中的所有元件都能存取狀態,請在 CounterStateProvider 元件 (Router) 中,以 App 包裝 App.razor 元件:

<CounterStateProvider>
    <Router ...>
        ...
    </Router>
</CounterStateProvider>

注意

隨著 ASP.NET Core 5.0.1 的發行以及在任何其他 5.x 版中,Router 元件會包含設定為 PreferExactMatches@true 參數。 如需詳細資訊,請參閱從 ASP.NET Core 3.1 移轉至 5.0

被包裝的元件會接收並可修改持久化的計數器狀態。 下列 Counter 元件會實作模式:

@page "/counter"

<p>Current count: <strong>@CounterStateProvider?.CurrentCount</strong></p>
<button @onclick="IncrementCount">Increment</button>

@code {
    [CascadingParameter]
    private CounterStateProvider? CounterStateProvider { get; set; }

    private async Task IncrementCount()
    {
        if (CounterStateProvider is not null)
        {
            await CounterStateProvider.IncrementCount();
        }
    }
}

不需要上述元件即可與 ProtectedBrowserStorage 互動,且上述元件也不會處理「載入」階段。

一般而言,建議使用狀態提供者父元件模式:

  • 若要在多個元件之間取用狀態。
  • 如果只有一個最上層狀態物件需要持久化。

若要保存許多不同的狀態物件,並在不同位置取用不同的物件子集,最好避免全域保存狀態。

在 Blazor WebAssembly 應用程式中建立的使用者狀態會保留在瀏覽器的記憶體中。

瀏覽器記憶體中保留的使用者狀態範例包括:

  • 元件實例的層次結構及其在已渲染的 UI 中最新的渲染輸出。
  • 元件執行個體中的欄位和屬性值。
  • 保存在依賴注入 (DI) 服務實例中的資料。
  • 透過 JavaScript Interop 呼叫設定的值。

當使用者關閉並重新開啟其瀏覽器或重新載入頁面時,保存在瀏覽器記憶體中的使用者狀態會遺失。

備註

受保護的瀏覽器儲存體 (Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage 命名空間) 會仰賴 ASP.NET Core Data Protection,且僅支援伺服器端 Blazor 應用程式。

跨瀏覽器工作階段保存狀態

一般而言,會在使用者積極建立資料的瀏覽器工作階段之間維護狀態,而不僅僅是讀取已經存在的資料。

若要跨瀏覽器工作階段保留狀態,應用程式必須將資料保存到瀏覽器記憶體以外的其他儲存位置。 狀態持續性並非自動。 開發應用程式以實作具狀態資料持續性時,您必須採取一些步驟。

通常只有使用者花費大量精力建立的高價值狀態才需要資料持續性。 在下列範例中,保存狀態可節省時間,或協助商務工作:

  • 多步驟網頁表單:如果使用者的狀態遺失,重新輸入多步驟表單中數個已完成步驟的資料會非常耗時。 如果使用者離開表單並在稍後返回,則在此案例中就會失去狀態。
  • 購物車:任何能夠代表潛在營收的重要商業元件都可以被維護。 失去狀態的使用者連同他們的購物車,在稍後返回網站時,可能會購買較少的產品或服務。

應用程式只能保存應用程式狀態。 無法儲存使用者介面,例如元件執行個體及其渲染樹狀結構。 組件和渲染樹通常無法序列化。 若要保存 UI 狀態 (例如樹狀檢視控制項的展開節點),應用程式必須使用自訂程式碼,將 UI 狀態的行為模型化為可序列化的應用程式狀態。

保存狀態的位置

有一些常見位置用於保存狀態:

伺服器端儲存體

針對跨多個使用者和裝置的永久資料持續性,應用程式可以使用透過 Web API 存取的獨立伺服器端儲存體。 這些選項包括:

  • Blob 儲存體
  • 鍵值儲存
  • 關聯式資料庫
  • 資料表儲存

儲存資料之後,會保留使用者的狀態,並可在任何新的瀏覽器工作階段中使用。

由於 Blazor WebAssembly 應用程式完全在使用者的瀏覽器中執行,因此需要額外的措施來存取安全的其他系統,例如儲存服務和資料庫。 Blazor WebAssembly 應用程式的保護方式與單頁應用程式 (SPA) 相同。 一般而言,應用程式會透過 OAuth/OpenID Connect (OIDC) 驗證使用者,然後透過 Web API 呼叫伺服器端應用程式與儲存體服務和資料庫互動。 伺服器端應用程式會調解 Blazor WebAssembly 應用程式與儲存體服務或資料庫之間的資料傳輸。 Blazor WebAssembly 應用程式會維護與伺服器端應用程式的暫時連線,而伺服器端應用程式則持續連線至儲存體。

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

如需 Azure 資料儲存體選項的詳細資訊,請參閱下列各項:

URL

若為代表瀏覽狀態的暫時性資料,請將資料模型化為 URL 的一部分。 在 URL 中建立模型的使用者狀態範例包括:

  • 已檢視實體的識別碼。
  • 分頁方格中的目前頁碼。

如果使用者手動重新載入頁面,則會保留瀏覽器網址列的內容。

如需使用 @page 指示詞定義 URL 模式的資訊,請參閱 ASP.NET Core Blazor 路由和導覽

瀏覽器儲存體

針對使用者正在主動建立的暫時性資料,常用的儲存位置是瀏覽器的 localStoragesessionStorage 集合:

  • localStorage 的範圍限定於瀏覽器實例。 如果使用者重新載入頁面或關閉並重新開啟瀏覽器,會保存狀態。 如果使用者開啟多個瀏覽器索引標籤,狀態會跨索引標籤共用。 資料會持續存在 localStorage 中,直到明確清除為止。 當最後一個「私人」分頁關閉時,載入於「私人瀏覽」或「無痕」會話的文件的 localStorage 數據會被清除。
  • sessionStorage 範圍設定為瀏覽器索引標籤。如果使用者重新載入索引標籤,則狀態會持續。 如果使用者關閉索引標籤或瀏覽器,狀態就會遺失。 如果使用者開啟多個瀏覽器索引標籤,則每個索引標籤都有自己的獨立資料版本。

注意

localStoragesessionStorage 可用於 Blazor WebAssembly 應用程式中,但只能藉由撰寫自訂程式碼或使用協力廠商套件。

一般而言,sessionStorage 使用上更安全。 sessionStorage 可避免使用者開啟多個索引標籤並遇到下列風險:

  • 跨頁籤的狀態儲存錯誤。
  • 當索引標籤覆寫其他索引標籤的狀態時,造成混淆的行為。

如果應用程式必須在關閉並重新開啟瀏覽器時保存狀態,則 localStorage 為較佳的選擇。

警告

使用者可以檢視或竄改儲存在 localStoragesessionStorage 中的資料。

記憶體內部狀態容器服務

巢狀元件通常會使用鏈結繫結來繫結資料,如 ASP.NET Core Blazor 資料繫結中所述。 巢狀和未巢狀的元件可以使用已註冊的記憶體內部狀態容器來共用資料的存取權。 自訂狀態容器類別可以使用可指派的 Action,在應用程式的不同部分通知元件狀態變更。 在以下範例中:

  • 一組元件會使用狀態容器來追蹤屬性。
  • 下列範例中的一個元件是嵌套於另一個元件中,但此方法無需嵌套即可運作。

重要

本節中的範例示範如何建立記憶體內部狀態容器服務、註冊服務,以及在元件中使用服務。 此範例不會在沒有進一步開發的情況下保存資料。 為了持續儲存資料,狀態容器必須採用在清除瀏覽器記憶體時存留的基礎儲存機制。 這可以透過 localStorage/sessionStorage 或一些其他技術來完成。

StateContainer.cs

public class StateContainer
{
    private string? savedString;

    public string Property
    {
        get => savedString ?? string.Empty;
        set
        {
            savedString = value;
            NotifyStateChanged();
        }
    }

    public event Action? OnChange;

    private void NotifyStateChanged() => OnChange?.Invoke();
}

用戶端應用程式 (Program 檔案):

builder.Services.AddSingleton<StateContainer>();

伺服器端應用程式 (Program 檔案、.NET 6 或更新版本中的 ASP.NET Core):

builder.Services.AddScoped<StateContainer>();

伺服器端應用程式 (Startup.ConfigureServicesStartup.cs、ASP.NET Core 早於 6.0 版本):

services.AddScoped<StateContainer>();

Shared/Nested.razor

@implements IDisposable
@inject StateContainer StateContainer

<h2>Nested component</h2>

<p>Nested component Property: <b>@StateContainer.Property</b></p>

<p>
    <button @onclick="ChangePropertyValue">
        Change the Property from the Nested component
    </button>
</p>

@code {
    protected override void OnInitialized()
    {
        StateContainer.OnChange += StateHasChanged;
    }

    private void ChangePropertyValue()
    {
        StateContainer.Property = 
            $"New value set in the Nested component: {DateTime.Now}";
    }

    public void Dispose()
    {
        StateContainer.OnChange -= StateHasChanged;
    }
}

StateContainerExample.razor

@page "/state-container-example"
@implements IDisposable
@inject StateContainer StateContainer

<h1>State Container Example component</h1>

<p>State Container component Property: <b>@StateContainer.Property</b></p>

<p>
    <button @onclick="ChangePropertyValue">
        Change the Property from the State Container Example component
    </button>
</p>

<Nested />

@code {
    protected override void OnInitialized()
    {
        StateContainer.OnChange += StateHasChanged;
    }

    private void ChangePropertyValue()
    {
        StateContainer.Property = "New value set in the State " +
            $"Container Example component: {DateTime.Now}";
    }

    public void Dispose()
    {
        StateContainer.OnChange -= StateHasChanged;
    }
}

上述元件會實作 IDisposable,在 Dispose 方法中會取消訂閱 OnChange 委派,而這些方法是在元件被處置時由架構呼叫的。 如需詳細資訊,請參閱 ASP.NET Core Razor 元件處置

層疊值與參數

使用 級聯值和參數,藉由將數據從上階 Razor 元件流向子代元件來管理狀態。

具有 CascadingValueSource<TValue> 的根層級階級化值允許 Razor 元件訂閱者接收已變更的階級化值通知。 如需詳細資訊和運作範例,請參閱 ASP.NET Core Blazor 級聯值和參數中的 NotifyingDalek 範例。

其他方法

實作自訂狀態儲存體時,有用的方法是採用串聯的值及參數

  • 若要在多個元件之間取用狀態。
  • 如果只有一個最上層狀態物件需要保存。

疑難排解

當您使用自定義狀態管理服務時,希望允許來自 Blazor同步處理內容外部的狀態修改(例如,從定時器或背景服務中),所有使用的元件都必須將 StateHasChanged 呼叫封裝在 ComponentBase.InvokeAsync中。 這可確保在渲染器的同步上下文中處理變更通知。

當狀態管理服務在Blazor的同步處理上下文中未呼叫StateHasChanged時,會發生以下錯誤:

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

如需如何解決此錯誤的詳細資訊和範例,請參閱 ASP.NET Core Razor 元件轉譯

其他資源