ASP.NET Core Blazor 状态管理

注意

此版本不是本文的最新版本。 有关当前版本,请参阅本文.NET 9 版本。

警告

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

有关当前版本,请参阅本文.NET 9 版本。

本文介绍在用户使用应用和跨浏览器会话时维护用户数据(状态)的常见方法。

注意

本文中的代码示例采用在 .NET 6 或更高版本的 ASP.NET Core 中支持的可为空的引用类型 (NRT) 和 .NET 编译器 Null 状态静态分析。 面向 ASP.NET Core 5.0 或更早版本时,请从文章示例中的类型中删除 NULL 类型指定 (?)。

维护用户状态

服务器端 Blazor 是有状态的应用框架。 大多数情况下,应用保持与服务器的连接。 用户的状态保留在线路中的服务器内存中。

线路中保留的用户状态示例:

  • 呈现的 UI 中组件实例的层次结构及其最新的呈现输出。
  • 组件实例中的字段和属性的值。
  • 在线路范围内的依赖关系注入 (DI) 服务实例中保留的数据。

还可以通过 JavaScript 互操作 调用在浏览器的内存集的 JavaScript 变量中找到用户状态。

如果用户遇到暂时的网络连接丢失问题,Blazor 会尝试将用户重新连接到具有其原始状态的原始线路。 但是,将用户重新连接到服务器内存中的原始电路并非总是能够实现的:

  • 服务器不能永久保留断开连接的线路。 超时后或在服务器面临内存压力时,服务器必须释放断开连接的线路。
  • 在负载均衡的多服务器部署环境中,不再需要单个服务器处理整个请求量时,它可能会失败或被自动删除。 在用户尝试重新连接时,用户的原始服务器处理请求可能会变得不可用。
  • 用户可能会关闭并重新打开其浏览器或重载页面,这会删除浏览器内存中保留的所有状态。 例如,通过 JavaScript 互操作调用设置的 JavaScript 变量值会丢失。

当无法将用户重新连接到其原始线路时,用户将收到一个具有空状态的新线路。 这等效于关闭并重新打开桌面应用。

跨线路保留状态

通常情况下,在用户主动创建数据,而不是简单地读取已存在的数据时,会跨线路保持状态。

若要跨线路保留状态,应用必须将数据保存到服务器的内存以外的其他存储位置。 状态暂留并非是自动进行的。 必须在开发应用时采取措施来实现有状态的数据暂留。

通常,只有用户投入了大量精力所创建的高价值状态才需要数据暂留。 在下面的示例中,保留状态可以节省时间或有助于商业活动:

  • 多步骤 Web 窗体:如果多步骤 Web 窗体的多个已完成步骤的状态丢失,用户重新输入这些步骤的数据会非常耗时。 如果用户离开窗体并在稍后返回,在这种应用场景下,用户将丢失状态。
  • 购物车:应用中任何代表潜在收入且具有重要商业价值的组件都可以保留。 如果用户丢失了其状态,进而丢失了其购物车,则在他们稍后返回站点时可购买较少的产品或服务。

应用只能保留应用状态。 不能保留 UI,如组件实例及其呈现树。 组件和呈现树通常不能序列化。 若要保留 UI 状态(如树视图控件的展开节点),应用必须使用自定义代码将 UI 状态行为建模为可序列化应用状态。

保留状态的位置

用于保留状态的常见位置有:

服务器端存储

对于跨多个用户和设备的永久数据持久性,应用可以使用服务器端存储。 选项包括:

  • Blob 存储
  • 键值存储
  • 关系数据库
  • 表存储

保存数据后,将保留用户的状态,并在任何新的线路中可用。

有关 Azure 数据存储选项的详细信息,请参阅以下内容:

URL

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

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

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

  • 如果用户手动重载页面。
  • 如果 Web 服务器不可用,且用户被强制重载页面,以便连接到其他服务器。

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

浏览器存储

