mcpField
C
coordinate-components

coordinate-components

github.com/dotnet开发5.8k3.2k 次安装1 天前更新
代码仓库

关于 coordinate-components

指导 Blazor 开发者在组件间选择和实现共享状态机制的技能。内容涵盖级联值、DI 级联值源与作用域服务,并说明渲染模式边界、服务生命周期和变更通知的处理方式。

主要功能

  1. 01按状态范围选择共享机制
  2. 02使用级联值传递子树状态
  3. 03用级联值源通知全应用更新
  4. 04用作用域服务管理可变状态
  5. 05处理渲染模式与服务生命周期

适用场景

  1. 01在组件树中共享主题或布局配置
  2. 02跨渲染模式共享用户或功能标记
  3. 03管理购物车、通知数或筛选条件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dotnet/skills --skill coordinate-components

安装后在 Claude Code 中输入/coordinate-components确认已加载。

详细介绍

协调组件

步骤 1 — 阅读 AGENTS.md

在进行更改前,阅读工作区根目录中的 AGENTS.md,了解项目约定。

步骤 2 — 确定范围

需求机制使用时机
子树(相同渲染模式)CascadingValue 组件布局中的主题、布局配置
整个应用(所有渲染模式)通过 DI 使用 CascadingValueSource<T>当前用户、功能标志、全局共享的主题
电路中的可变共享状态Scoped 服务 + Action 事件购物车、通知计数、已选筛选条件

对于父→子单层传递:使用 [Parameter] / EventCallback(参见 author-component 技能)。 对于在预渲染→交互模式之间保留状态:参见 support-prerendering 技能。

工作流(快速参考)

  1. 根据步骤 2 中的表格选择机制
  2. 如果跨越渲染模式边界 → 使用 CascadingValueSource<T>(步骤 4)
  3. 在 Program.cs 中使用 AddCascadingValue(...) 和 isFixed: false 注册
  4. 在子组件中通过 [CascadingParameter] 使用
  5. 通过 NotifyChangedAsync(newValue) 更新 — 绝不要重新加载页面
  6. 如果电路中还有其他可变状态 → 添加 Scoped 服务(步骤 5)
  7. 将后台线程中的任何 StateHasChanged 调用包装在 InvokeAsync 中
  8. 实现 IDisposable — 释放计时器、取消令牌、取消订阅事件

步骤 3 — 使用 CascadingValue 管理子树状态

使用 <CascadingValue> 包装子树,将数据传递给所有后代,而无需通过每个中间组件逐层传递。

@* In a layout or parent component *@
<CascadingValue Value="theme">
    @Body
</CascadingValue>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-primary" };
}

在任意后代中使用:

[CascadingParameter]
private ThemeInfo? Theme { get; set; }

规则:

  • 按类型匹配,而不是按名称匹配。要级联多个相同类型的值,请添加 Name:
    <CascadingValue Value="primary" Name="PrimaryTheme">...</CascadingValue>
    
    [CascadingParameter(Name = "PrimaryTheme")]
    private ThemeInfo? Primary { get; set; }
    
  • 当值永不改变时,设置 IsFixed="true" — 避免订阅开销。
  • 不会跨越渲染模式边界。 静态 SSR 父组件中的 <CascadingValue> 对交互式子组件不可见。参见步骤 6。

步骤 4 — 使用 CascadingValueSource<T> 管理整个应用的状态

当某个值必须对所有组件均可用,不受渲染模式影响时,在 DI 中注册 CascadingValueSource<T>。

// Program.cs
builder.Services.AddCascadingValue(sp =>
{
    var theme = new ThemeInfo { ButtonClass = "btn-primary" };
    return new CascadingValueSource<ThemeInfo>(theme, isFixed: false);
});

使用方式与步骤 3 相同:

[CascadingParameter]
private ThemeInfo? Theme { get; set; }

要更新并通知订阅者,可以修改现有对象,也可以替换它:

@* Component that changes the theme *@
@inject CascadingValueSource<ThemeInfo> ThemeSource

<button @onclick="ToggleDarkMode">Toggle theme</button>

@code {
    private bool isDark;

    private async Task ToggleDarkMode()
    {
        isDark = !isDark;
        // Replace the value entirely:
        var newTheme = new ThemeInfo { ButtonClass = isDark ? "btn-dark" : "btn-primary" };
        await ThemeSource.NotifyChangedAsync(newTheme);
    }
}

NotifyChangedAsync()(不带参数)也可使用 — 修改对象后调用它即可。NotifyChangedAsync(newValue) 会替换该值并在一步内发出通知。

更新协议: 每当共享状态发生变化,修改该状态的组件都必须注入 CascadingValueSource<T> 并调用 NotifyChangedAsync()。这是触发所有 [CascadingParameter] 订阅者重新渲染的唯一机制。若不调用此方法,任何订阅者都不会更新。不要用 NavigationManager.Refresh() 或重新加载页面来替代。

