coordinate-components

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Coordinate Components

组件协同

Step 1 — Read AGENTS.md

步骤1 — 阅读AGENTS.md

Read
AGENTS.md
at the workspace root to learn the project's conventions before making changes.
在进行修改前,请阅读工作区根目录下的
AGENTS.md
,了解项目约定。

Step 2 — Decide the scope

步骤2 — 确定作用范围

NeedMechanismWhen to use
Subtree (same render mode)
CascadingValue
component
Theme, layout config within a layout
App-wide (all render modes)
CascadingValueSource<T>
via DI
Current user, feature flags, theme shared globally
Mutable shared state within a circuitScoped service +
Action
event
Shopping cart, notification count, selected filters
For parent→child one level: use
[Parameter]
/
EventCallback
(see
author-component
skill). For persisting state across prerender→interactive: see
support-prerendering
skill.
需求实现机制使用场景
子树(同一渲染模式)
CascadingValue
组件
布局内的主题、布局配置
全局范围(所有渲染模式)通过DI注入的
CascadingValueSource<T>
当前用户、功能标志、全局共享主题
电路内的可变共享状态作用域服务 +
Action
事件
购物车、通知计数、选中的筛选器
对于父→子单层级传递:使用
[Parameter]
/
EventCallback
(参考
author-component
技能)。 对于跨预渲染→交互式的状态持久化:参考
support-prerendering
技能。

Workflow (quick reference)

工作流程(快速参考)

  1. Choose the mechanism from the table in Step 2
  2. If crossing render mode boundaries → use
    CascadingValueSource<T>
    (Step 4)
  3. Register in
    Program.cs
    with
    AddCascadingValue(...)
    and
    isFixed: false
  4. Consume via
    [CascadingParameter]
    in child components
  5. Update via
    NotifyChangedAsync(newValue)
    — never page reload
  6. For additional mutable state within a circuit → add scoped service (Step 5)
  7. Wrap any
    StateHasChanged
    from background threads in
    InvokeAsync
  8. Implement
    IDisposable
    — dispose timers, cancel tokens, unsubscribe events
  1. 从步骤2的表格中选择合适的实现机制
  2. 若需跨渲染模式边界 → 使用
    CascadingValueSource<T>
    (步骤4)
  3. Program.cs
    中通过
    AddCascadingValue(...)
    注册,并设置
    isFixed: false
  4. 在子组件中通过
    [CascadingParameter]
    消费
  5. 通过
    NotifyChangedAsync(newValue)
    更新状态 — 绝不要刷新页面
  6. 若需电路内额外的可变状态 → 添加作用域服务(步骤5)
  7. 将后台线程中的任何
    StateHasChanged
    调用包裹在
    InvokeAsync
  8. 实现
    IDisposable
    接口 — 释放定时器、取消令牌、取消事件订阅

Step 3 — CascadingValue for subtree state

步骤3 — 用于子树状态的CascadingValue

Wrap a subtree with
<CascadingValue>
to flow data to all descendants without passing it through every intermediate component.
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
    Name
    :
    razor
    <CascadingValue Value="primary" Name="PrimaryTheme">...</CascadingValue>
    csharp
    [CascadingParameter(Name = "PrimaryTheme")]
    private ThemeInfo? Primary { get; set; }
  • Set
    IsFixed="true"
    when the value never changes — avoids subscription overhead.
  • Does NOT cross render mode boundaries. A
    <CascadingValue>
    in a static SSR parent is invisible to interactive children. See Step 6.
使用
<CascadingValue>
包裹子树,无需通过每个中间组件传递数据,即可将数据传递给所有后代组件。
razor
@* 在布局或父组件中 *@
<CascadingValue Value="theme">
    @Body
</CascadingValue>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-primary" };
}
在任意后代组件中消费:
csharp
[CascadingParameter]
private ThemeInfo? Theme { get; set; }
规则:
  • 类型匹配,而非名称。若要级联多个同类型的值,请添加
    Name
    属性:
    razor
    <CascadingValue Value="primary" Name="PrimaryTheme">...</CascadingValue>
    csharp
    [CascadingParameter(Name = "PrimaryTheme")]
    private ThemeInfo? Primary { get; set; }
  • 当值永远不会改变时,设置
    IsFixed="true"
    — 可避免订阅开销。
  • 无法跨渲染模式边界。静态SSR父组件中的
    <CascadingValue>
    对交互式子组件不可见。请参考步骤6。

Step 4 — CascadingValueSource<T> for app-wide state

步骤4 — 用于全局状态的CascadingValueSource<T>