对于用户正在主动创建的暂时性数据,通用存储位置是浏览器的 localStoragesessionStorage 集合:

  • localStorage 的应用范围限定为浏览器的窗口。 如果用户重载页面或关闭并重新打开浏览器,则状态保持不变。 如果用户打开多个浏览器选项卡,则状态跨选项卡共享。 数据保留在 localStorage 中,直到被显式清除为止。
  • sessionStorage 的应用范围限定为浏览器的选项卡。如果用户重载该选项卡,则状态保持不变。 如果用户关闭该选项卡或该浏览器,则状态丢失。 如果用户打开多个浏览器选项卡,则每个选项卡都有自己独立的数据版本。

通常,sessionStorage 使用起来更安全。 sessionStorage 避免了用户打开多个选项卡并遇到以下问题的风险:

  • 跨选项卡的状态存储中出现 bug。
  • 一个选项卡覆盖其他选项卡的状态时出现混乱行为。

如果应用必须在关闭和重新打开浏览器期间保持状态,则 localStorage 是更好的选择。

使用浏览器存储时的注意事项:

  • 与使用服务器端数据库类似,加载和保存数据都是异步的。
  • 在预呈现阶段,请求的页面在浏览器中不存在,因此,在预呈现期间,本地存储不可用。
  • 对于服务器端 Blazor 应用,持久存储几千字节的数据是合理的。 超出几千字节后,你就须考虑性能影响,因为数据是跨网络加载和保存的。
  • 用户可以查看或篡改数据。 ASP.NET Core 数据保护可以降低风险。 例如,ASP.NET Core 受保护的浏览器存储使用 ASP.NET Core 数据保护。

第三方 NuGet 包提供使用 localStoragesessionStorage 时采用的 API。 值得考虑的是,选择一个透明地使用 ASP.NET Core 数据保护的包。 数据保护可对存储的数据进行加密,并降低篡改存储数据的潜在风险。 如果 JSON 序列化的数据以纯文本形式存储,则用户可以使用浏览器开发人员工具查看数据,还可以修改存储的数据。 保护普通数据并不是问题。 例如,读取或修改 UI 元素的存储颜色不会对用户或组织造成严重的安全风险。 避免允许用户检查或篡改敏感数据。

ASP.NET Core 受保护的浏览器存储

ASP.NET Core 受保护的浏览器存储将 ASP.NET Core 数据保护用于 localStoragesessionStorage

注意

受保护的浏览器存储依赖于 ASP.NET Core 数据保护,仅支持用于服务器端 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 项目模板的应用的 Counter 组件中保留 currentCount 值,请修改 IncrementCount 方法以使用 ProtectedSessionStore.SetAsync

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

在更大、更真实的应用中,存储单个字段是不太可能出现的情况。 应用更有可能存储包含复杂状态的整个模型对象。 ProtectedSessionStore 自动串行化和反序列化 JSON 数据以存储复杂的状态对象。

在前面的代码示例中,currentCount 数据存储为用户浏览器中的 sessionStorage['count']。 数据不会以纯文本形式存储,而是使用 ASP.NET Core 的数据保护进行保护。 如果在浏览器的开发人员控制台中评估了 sessionStorage['count'],则可以检查已加密的数据。

若要在用户稍后返回到 Counter 组件时(包括用于位于新线路上时)恢复 currentCount 数据,请使用 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,而不是 OnInitializedAsyncOnInitializedAsync 仅在首次实例化组件时调用一次。 如果用户导航到不同的 URL,而仍然停留在相同的页面上,则 OnInitializedAsync 之后不会再次调用。 有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期

警告

本节中的示例仅在服务器未启用预呈现的情况下有效。 启用预呈现后,会生成错误,说明由于正在预呈现组件,无法发起 JavaScript 互操作调用。

禁用预呈现或添加其他代码以处理预呈现。 若要了解有关编写可处理预呈现的代码的详细信息,请参阅处理预呈现一节。

处理加载状态

