关于 coordinate-components
指导 Blazor 开发者在组件间选择和实现共享状态机制的技能。内容涵盖级联值、DI 级联值源与作用域服务,并说明渲染模式边界、服务生命周期和变更通知的处理方式。
主要功能
- 01按状态范围选择共享机制
- 02使用级联值传递子树状态
- 03用级联值源通知全应用更新
- 04用作用域服务管理可变状态
- 05处理渲染模式与服务生命周期
适用场景
- 01在组件树中共享主题或布局配置
- 02跨渲染模式共享用户或功能标记
- 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 技能。
工作流(快速参考)
- 根据步骤 2 中的表格选择机制
- 如果跨越渲染模式边界 → 使用
CascadingValueSource<T>(步骤 4) - 在
Program.cs中使用AddCascadingValue(...)和isFixed: false注册 - 在子组件中通过
[CascadingParameter]使用 - 通过
NotifyChangedAsync(newValue)更新 — 绝不要重新加载页面 - 如果电路中还有其他可变状态 → 添加 Scoped 服务(步骤 5)
- 将后台线程中的任何
StateHasChanged调用包装在InvokeAsync中 - 实现
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 上的服务生命周期
| 生命周期 | Server | WebAssembly |
|---|---|---|
| 作用域 | 每个电路(每个用户连接) | 每个浏览器标签页 |
| 单例 | 所有用户共享 | 每个浏览器标签页(安全) |
| 瞬态 | 每次注入创建新实例 | 每次注入创建新实例 |
在 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。