coordinate-components
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCoordinate Components
组件协同
Step 1 — Read AGENTS.md
步骤1 — 阅读AGENTS.md
Read at the workspace root to learn the project's conventions before making changes.
AGENTS.md在进行修改前,请阅读工作区根目录下的,了解项目约定。
AGENTS.mdStep 2 — Decide the scope
步骤2 — 确定作用范围
| Need | Mechanism | When to use |
|---|---|---|
| Subtree (same render mode) | | Theme, layout config within a layout |
| App-wide (all render modes) | | Current user, feature flags, theme shared globally |
| Mutable shared state within a circuit | Scoped service + | Shopping cart, notification count, selected filters |
For parent→child one level: use / (see skill).
For persisting state across prerender→interactive: see skill.
[Parameter]EventCallbackauthor-componentsupport-prerendering| 需求 | 实现机制 | 使用场景 |
|---|---|---|
| 子树(同一渲染模式) | | 布局内的主题、布局配置 |
| 全局范围(所有渲染模式) | 通过DI注入的 | 当前用户、功能标志、全局共享主题 |
| 电路内的可变共享状态 | 作用域服务 + | 购物车、通知计数、选中的筛选器 |
对于父→子单层级传递:使用 / (参考技能)。
对于跨预渲染→交互式的状态持久化:参考技能。
[Parameter]EventCallbackauthor-componentsupport-prerenderingWorkflow (quick reference)
工作流程(快速参考)
- Choose the mechanism from the table in Step 2
- If crossing render mode boundaries → use (Step 4)
CascadingValueSource<T> - Register in with
Program.csandAddCascadingValue(...)isFixed: false - Consume via in child components
[CascadingParameter] - Update via — never page reload
NotifyChangedAsync(newValue) - For additional mutable state within a circuit → add scoped service (Step 5)
- Wrap any from background threads in
StateHasChangedInvokeAsync - Implement — dispose timers, cancel tokens, unsubscribe events
IDisposable
- 从步骤2的表格中选择合适的实现机制
- 若需跨渲染模式边界 → 使用(步骤4)
CascadingValueSource<T> - 在中通过
Program.cs注册,并设置AddCascadingValue(...)isFixed: false - 在子组件中通过消费
[CascadingParameter] - 通过更新状态 — 绝不要刷新页面
NotifyChangedAsync(newValue) - 若需电路内额外的可变状态 → 添加作用域服务(步骤5)
- 将后台线程中的任何调用包裹在
StateHasChanged中InvokeAsync - 实现接口 — 释放定时器、取消令牌、取消事件订阅
IDisposable
Step 3 — CascadingValue for subtree state
步骤3 — 用于子树状态的CascadingValue
Wrap a subtree with to flow data to all descendants without passing it through every intermediate component.
<CascadingValue>razor
@* In a layout or parent component *@
<CascadingValue Value="theme">
@Body
</CascadingValue>
@code {
private ThemeInfo theme = new() { ButtonClass = "btn-primary" };
}Consume in any descendant:
csharp
[CascadingParameter]
private ThemeInfo? Theme { get; set; }Rules:
- Matched by type, not name. To cascade multiple values of the same type, add :
Namerazor<CascadingValue Value="primary" Name="PrimaryTheme">...</CascadingValue>csharp[CascadingParameter(Name = "PrimaryTheme")] private ThemeInfo? Primary { get; set; } - Set when the value never changes — avoids subscription overhead.
IsFixed="true" - Does NOT cross render mode boundaries. A in a static SSR parent is invisible to interactive children. See Step 6.
<CascadingValue>
使用包裹子树,无需通过每个中间组件传递数据,即可将数据传递给所有后代组件。
<CascadingValue>razor
@* 在布局或父组件中 *@
<CascadingValue Value="theme">
@Body
</CascadingValue>
@code {
private ThemeInfo theme = new() { ButtonClass = "btn-primary" };
}在任意后代组件中消费:
csharp
[CascadingParameter]
private ThemeInfo? Theme { get; set; }规则:
- 按类型匹配,而非名称。若要级联多个同类型的值,请添加属性:
Namerazor<CascadingValue Value="primary" Name="PrimaryTheme">...</CascadingValue>csharp[CascadingParameter(Name = "PrimaryTheme")] private ThemeInfo? Primary { get; set; } - 当值永远不会改变时,设置— 可避免订阅开销。
IsFixed="true" - 无法跨渲染模式边界。静态SSR父组件中的对交互式子组件不可见。请参考步骤6。
<CascadingValue>
Step 4 — CascadingValueSource<T> for app-wide state
步骤4 — 用于全局状态的CascadingValueSource<T>
Register a in DI when the value must be available to all components regardless of render mode.
CascadingValueSource<T>csharp
// Program.cs
builder.Services.AddCascadingValue(sp =>
{
var theme = new ThemeInfo { ButtonClass = "btn-primary" };
return new CascadingValueSource<ThemeInfo>(theme, isFixed: false);
});Consume identically to Step 3:
csharp
[CascadingParameter]
private ThemeInfo? Theme { get; set; }To update and notify subscribers, either mutate the existing object or replace it:
razor
@* 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)Update protocol: Whenever shared state changes, the component that changes it MUST inject and call . This is the only mechanism that triggers re-rendering in all subscribers. Without this call, no subscribers update. Do not use or page reloads as a substitute.
CascadingValueSource<T>NotifyChangedAsync()[CascadingParameter]NavigationManager.Refresh()Rules:
- enables change notifications.
isFixed: falseis better for truly static values (feature flags).isFixed: true - Crosses render mode boundaries — works for per-page interactivity, global interactivity, and WebAssembly. Key advantage over .
<CascadingValue> - Keep cascaded types granular. Every re-renders ALL subscribers regardless of which property changed. Don't put all app state into one cascaded type.
NotifyChangedAsync - For Auto/WebAssembly apps, register in both server and
.Client. The type must be in a shared assembly.Program.cs
当值需要对**所有组件(无论渲染模式如何)**可用时,在DI中注册。
CascadingValueSource<T>csharp
// Program.cs
builder.Services.AddCascadingValue(sp =>
{
var theme = new ThemeInfo { ButtonClass = "btn-primary" };
return new CascadingValueSource<ThemeInfo>(theme, isFixed: false);
});消费方式与步骤3完全相同:
csharp
[CascadingParameter]
private ThemeInfo? Theme { get; set; }更新并通知订阅者,可选择修改现有对象或替换对象:
razor
@* 修改主题的组件 *@
@inject CascadingValueSource<ThemeInfo> ThemeSource
<button @onclick="ToggleDarkMode">切换主题</button>
@code {
private bool isDark;
private async Task ToggleDarkMode()
{
isDark = !isDark;
// 完全替换值:
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
Step 5 — Scoped state service with change events
步骤5 — 带变更事件的作用域状态服务
For mutable shared state that multiple components read and write (shopping cart, notification count, filters), use a scoped service with an event for change notification.
Define the service:
csharp
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();
}
}Register as scoped:
csharp
builder.Services.AddScoped<CartState>();Subscribe in components:
razor
@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;
}
}The simple pattern works when the event fires from the Blazor sync context (button click → ). If the event fires from outside the sync context (timer, background task, SignalR hub), wrap in :
Action OnChangeCart.Add(…)InvokeAsynccsharp
private Action? _handler;
protected override void OnInitialized()
{
_handler = () => InvokeAsync(StateHasChanged);
Cart.OnChange += _handler;
}
public void Dispose() => Cart.OnChange -= _handler;Store the delegate in a field so you can unsubscribe the exact same instance.
对于多个组件需要读写的可变共享状态(如购物车、通知计数、筛选器),请使用带变更事件的作用域服务。
定义服务:
csharp
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();
}
}注册为作用域服务:
csharp
builder.Services.AddScoped<CartState>();在组件中订阅:
razor
@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同步上下文触发(如按钮点击→)时,简单的模式即可生效。若事件从同步上下文外部触发(如定时器、后台任务、SignalR集线器),请将调用包裹在中:
Cart.Add(…)Action OnChangeInvokeAsynccsharp
private Action? _handler;
protected override void OnInitialized()
{
_handler = () => InvokeAsync(StateHasChanged);
Cart.OnChange += _handler;
}
public void Dispose() => Cart.OnChange -= _handler;将委托存储在字段中,以便取消订阅完全相同的实例。
Step 6 — Render mode and service lifetime rules
步骤6 — 渲染模式与服务生命周期规则
Cascading values don't cross render mode boundaries
级联值无法跨渲染模式边界
A placed in a static SSR layout ( when the layout renders statically) will not reach interactive children. The interactive component sees for the cascading parameter.
<CascadingValue>MainLayout.razornullFix: Use registered in DI (Step 4) or a scoped service (Step 5). Both cross boundaries because DI services are resolved per-circuit, not from the component tree.
CascadingValueSource<T>放置在静态SSR布局(当布局以静态方式渲染时的)中的无法传递到交互式子组件。交互式组件会将级联参数视为。
MainLayout.razor<CascadingValue>null解决方法: 使用DI注册的(步骤4)或作用域服务(步骤5)。两者均可跨边界,因为DI服务是按电路解析的,而非从组件树解析。
CascadingValueSource<T>Service lifetime on Server vs WebAssembly
Server与WebAssembly的服务生命周期对比
| Lifetime | Server | WebAssembly |
|---|---|---|
| Scoped | Per circuit (per user connection) | Per browser tab |
| Singleton | Shared across ALL users | Per browser tab (safe) |
| Transient | New instance per injection | New instance per injection |
On Server, never store user-specific state in a singleton — every user's circuit shares the same singleton. One user's cart leaks into another's. Use .
AddScoped<T>()On WebAssembly, singletons are per-tab and safe. But code meant for both Server and WebAssembly (Auto mode) must use scoped.
| 生命周期 | Server | WebAssembly |
|---|---|---|
| 作用域(Scoped) | 每个电路(每个用户连接) | 每个浏览器标签页 |
| 单例(Singleton) | 所有用户共享 | 每个浏览器标签页(安全) |
| 瞬时(Transient) | 每次注入生成新实例 | 每次注入生成新实例 |
在Server模式下,绝不要在单例中存储用户特定状态 — 所有用户的电路共享同一个单例,会导致用户间的状态泄露。请使用。
AddScoped<T>()在WebAssembly模式下,单例是按标签页隔离的,因此是安全的。但同时适用于Server和WebAssembly(Auto模式)的代码必须使用作用域服务。
Auto/WebAssembly with prerendering
带预渲染的Auto/WebAssembly模式
State services must be defined in the project or a shared assembly — they cannot reference server-only types. Register the service in both files. State created during prerender does not survive the switch to the interactive runtime. Use the skill's pattern to carry state across.
.ClientProgram.cssupport-prerendering[PersistentState]状态服务必须定义在项目或共享程序集中 — 它们不能引用仅服务器端的类型。需在两个文件中注册服务。预渲染期间创建的状态无法在切换到交互式运行时后保留。请使用技能中的模式来传递状态。
.ClientProgram.cssupport-prerendering[PersistentState]Don'ts
注意事项
- Don't use a singleton for per-user state on Server — all circuits share it, leaking state between users.
- Don't put all app state into one cascaded object — re-renders ALL subscribers on every change. Separate concerns into distinct types (
NotifyChangedAsync,ThemeState,CartState).UserPreferences - Don't forget to unsubscribe — omitting on event subscriptions causes memory leaks that grow per-circuit.
Dispose - Don't use in a static layout expecting it to reach interactive children — it won't cross render mode boundaries. Use DI-registered
<CascadingValue>or scoped services.CascadingValueSource<T> - Don't use to propagate cascading value changes — this destroys the circuit and forces a full page reload. Instead, inject
NavigationManager.Refresh(forceReload: true)and callCascadingValueSource<T>to push updates to allNotifyChangedAsync(newValue)subscribers without a page reload.[CascadingParameter] - Don't call from a non-Blazor thread — wrap in
StateHasChanged. The framework throwsInvokeAsync.InvalidOperationException: The current thread is not associated with the Dispatcher
- 不要在Server模式下使用单例存储每个用户的状态 — 所有电路共享单例,会导致用户间状态泄露。
- 不要将所有应用状态放入单个级联对象中 — 会在每次变更时重新渲染所有订阅者。应将关注点分离为不同的类型(如
NotifyChangedAsync、ThemeState、CartState)。UserPreferences - 不要忘记取消订阅 — 省略中的事件订阅会导致内存泄漏,且泄漏量会随电路数量增长。
Dispose - 不要在静态布局中使用并期望它能传递到交互式子组件 — 它无法跨渲染模式边界。请使用DI注册的
<CascadingValue>或作用域服务。CascadingValueSource<T> - 不要使用来传播级联值变更 — 这会销毁电路并强制全页面刷新。相反,应注入
NavigationManager.Refresh(forceReload: true)并调用CascadingValueSource<T>,无需页面刷新即可将更新推送给所有NotifyChangedAsync(newValue)订阅者。[CascadingParameter] - 不要从非Blazor线程调用— 请将调用包裹在
StateHasChanged中。否则框架会抛出InvokeAsync异常。InvalidOperationException: The current thread is not associated with the Dispatcher