由于浏览器存储是异步访问(通过网络连接进行访问)的,因此往往需要一段时间才能加载完数据并可供组件使用。 为获得最佳结果,请在加载进行过程中呈现一条消息,而不要显示空数据或默认数据。

一种方法是跟踪数据是否为 null(表示数据仍在加载)。 在默认 Counter 组件中,计数保留在 int 中。 通过将问号 (?) 添加到类型 (int),使 currentCount 可以为 null

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 互操作调用。

解决此错误的一种方法是禁用预呈现。 如果应用大量使用基于浏览器的存储,则这通常是最佳选择。 预呈现会增加复杂性,且不会给应用带来好处,因为在 localStoragesessionStorage 可用之前,应用无法预呈现任何有用的内容。

若要禁用预呈现,请通过在应用组件层次结构中的最高级别组件(不是根组件)处将 prerender 参数设置为 false 来指示呈现模式。

注意

不支持让根组件具有交互性(例如 App 组件)。 因此,App 组件无法直接禁用预呈现。

对于基于 Blazor Web App 项目模板的应用,如果在 App 组件 (Components/App.razor) 中使用了 Routes 组件,通常会禁用预呈现:

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

此外,请禁用 HeadOutlet 组件的预呈现:

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

有关详细信息,请参阅 ASP.NET Core Blazor 呈现模式

若要禁用预呈现,请打开 _Host.cshtml 文件,并将组件标记帮助程序render-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

@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 OnInitializedAsync()
    {
        var result = await ProtectedSessionStore.GetAsync<int>("count");
        CurrentCount = result.Success ? result.Value : 0;
        isLoaded = true;
    }

    public async Task SaveChangesAsync()
    {
        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 OnInitializedAsync()
    {
        CurrentCount = await ProtectedSessionStore.GetAsync<int>("count");
        isLoaded = true;
    }

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

注意

有关 RenderFragment 的详细信息,请参阅 ASP.NET Core Razor 组件

CounterStateProvider 组件处理加载阶段的方式是在状态加载完成后才呈现其子内容。

若要使应用中的所有组件都可以访问状态,请使用全局交互式服务器端呈现(交互式 SSR)将 CounterStateProvider 组件包装在 Routes 组件中的 Router (<Router>...</Router>) 周围。

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

<Routes @rendermode="InteractiveServer" />

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

若要使用 CounterStateProvider 组件,请围绕需要访问计数器状态的任何其他组件包装该组件的实例。 若要使某个应用中的所有组件都可以访问该状态,请围绕 App 组件 (App.razor) 中的 Router 来包装 CounterStateProvider 组件:

<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)
        {
            CounterStateProvider.CurrentCount++;
            await CounterStateProvider.SaveChangesAsync();
        }
    }
}

ProtectedBrowserStorage 进行交互无需前面的组件,该组件也不会处理“正在加载”阶段。

如前所述,若要处理预呈现,可对 CounterStateProvider 进行修改,以便所有使用计数器数据的组件均可自动处理预呈现。 有关详细信息,请参阅处理预呈现部分。

通常,建议在以下情况下使用状态提供程序父组件模式:

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

若要保留多个不同的状态对象并在不同位置使用不同的对象子集,最好避免全局保留状态。

在 Blazor WebAssembly 应用中创建的用户状态会保存在浏览器的内存中。

浏览器内存中保留的用户状态的示例:

  • 呈现的 UI 中组件实例的层次结构及其最新的呈现输出。
  • 组件实例中的字段和属性的值。
  • 依赖关系注入 (DI) 服务实例中保留的数据。
  • 通过 JavaScript 互操作调用设置的值。

当用户关闭并重新打开其浏览器或重新加载页面时,浏览器的内存中保存的用户状态丢失。

注意

受保护的浏览器存储Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage 命名空间)依赖于 ASP.NET Core 数据保护,仅支持用于服务器端 Blazor 应用。

跨浏览器会话保留状态

通常情况下,在用户主动创建数据,而不是简单地读取已存在的数据时,会跨浏览器会话保持状态。

