Blazor 狀態管理

Blazor 狀態管理

想像一下,您正在填寫世界上最長的表格。您已經花了30分鐘時間輸入詳細資訊,從地址到您的生日,再到最近訪問過七個國家/地區的清單。您點擊「提交」按鈕,將立即獲得「連線已中斷」訊息。

最後更新 2022/4/18 下午7:53
寒冰屋
預計閱讀 11 分鐘
分類
Blazor
標籤
.NET Blazor 狀態管理

想像一下,您正在填寫世界上最長的表格。您已經花了 30 分鐘時間輸入詳細資訊,從地址到您的生日,再到最近訪問過七個國家/地區的清單。您點擊「提交」按鈕,然後立即看到「連線已遺失」訊息。別擔心,對吧?只要點擊「上一頁」按鈕,… 喔,不!表格是空的。這感覺很粗暴,而且您保證再也不造訪該網站了。

這不是您希望網站訪客獲得的體驗。因此,了解如何在 Blazor 應用程式中管理狀態非常重要。在管理狀態的同時盡量減少管理狀態所需編寫的程式碼量?「是的,拜託!」

觀看相關影片:Blazor Apps 中的狀態管理

1 Blazor 狀態的定義

首先,讓我們弄清楚 Blazor 應用中的「狀態」是什麼意思。為了獲得最佳的使用者體驗,當終端使用者的連線暫時中斷、重新整理或導航回到頁面時,為終端使用者提供一致的體驗非常重要。經驗的組成部分包括:

  • 表示使用者介面(UI)的 HTML 文件物件模型(DOM)
  • 表示頁面上正在輸入和/或輸出的資料的欄位和屬性
  • 作為頁面程式碼一部分執行的註冊服務的狀態

在沒有任何特殊程式碼的情況下,根據 Blazor 託管模型,狀態保存在兩個位置。對於 Blazor WebAssembly(用戶端)應用程式,狀態保持在瀏覽器記憶體中,直到使用者重新整理或導航離開頁面為止。在 Blazor Server 應用程式中,狀態保存在分配給每個稱為電路的用戶端會話的特殊「儲存桶」中。這些電路在斷開連線後超時時可能會遺失狀態,甚至在伺服器處於記憶體壓力下的活動連線過程中也可能消失。

2. 參考應用程式

為了說明狀態的細微差別,我從 Blazor Health App 開始:

從 Angular 到 Blazor:The Health App

在 Blazor 中建構範例應用程式,Blazor 是基於 .NET 的框架,用於建構可在瀏覽器中執行的 Web 應用程式,並利用 C# 和 Razor 範本產生跨平台的、相容 HTML5 的 WebAssembly 程式碼。

我將其擴展為包括兩個頁面,以說明導航的一些細微差別。在相關的 GitHub 儲存庫中:

JeremyLikness/BlazorState

有幾個範例專案。問題在 Blazor WebAssembly 和 Blazor Server 專案中的體現是不同的。

3. Blazor WebAssembly 中的狀態

在 Blazor WebAssembly(用戶端專案)中,狀態保存在記憶體中。這意味著重新整理或強制導航將破壞狀態。要查看實際效果:

  1. 設定 BlazorState.Wasm 為啟動專案並執行它。
  2. 更新表單資訊。
  3. 導航到「結果」並驗證是否存在相同的結果。
  4. 導航回「首頁」並強制重新整理(通常為 CTRL+F5)。請注意該表單還原為預設值。
  5. 更新表單資訊,然後透過在瀏覽器的 URL 欄中新增 /results 來手動導航,然後按 ENTER。請注意,它也使用預設值。

不好的經歷!與 Blazor Server 相比,它略有不同。

4. Blazor 伺服器中的狀態

將啟動專案更改為 BlazorState.Server 並執行該專案。請嘗試執行與用戶端版本相同的步驟,並注意保持了狀態,因為該狀態保存在伺服器記憶體中。開啟應用程式後,停止並重新啟動 Web 伺服器。您應該會看到一個斷開連線訊息。伺服器重新啟動後,按一下「重新載入」選項,請注意,儘管應用程式已恢復,但它會遺失所有狀態。

現在我們有一個問題。讓我們來研究解決方案!

5. 解決方案架構

以下解決方案使用一種旨在最大化重複使用性的架構方法。Blazor.ViewModel 專案託管該應用程式的介面、屬性和業務邏輯。它是 Model-View-ViewModel(MVVM)模式 的 .NET Standard 程式庫實作,可以從 WPF、Xamarin 甚至 Blazor 的任何類型的 .NET Core 專案輕鬆參考。最大程度的重複使用!

對於 UI 和使用者體驗邏輯,以及可共用資產(例如影像、樣式表、JavaScript 程式碼甚至 Razor 檢視元件),Blazor.Shared 都利用 Razor 類別庫。該解決方案實作了 HealthModelBase 避免重複 MVVM 程式碼的功能。它還將此處描述的所有狀態管理解決方案實作為可輕鬆應用於 Blazor WebAssembly 和 Blazor Server 專案的服務和/或元件。由於「宿主」專案僅提供一些結構來參考共用元件和資源,這進一步使程式碼重複使用最大化。

