ASP.NET 核心 Blazor 状态管理概述

本文和此节点中的其他文章介绍了在使用应用时以及跨浏览器会话(包括服务器预呈现期间)维护用户数据(状态)的常见方法。

应用开发过程中 Blazor 的典型要求是跨组件共享状态:

  • 父级到子组件:父组件使用参数将状态传递给子组件。
  • 子级到父级:子组件允许数据绑定到其状态或通过回调提供状态。
  • 父级到后代:父级共享状态,其所有后代都使用级联值。
  • 应用范围:使用配置的应用状态服务在整个应用中共享状态。
  • 每个线路:使用范围的应用状态服务为特定线路共享状态。

持久状态可能需要在页面刷新、恢复线路和预呈现中幸存下来。 状态通常需要集中管理、跟踪和测试。 保存状态的位置和技术高度可变。

Blazor 不提供全面、有意见的状态管理。 与 Flux、Redux 和 MobX 无缝协作 Blazor的第三方状态容器产品和服务几乎满足任何应用要求。

本文的其余部分讨论任何类型的 Blazor 应用的常规状态管理策略。

使用 URL 进行状态管理

对于表示导航状态的暂时性数据,请将数据作为 URL 的一部分进行建模。 URL 中建模的用户状态示例:

  • 已查看实体的 ID。
  • 分页网格中的当前页码。

保留浏览器地址栏的内容:

  • 如果用户手动重载页面。
  • 仅服务器端方案:如果 Web 服务器不可用,并且用户被迫重新加载页面以连接到其他服务器。

有关使用 @page 指令定义 URL 模式的信息,请参阅 ASP.NET Core Blazor 路由和导航

内存中状态容器服务

嵌套组件通常使用 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通常位于 .NET 6 或更早版本中):

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,并且 OnChange 委托在 Dispose 方法中取消订阅,这些方法是在释放组件时由框架调用的。 有关详细信息,请参阅 ASP.NET Core Razor 组件处置

级联值和参数

使用 级联值和参数 通过将数据从上级 Razor 组件流向后代组件来管理状态:

  • 跨多个组件使用状态。
  • 只有一个顶级状态对象要保留时。

根级级联值允许通知CascadingValueSource<TValue>Razor组件的订阅者有关已更改的级联值。 有关详细信息和工作示例,请参阅 NotifyingDalekASP.NET 核心 Blazor 级联值和参数中的示例。

支持从外部 Blazor同步上下文进行状态修改

使用自定义状态管理服务时,如果要支持从外部的 Blazor 同步上下文(例如计时器或后台服务)进行状态修改,则所有使用的组件都必须将 StateHasChanged 调用包装在 ComponentBase.InvokeAsync 中。 这可确保在呈现器的同步上下文上处理更改通知。

如果状态管理服务没有在 StateHasChanged 的同步上下文上调用 Blazor,会引发以下错误:

System.InvalidOperationException:“当前线程未与 Dispatcher 相关联。 触发呈现或组件状态时,使用 InvokeAsync() 将执行切换到 Dispatcher。”

有关详细信息以及如何解决此错误的示例,请参阅 ASP.NET Core Razor 组件呈现