若要跨浏览器会话保留状态,应用必须将数据保存到浏览器的内存以外的其他存储位置。 状态暂留并非是自动进行的。 必须在开发应用时采取措施来实现有状态的数据暂留。

通常,只有用户投入了大量精力所创建的高价值状态才需要数据暂留。 在下面的示例中,保留状态可以节省时间或有助于商业活动:

  • 多步骤 Web 窗体:如果多步骤 Web 窗体的多个已完成步骤的状态丢失,用户重新输入这些步骤的数据会非常耗时。 如果用户离开窗体并在稍后返回,在这种应用场景下,用户将丢失状态。
  • 购物车:应用中任何代表潜在收入且具有重要商业价值的组件都可以保留。 如果用户丢失了其状态,进而丢失了其购物车,则在他们稍后返回站点时可购买较少的产品或服务。

应用只能保留应用状态。 不能保留 UI,如组件实例及其呈现树。 组件和呈现树通常不能序列化。 若要保留 UI 状态(如树视图控件的展开节点),应用必须使用自定义代码将 UI 状态行为建模为可序列化应用状态。

保留状态的位置

用于保留状态的常见位置有:

服务器端存储

对于跨多个用户和设备的永久数据持久性,应用可以使用通过 Web API 访问的独立服务器端存储。 选项包括:

  • Blob 存储
  • 键值存储
  • 关系数据库
  • 表存储

保存数据后,将保留用户的状态,并在任何新的浏览器会话中可用。

由于 Blazor WebAssembly 应用完全在用户的浏览器中运行,因此它们需要额外的措施来访问安全的外部系统,如存储服务和数据库。 Blazor WebAssembly 应用的保护方式与单页应用 (SPA) 相同。 通常,应用通过 OAuth/OpenID Connect (OIDC) 对用户进行身份验证,然后通过对服务器端应用的 Web API 调用与存储服务和数据库进行交互。 服务器端应用可协调 Blazor WebAssembly 应用与存储服务或数据库之间的数据传输。 Blazor WebAssembly 应用保持与服务器端应用的临时连接,而服务器端应用具有到存储的持久连接。

有关详细信息,请参阅以下资源:

有关 Azure 数据存储选项的详细信息,请参阅以下内容:

URL

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

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

在用户手动重新加载页面时保留的浏览器地址栏的内容。

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

浏览器存储

对于用户正在主动创建的暂时性数据,通用存储位置是浏览器的 localStoragesessionStorage 集合:

  • localStorage 的应用范围限定为浏览器的窗口。 如果用户重载页面或关闭并重新打开浏览器,则状态保持不变。 如果用户打开多个浏览器选项卡,则状态跨选项卡共享。 数据保留在 localStorage 中,直到被显式清除为止。
  • sessionStorage 的应用范围限定为浏览器的选项卡。如果用户重载该选项卡,则状态保持不变。 如果用户关闭该选项卡或该浏览器,则状态丢失。 如果用户打开多个浏览器选项卡,则每个选项卡都有自己独立的数据版本。

注意

localStoragesessionStorage 可用于 Blazor WebAssembly 应用,但只能通过编写自定义代码或使用第三方包的方式使用。

通常,sessionStorage 使用起来更安全。 sessionStorage 避免了用户打开多个选项卡并遇到以下问题的风险:

  • 跨选项卡的状态存储中出现 bug。
  • 一个选项卡覆盖其他选项卡的状态时出现混乱行为。

如果应用必须在关闭和重新打开浏览器期间保持状态,则 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.csStartup.ConfigureServices、早于 6.0 版的 ASP.NET Core):

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 组件生命周期

其他方法

实现自定义状态存储时,一种有用的方法是采用级联值和参数

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

疑难解答

在自定义状态管理服务中,如果回调是在 Blazor 的同步上下文之外调用的,那么该回调必须将回调逻辑包装在 ComponentBase.InvokeAsync 中,以将其移到呈现器的同步上下文中。

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

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

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

其他资源