Blazor status management

Blazor status management

Imagine you are filling out the longest form in the world. You have spent 30 minutes entering details, from address to your birthday, to a list of seven countries you have recently visited. You click the "Submit" button and you will immediately get the "Connection Lost" message.

最后更新 4/18/2022 7:53 PM
寒冰屋
预计阅读 12 分钟
分类
Blazor
标签
.NET Blazor state management

Imagine you are filling out the longest form in the world. You have spent 30 minutes entering details, from address to your birthday, to a list of seven countries you have recently visited. You click the "Submit" button and you will immediately get the "Connection Lost" message. Don't worry, right? Just click the Back button,…Oh, no! The table is empty. This may seem barbaric, and you promise not to visit the site again.

This is not the experience you want website visitors to have. Therefore, it is important to understand how to manage state in a Blazor application. Manage state while minimizing the amount of code you have to write to manage state? "Yes, please!"

观看相关视频:Blazor Apps 中的状态管理

1 Definition of Blazor state

First, let's figure out what "state" means in a Blazor application. To get the best user experience, it is important to provide end users with a consistent experience when the end user's connection is temporarily interrupted, refreshed or navigated back to the page. Components of experience include:

  • The HTML Document Object Model (DOM) that represents the user interface (UI)
  • Fields and properties that represent the data being entered and/or exported on the page
  • Status of the registration service running as part of the page code

在没有任何特殊代码的情况下,根据Blazor 托管模型,将状态保存在两个位置。对于 Blazor WebAssembly(客户端)应用程序,状态保持在浏览器内存中,直到用户刷新或导航离开页面为止。在 Blazor Server 应用程序中,状态保存在分配给每个称为电路的每个客户端会话的特殊“存储桶”中。这些电路在断开连接后超时时可能会丢失状态,甚至在服务器处于内存压力下的活动连接过程中也可能消失。

2. reference application

为了说明状态的细微差别,我从Blazor Health App开始:

从 Angular 到 Blazor:The Health App

Build sample applications in Blazor, a. NET-based framework for building Web applications that run in a browser and leveraging C#and Razor templates to generate cross-platform, HTML5-compatible WebAssembly code.

I expanded it to include two pages to illustrate some of the nuances of navigation. In the relevant GitHub repository:

JeremyLikness/BlazorState

There are several example projects. The problem is reflected differently in the Blazor WebAssembly and Blazor Server projects.

3. Status in Blazor WebAssembly

In Blazor WebAssembly (client project), state is saved in memory. This means that refreshing or forcing navigation will destroy the state. To see the actual results:

  1. 设置BlazorState.Wasm为启动项目并运行它。
  2. Update form information.
  3. Navigate to Results and verify that the same result exists.
  4. 导航回“主页”并强制刷新(通常为CTRL+F5)。请注意该窗体还原为默认值。
  5. 更新表单信息,然后通过添加/results到浏览器中的 URL 栏来手动导航,然后按ENTER。请注意,它也使用默认值。

Bad experience! Compared to Blazor Server, it is slightly different.

4. Status in the Blazor server

将启动项目更改为BlazorState.Server并运行该项目。请尝试执行与客户端版本相同的步骤,并注意保持了状态,因为该状态保存在服务器内存中。打开应用程序后,停止并重新启动 Web 服务器。您应该看到一个断开连接消息。服务器重新启动后,单击“重新加载”选项,请注意,尽管应用程序已恢复,但它会丢失所有状态。

Now we have a problem. Let's study solutions!

5. solution architecture

以下解决方案使用一种旨在最大化重用性的体系结构方法。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 项目的服务和/或组件。由于“宿主”项目仅提供一些结构来引用共享组件和资源,因此这进一步使代码重用最大化。

Now that I have solved the problem and the solution, let's continue to manage state in the Blazor app!

6. service registration

第一步可能并不那么明显,但是为了全面起见,我想介绍服务。要查看实际效果,请创建一个新的 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++;
}

To maintain an "in-memory"(or "in-circuit" on the Blazor server) state, you can create a counter "service":

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>

When a component is destroyed/recreated, the service remains in memory, maintaining consistent counts even while navigation. This is the first step in maintaining your status. The reference application registers the main view model in this way.

7. browser cache

维护状态的一种方法是使用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

Templates just wrap subcomponents and render them when loaded.

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

After initializing the component, the code attempts to load the view model from the cache:

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>

By wrapping the router, state management handles all pages and components in the application without writing additional code. You can open the Browser Developer Tool and navigate to the application Local Storage to observe how the values change as the form is updated.

重要的是要注意,用户可以访问其本地缓存,因此,如果您要存储敏感值,则应对它们进行加密。Microsoft.ASpNetCore.ProtectedBrowserStorage包提供了一个示例。

8. server management

处理状态的另一种方法是调用 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;
}

This code is very similar to the client-side caching method, but retrieves the model from API calls rather than local caches. Property change handler serializes the model and publishes the model to the server:

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(因为端口可能不同)。在运行解决方案的情况下,打开“网络”选项卡,并在更新表单时记下调用。

The service has been demonstrated for Blazor WebAssembly, but is the same as Blazor Server.

9. conclusion

Blazor 对您如何管理状态没有意见。服务和组件模型使实现项目范围的解决方案变得容易。这篇文章着重于 Model-View-ViewModel 模式的实现,并注册了属性更改通知以处理本地或通过 API 的序列化状态。如果您使用诸如Redux之类的不同方法,则相同的方法将起作用。重要的步骤是在属性发生变化时更新商店,并在组件初始化时从状态管理解决方案加载。剩下的就是浏览器的历史记录!

查看有关ASP.NET Core Blazor 状态管理的官方文档。

Keep Exploring

延伸阅读

更多文章
同分类 / 同标签 11/6/2024

Why is my blog site back in Blazor?

The blog website has gone through hardships in development. It has tried nearly 10 versions such as MVC, Vue, and Go. Now it has returned to Blazor and adopts static SSR. The speed has skyrocketed and it has been successfully launched.

继续阅读
同分类 / 同标签 2/29/2024

Data display can also be done in Winform

In the process of developing winform, data display functions are often needed. I have been using the gridcontrol before. Today, I want to use an example to introduce to you how to use the table component in ant design blazor hybrid to display data.

继续阅读
同分类 / 同标签 2/29/2024

Can Winform's interface become better?

A few days ago, I introduced to you the use of blazor hybrid in winform, and I also said that with blazor's ui, our winform program design can be more beautiful. Next, I want to use an example of drawing in winform blazor hybrid to illustrate it, hoping to be helpful to you.

继续阅读
同分类 / 同标签 1/7/2024

Code Workshop "Article Title URL Alias Generator" is launched

Code Workshop is a newly opened open source project by the webmaster that provides online tools, cross-platform desktop and mobile applications. Webmasters will ultimately strive to bring you a more efficient and convenient experience. Today, webmasters are honored to launch the "Article Title URL Alias Generator" to help you easily create URL aliases for article titles and improve SEO effects and user experience. Come to the code workshop and explore more practical tools!

继续阅读