规则:

  • isFixed: false 会启用更改通知。对于真正静态的值(如功能标志),isFixed: true 更合适。
  • 可跨越渲染模式边界 — 适用于按页面启用交互、全局启用交互以及 WebAssembly。这是相较于 <CascadingValue> 的主要优势。
  • 级联类型应保持细粒度。每次 NotifyChangedAsync 都会让所有订阅者重新渲染,无论哪个属性发生了变化。不要将所有应用状态都放进一个级联类型中。
  • 对于 Auto/WebAssembly 应用,请在服务器端和 .Client 的 Program.cs 中都注册。该类型必须位于共享程序集内。

第 5 步 — 带变更事件的作用域状态服务

对于由多个组件读取并写入的可变共享状态(购物车、通知计数、筛选条件),请使用带变更通知事件的作用域服务。

定义服务:

public class CartState
{
    private readonly List<CartItem> _items = [];

    public IReadOnlyList<CartItem> Items => _items;
    public int Count => _items.Count;

    public event Action? OnChange;

    public void Add(CartItem item)
    {
        _items.Add(item);
        OnChange?.Invoke();
    }

    public void Remove(CartItem item)
    {
        _items.Remove(item);
        OnChange?.Invoke();
    }
}

注册为作用域服务:

builder.Services.AddScoped<CartState>();

在组件中订阅:

@inject CartState Cart
@implements IDisposable

<span class="badge">@Cart.Count</span>

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

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

当事件在 Blazor 同步上下文中触发时(按钮点击 → Cart.Add(…)),简单的 Action OnChange 模式即可使用。如果事件在同步上下文之外触发(计时器、后台任务、SignalR 集线器),请使用 InvokeAsync 包装:

private Action? _handler;

protected override void OnInitialized()
{
    _handler = () => InvokeAsync(StateHasChanged);
    Cart.OnChange += _handler;
}

public void Dispose() => Cart.OnChange -= _handler;

将委托存储在字段中,以便取消订阅时使用完全相同的实例。

第 6 步 — 渲染模式和服务生命周期规则

级联值不会跨越渲染模式边界

放置在静态 SSR 布局中的 <CascadingValue>(布局静态渲染时的 MainLayout.razor)无法传递给交互式子组件。交互式组件的级联参数会得到 null。

解决方法: 使用在 DI 中注册的 CascadingValueSource<T>(第 4 步)或作用域服务(第 5 步)。两者都能跨越边界,因为 DI 服务是按电路解析的,而不是从组件树中解析的。

Server 与 WebAssembly 上的服务生命周期
生命周期ServerWebAssembly
作用域每个电路(每个用户连接)每个浏览器标签页
单例所有用户共享每个浏览器标签页(安全)
瞬态每次注入创建新实例每次注入创建新实例

在 Server 上,切勿将特定于用户的状态存储在单例中——所有用户的电路共享同一个单例。一个用户的购物车会泄露给另一个用户。请使用 AddScoped<T>()。

在 WebAssembly 上,单例按标签页隔离,是安全的。但同时面向 Server 和 WebAssembly 的代码(Auto 模式)必须使用作用域服务。

Auto/WebAssembly 与预渲染

状态服务必须定义在 .Client 项目或共享程序集内——它们不能引用仅供服务器使用的类型。请在两个 Program.cs 文件中注册该服务。预渲染期间创建的状态不会在切换到交互式运行时时保留。使用 support-prerendering 技能中的 [PersistentState] 模式来传递状态。

注意事项

  • 不要在 Server 上使用单例存储每个用户的状态——所有电路共享该单例,会导致用户之间的状态泄露。
  • 不要将所有应用状态放入一个级联对象中——每次变更调用 NotifyChangedAsync 都会重新渲染所有订阅者。请将不同关注点拆分为不同类型(ThemeState、CartState、UserPreferences)。
  • 不要忘记取消订阅——事件订阅中不实现 Dispose 会导致内存泄漏,并且会随着每个电路持续增长。
  • 不要在静态布局中使用 <CascadingValue>,并期望它能传递给交互式子组件——它无法跨越渲染模式边界。请使用通过 DI 注册的 CascadingValueSource<T> 或作用域服务。
  • 不要使用 NavigationManager.Refresh(forceReload: true) 来传播级联值变更——这会销毁电路并强制完整页面重新加载。应改为注入 CascadingValueSource<T> 并调用 NotifyChangedAsync(newValue),无需重新加载页面即可向所有 [CascadingParameter] 订阅者推送更新。
  • 不要从非 Blazor 线程调用 StateHasChanged——请使用 InvokeAsync 包装。框架会抛出 InvalidOperationException: The current thread is not associated with the Dispatcher。

评论

支持 Markdown