現在,我已經解決了問題和解決方案的方法,讓我們繼續在 Blazor 應用程式中管理狀態!

6. 服務註冊

第一步可能並不那麼明顯,但是為了全面起見,我想介紹服務。要查看實際效果,請建立一個新的 Blazor 用戶端應用程式並執行它。內建範本為幾個頁面提供了簡單的導航。導航到 Counter 頁面並增加計數器。現在,離開頁面瀏覽並返回。計數器重設為零!這是因為計數器的狀態保留在元件中,因此每次初始化元件時都會將其重設:

<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
private int currentCount = 0;
private void IncrementCount()
{
    currentCount++;
}

要維持「記憶體中」(或 Blazor 伺服器「電路中」)狀態,可以建立計數器「服務」:

public class CounterService
{
    public int Count { get; private set; }
    public void Increment()
    {
        Count += 1;
    }
}

Startup.cs 以下位置註冊服務:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<CounterService, CounterService>();
}

…然後刪除 @code 區塊中的程式碼 Counter.razor,注入計數器服務並直接進行資料繫結:

@inject CounterService Svc
<h1>Counter</h1>
<p>Current count: @Svc.Count</p>
<button class="btn btn-primary" @onclick="Svc.Increment">Click me</button>

當元件被銷毀/重新建立時,該服務將保留在記憶體中,即使在導航時也保持一致的計數。這是維持狀態的第一步。參考應用程式以此方式註冊主檢視模型。

7. 瀏覽器快取

維護狀態的一種方法是使用 HTML5 Web Storage 來利用瀏覽器快取。API 非常簡單。BlazorState.Shared 中的 stateManagement.js 檔案定義了一個簡單的、可全域存取的介面。它使用 localStorage JavaScript API,但您可以選擇使用 sessionStorage

window.stateManager = {
    save: function (key, str) {
        localStorage[key] = str;
    },
    load: function (key) {
        return localStorage[key];
    }
};

它包含在 Blazor WebAssembly 專案的 index.html 和 Blazor Server 專案的 _Host.cshtml 的根目錄中。包括共用資產(shared assets)就像使用路徑一樣簡單:

<script src="_content/BlazorState.Shared/stateManagement.js"></script>

Blazor 的元件模型使建立用於管理狀態變更的「包裝器」元件變得簡單。這是在 StorageHelper.razor 中實作的。首先,using 陳述式參考檢視模型、JavaScript 互通性和 JSON 序列化程式。實作被注入。

@using Microsoft.JSInterop; @using System.Text.Json; @inject IJSRuntime
JsRuntime @inject IHealthModel Model

範本只是包裝子元件,並在載入狀態時呈現它們。

@if (hasLoaded) { @ChildContent } else {
<p>Loading...</p>
}

初始化元件後,程式碼嘗試從快取中載入檢視模型:

string vm;
try
{
    vm = await JsRuntime.InvokeAsync<string>("stateManager.load", nameof(HealthModel));
}
catch(InvalidOperationException)
{
    return;
}

在 Blazor 伺服器中,元件已在伺服器上預先渲染。JavaScript 不可用,因此 interop 呼叫將拋出 InvalidOperationException。這是第一次被發現。第二個呼叫是從用戶端進行的,並且如果快取了 viewmodel,它將成功。從快取載入檢視模型的 JSON 後,將對其進行反序列化,並將屬性移至全域檢視模型實例。

var viewModel = JsonSerializer.Deserialize<HealthModel>(vm);
if (viewModel != null)
{
    isDeserializing = true;
    Model.AgeYears = viewModel.AgeYears;
    Model.HeightInches = viewModel.HeightInches;
    Model.IsFemale = viewModel.IsFemale;
    Model.IsImperial = viewModel.IsImperial;
    Model.WeightPounds = viewModel.WeightPounds;
    isDeserializing = false;
}

isDeserializing 標誌對於避免無限迴圈很重要,正如您在下一個註冊屬性變更通知的程式碼中所看到的:

Model.PropertyChanged += async (o, e) =>
{
    if (isDeserializing)
    {
        return;
    }
    var vmStr = JsonSerializer.Serialize(((HealthModel)Model));
    await JsRuntime.InvokeAsync<object>(
        "stateManager.save", nameof(HealthModel), vmStr);
};
hasLoaded = true;

如果檢視模型上的屬性發生變更,則檢視模型將被序列化並儲存在快取中。當由於初始載入而觸發了屬性變更時,將跳過此操作(因此會出現該 isDeserializing 標誌,否則它將在嘗試反序列化時進行序列化)。現在該元件可以使用了!Blazor.ServerLocalBlazor.WasmLocal 都是幫助類,它在 App.razor 中的實作方式相同:

<BlazorState.Shared.StorageHelper>
  <Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
      <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
      <LayoutView Layout="@typeof(MainLayout)">
        <p>Sorry, there's nothing at this address.</p>
      </LayoutView>
    </NotFound>
  </Router>
</BlazorState.Shared.StorageHelper>

透過包裝路由器,狀態管理無需編寫其他程式碼即可處理應用程式中的所有頁面和元件。您可以開啟瀏覽器開發人員工具並導航到應用程式「本機儲存」,以觀察值在更新表單時的變化。

重要的是要注意,使用者可以存取其本機快取,因此,如果您要儲存敏感值,則應對它們進行加密。Microsoft.ASpNetCore.ProtectedBrowserStorage 套件提供了一個範例。

8. 伺服器端管理

處理狀態的另一種方法是呼叫 API 並將其持久保存在伺服器上。其持久性取決於您:選擇範圍從 SQL、NoSQL 到簡單的快取(例如 Redis)。BlazorState.WasmRemote.Server 是 ASP.NET 託管的 Blazor WebAssembly 應用程式。該 StateController 公開一個 API,它儲存和檢索使用遠端 IP 位址作為金鑰對檢視模型。這樣做是為了保持示範簡單。具有身份驗證的生產應用程式可能會鎖定使用者和/或工作階段。

Blazor.Shared 中的 StateService 處理 API 呼叫。建構函式接受全域 viewmodel 實例、提供 API 端點 URL 的 IStateServiceConfig 實例和 HttpClient 的實例。注入 HttpClient 而不是建立新實例非常重要,因為 Blazor WebAssembly 需要專門配置為在瀏覽器沙箱中執行的版本。建構函式從檢視模型註冊屬性變更通知。

在初始化期間由頁面元件呼叫 InitAsync 以載入檢視模型狀態。

public async Task InitAsync()
{
    _initializing = true;
    var vmJson = await _client.GetStringAsync(_config.Url);
    var vm = JsonSerializer.Deserialize<HealthModel>(vmJson, _options);
    _model.AgeYears = vm.AgeYears;
    _model.HeightInches = vm.HeightInches;
    _model.IsFemale = vm.IsFemale;
    _model.IsMetric = vm.IsMetric;
    _model.WeightPounds = vm.WeightPounds;
    _initializing = false;
}

該程式碼與用戶端快取方法非常相似,但是從 API 呼叫而不是本機快取中檢索模型。屬性變更處理常式序列化模型並將模型發佈到伺服器:

private async void Model_PropertyChanged(object sender,
    System.ComponentModel.PropertyChangedEventArgs e)
{
    if (_initializing || _config == null)
    {
        return;
    }
    var vm = JsonSerializer.Serialize(_model);
    var content = new StringContent(vm);
    content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
    await _client.PostAsync(_config.Url, content);
}

設定 BlazorState.WasmRemote.Server 為啟動專案並執行它以查看實際效果。您可能需要更新在 .Client 專案中 IStateServiceConfigStartup.cs 實作中的正確 URL(因為埠可能不同)。在執行解決方案的情況下,開啟「網路」標籤,並在更新表單時記下呼叫。

該服務已針對 Blazor WebAssembly 進行了示範,但與 Blazor Server 相同。

9. 結論

Blazor 對您如何管理狀態沒有意見。服務和元件模型使實作專案範圍的解決方案變得容易。這篇文章著重於 Model-View-ViewModel 模式的實作,並註冊了屬性變更通知以處理本機或透過 API 的序列化狀態。如果您使用諸如 Redux 之類的不同方法,則相同的方法將起作用。重要的步驟是在屬性發生變化時更新商店,並在元件初始化時從狀態管理解決方案載入。剩下的就是瀏覽器的歷史記錄!

檢視有關 ASP.NET Core Blazor 狀態管理 的官方文件。

繼續探索

延伸閱讀

更多文章
同分類 / 同標籤 2024/2/29

Winform中也可以這樣做資料展示

在做winform開發的過程中,經常需要做資料展示的功能,之前一直使用的是gridcontrol控制項,今天想透過一個範例,跟大家介紹一下如何在winform blazor hybrid中使用ant design blazor中的table元件做資料展示。

繼續閱讀
同分類 / 同標籤 2024/2/29

Winform的介面也可以變好看?

前幾天跟大家介紹了在winform中使用blazor hybrid,而且還說配上blazor的UI可以讓我們的winform程式設計的更加好看,接下來我想以一個在winform blazor hybrid中繪圖的範例來進行說明,希望對你有所幫助。

繼續閱讀
同分類 / 同標籤 2024/1/7

碼坊「文章標題URL別名生成器」上線

碼坊是站長新開的一個提供網頁在線工具、跨平台桌面和手機應用的開源專案。站長將終致力於為你帶來更高效、更便捷的使用體驗。今天,站長榮幸地推出「文章標題URL別名生成器」,幫助你輕鬆創建文章標題的URL別名,提升SEO效果和用戶體驗。快來碼坊,探索更多實用工具吧!

繼續閱讀