Register a
CascadingValueSource<T>
in DI when the value must be available to all components regardless of render mode.
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()
(no argument) also works — mutate the object and then call it.
NotifyChangedAsync(newValue)
replaces the value and notifies in one step.
Update protocol: Whenever shared state changes, the component that changes it MUST inject
CascadingValueSource<T>
and call
NotifyChangedAsync()
. This is the only mechanism that triggers re-rendering in all
[CascadingParameter]
subscribers. Without this call, no subscribers update. Do not use
NavigationManager.Refresh()
or page reloads as a substitute.
Rules:
  • isFixed: false
    enables change notifications.
    isFixed: true
    is better for truly static values (feature flags).
  • Crosses render mode boundaries — works for per-page interactivity, global interactivity, and WebAssembly. Key advantage over
    <CascadingValue>
    .
  • Keep cascaded types granular. Every
    NotifyChangedAsync
    re-renders ALL subscribers regardless of which property changed. Don't put all app state into one cascaded type.
  • For Auto/WebAssembly apps, register in both server and
    .Client
    Program.cs
    . The type must be in a shared assembly.
当值需要对**所有组件(无论渲染模式如何)**可用时,在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
Action OnChange
pattern works when the event fires from the Blazor sync context (button click →
Cart.Add(…)
). If the event fires from outside the sync context (timer, background task, SignalR hub), wrap in
InvokeAsync
:
csharp
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同步上下文触发(如按钮点击→
Cart.Add(…)
)时,简单的
Action OnChange
模式即可生效。若事件从同步上下文外部触发(如定时器、后台任务、SignalR集线器),请将调用包裹在
InvokeAsync
中:
csharp
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
<CascadingValue>
placed in a static SSR layout (
MainLayout.razor
when the layout renders statically) will not reach interactive children. The interactive component sees
null
for the cascading parameter.
Fix: Use
CascadingValueSource<T>
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.
放置在静态SSR布局(当布局以静态方式渲染时的
MainLayout.razor
)中的
<CascadingValue>
无法传递到交互式子组件。交互式组件会将级联参数视为
null
解决方法: 使用DI注册的
CascadingValueSource<T>
(步骤4)或作用域服务(步骤5)。两者均可跨边界,因为DI服务是按电路解析的,而非从组件树解析。

Service lifetime on Server vs WebAssembly

Server与WebAssembly的服务生命周期对比

LifetimeServerWebAssembly
ScopedPer circuit (per user connection)Per browser tab
SingletonShared across ALL usersPer browser tab (safe)
TransientNew instance per injectionNew 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.
生命周期ServerWebAssembly
作用域(Scoped)每个电路(每个用户连接)每个浏览器标签页
单例(Singleton)所有用户共享每个浏览器标签页(安全)
瞬时(Transient)每次注入生成新实例每次注入生成新实例
在Server模式下,绝不要在单例中存储用户特定状态 — 所有用户的电路共享同一个单例,会导致用户间的状态泄露。请使用
AddScoped<T>()
在WebAssembly模式下,单例是按标签页隔离的,因此是安全的。但同时适用于Server和WebAssembly(Auto模式)的代码必须使用作用域服务。

Auto/WebAssembly with prerendering

带预渲染的Auto/WebAssembly模式

State services must be defined in the
.Client
project or a shared assembly — they cannot reference server-only types. Register the service in both
Program.cs
files. State created during prerender does not survive the switch to the interactive runtime. Use the
support-prerendering
skill's
[PersistentState]
pattern to carry state across.
状态服务必须定义在
.Client
项目或共享程序集中 — 它们不能引用仅服务器端的类型。需在两个
Program.cs
文件中注册服务。预渲染期间创建的状态无法在切换到交互式运行时后保留。请使用
support-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
    NotifyChangedAsync
    re-renders ALL subscribers on every change. Separate concerns into distinct types (
    ThemeState
    ,
    CartState
    ,
    UserPreferences
    ).
  • Don't forget to unsubscribe — omitting
    Dispose
    on event subscriptions causes memory leaks that grow per-circuit.
  • Don't use
    <CascadingValue>
    in a static layout expecting it to reach interactive children
    — it won't cross render mode boundaries. Use DI-registered
    CascadingValueSource<T>
    or scoped services.
  • Don't use
    NavigationManager.Refresh(forceReload: true)
    to propagate cascading value changes
    — this destroys the circuit and forces a full page reload. Instead, inject
    CascadingValueSource<T>
    and call
    NotifyChangedAsync(newValue)
    to push updates to all
    [CascadingParameter]
    subscribers without a page reload.
  • Don't call
    StateHasChanged
    from a non-Blazor thread
    — wrap in
    InvokeAsync
    . The framework throws
    InvalidOperationException: The current thread is not associated with the Dispatcher
    .
  • 不要在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
    异常。