关于 dotnet-blazor-components
面向 Blazor 开发者的组件实现技能,涵盖生命周期、状态管理、JavaScript 互操作、表单验证和 QuickGrid。内容还说明不同渲染模式下的行为差异,适用于构建和维护 Blazor 组件。
主要功能
- 01掌握组件生命周期与渲染模式差异
- 02管理级联值、依赖注入和浏览器存储
- 03实现 JavaScript 互操作与模块导入
- 04配置 EditForm 与数据注解验证
- 05使用 QuickGrid 排序、筛选、分页和虚拟化
- 06了解 .NET 11 预览组件功能
适用场景
- 01排查静态 SSR 下生命周期回调行为
- 02在组件间共享状态与服务
- 03连接 JavaScript 模块和 .NET 回调
- 04为表单添加验证并呈现分页数据
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-blazor-components安装后在 Claude Code 中输入/dotnet-blazor-components确认已加载。
详细介绍
dotnet-blazor-components
Blazor 组件架构:生命周期方法、状态管理(级联值、DI、浏览器存储)、JavaScript 互操作(兼容 AOT)、EditForm 验证和 QuickGrid。必要时说明不同渲染模式下的行为差异。
范围边界: 本技能负责组件实现模式。托管模型选择和渲染模式配置由 [skill:dotnet-blazor-patterns] 负责。身份验证组件(AuthorizeView、CascadingAuthenticationState)由 [skill:dotnet-blazor-auth] 负责。
不在范围内: bUnit 测试——参见 [skill:dotnet-blazor-testing]。独立 SignalR 中心模式——参见 [skill:dotnet-realtime-communication]。E2E 测试——参见 [skill:dotnet-playwright]。UI 框架选择——参见 [skill:dotnet-ui-chooser]。
交叉引用:[skill:dotnet-blazor-patterns](托管模型和渲染模式)、[skill:dotnet-blazor-auth](身份验证)、[skill:dotnet-blazor-testing](bUnit 测试)、[skill:dotnet-realtime-communication](独立 SignalR)、[skill:dotnet-playwright](E2E 测试)、[skill:dotnet-ui-chooser](框架选择)、[skill:dotnet-accessibility](无障碍模式,包括 ARIA、键盘导航和屏幕阅读器)。
组件生命周期
生命周期方法
@code {
// 1. Called when parameters are set/updated
public override async Task SetParametersAsync(ParameterView parameters)
{
// Access raw parameters before they are applied
await base.SetParametersAsync(parameters);
}
// 2. Called after parameters are assigned (sync)
protected override void OnInitialized()
{
// One-time initialization (runs once per component instance)
}
// 3. Called after parameters are assigned (async)
protected override async Task OnInitializedAsync()
{
// Async initialization (data fetching, service calls)
products = await ProductService.GetProductsAsync();
}
// 4. Called every time parameters change
protected override void OnParametersSet()
{
// React to parameter changes
}
// 5. Called after each render
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
// JS interop safe here -- DOM is available
}
}
// 6. Async version of OnAfterRender
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JSRuntime.InvokeVoidAsync("initializeChart", chartElement);
}
}
// 7. Cleanup
public void Dispose()
{
// Unsubscribe from events, dispose resources
}
// 8. Async cleanup
public async ValueTask DisposeAsync()
{
// Async cleanup (dispose JS object references)
if (module is not null)
{
await module.DisposeAsync();
}
}
}
各渲染模式下的生命周期行为
| 生命周期事件 | Static SSR | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |
|---|---|---|---|---|---|
OnInitialized(Async) | 在服务器上运行 | 在服务器上运行 | 在浏览器中运行 | 首次加载时在服务器上运行,WASM 缓存后在浏览器中运行 | 在进程内运行 |
OnAfterRender(Async) | 从不调用 | SignalR 确认渲染后在服务器上运行 | DOM 更新后在浏览器中运行 | 先在服务器端运行,再在浏览器端运行(与当前运行时一致) | WebView 渲染后运行 |
Dispose(Async) | 响应返回后调用 | circuit 结束时调用 | 组件移除时调用 | circuit 结束时(Server 阶段)或组件移除时(WASM 阶段)调用 | 组件移除时调用 |
注意: 在 Static SSR 中,由于没有持久连接,OnAfterRender 永远不会执行。不要在 Static SSR 页面中将关键逻辑放在 OnAfterRender 里。
状态管理
级联值
级联值可以在组件树中向下传递数据,而无需显式传递参数。
<CascadingValue Value="@theme" Name="AppTheme">
<Router AppAssembly="typeof(App).Assembly">
</Router>
</CascadingValue>
@code {
private ThemeSettings theme = new() { IsDarkMode = false, AccentColor = "#0078d4" };
}
@code {
[CascadingParameter(Name = "AppTheme")]
public ThemeSettings? Theme { get; set; }
}
固定级联值(.NET 8+): 对于初始渲染后永不改变的值,请使用 IsFixed="true",以避免额外的重新渲染开销:
<CascadingValue Value="@config" IsFixed="true">
<ChildComponent />
</CascadingValue>
依赖注入
// Register services in Program.cs
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddSingleton<AppState>();
// Inject in components
@inject IProductService ProductService
@inject AppState State
各渲染模式下的 DI 生命周期行为:
| 生命周期 | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |
|---|---|---|---|---|
| Singleton | 在服务器上的所有 circuit 之间共享 | 每个浏览器标签页一个 | Server 阶段在服务器间共享;切换到 WASM 后每个标签页一个 | 每个应用实例一个 |
| Scoped | 每个 circuit 一个(相当于每位用户一个) | 每个浏览器标签页一个(与 Singleton 相同) | 每个 circuit 一个(Server 阶段),每个标签页一个(WASM 阶段)——阶段之间不会传递状态 | 每个应用实例一个(与 Singleton 相同) |
| Transient | 每次注入时创建新实例 | 每次注入时创建新实例 | 每次注入时创建新实例 | 每次注入时创建新实例 |
注意: 在 Blazor Server 中,Scoped 服务的生命周期持续整个 circuit(而非像 MVC 那样按请求划分)。circuit 会一直存在,直到用户离开页面或连接断开。长期存活的 scoped 服务可能会累积状态——请使用 OwningComponentBase<T> 实现组件范围内的 DI。
浏览器存储
// ProtectedBrowserStorage -- encrypted, per-user storage
// Available in InteractiveServer only (not WASM -- server encrypts/decrypts)
@inject ProtectedSessionStorage SessionStorage
@inject ProtectedLocalStorage LocalStorage
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Session storage (cleared when tab closes)
await SessionStorage.SetAsync("cart", cartItems);
var result = await SessionStorage.GetAsync<List<CartItem>>("cart");
if (result.Success) { cartItems = result.Value!; }
// Local storage (persists across sessions)
await LocalStorage.SetAsync("preferences", userPrefs);
}
}
对于 InteractiveWebAssembly,可使用 JS 互操作直接访问浏览器存储:
// WASM: Direct browser storage via JS interop
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "key",
JsonSerializer.Serialize(value, AppJsonContext.Default.UserPrefs));
var json = await JSRuntime.InvokeAsync<string?>("localStorage.getItem", "key");
if (json is not null)
{
value = JsonSerializer.Deserialize(json, AppJsonContext.Default.UserPrefs);
}
注意: 预渲染期间无法使用 ProtectedBrowserStorage。请始终在 OnAfterRenderAsync(firstRender: true) 中访问它,切勿在 OnInitializedAsync 中访问。
JavaScript 互操作
从 .NET 调用 JavaScript
@inject IJSRuntime JSRuntime
// Invoke a global JS function
await JSRuntime.InvokeVoidAsync("console.log", "Hello from Blazor");
// Invoke and get a return value
var width = await JSRuntime.InvokeAsync<int>("getWindowWidth");
// With timeout (important for Server to avoid hanging circuits)
var result = await JSRuntime.InvokeAsync<string>(
"expensiveOperation",
TimeSpan.FromSeconds(10),
inputData);
JavaScript 模块导入(兼容 AOT)
// Import a JS module -- trim-safe, no reflection
private IJSObjectReference? module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await JSRuntime.InvokeAsync<IJSObjectReference>(
"import", "./js/interop.js");
await module.InvokeVoidAsync("initialize", elementRef);
}
}
// Always dispose module references
public async ValueTask DisposeAsync()
{
if (module is not null)
{
await module.DisposeAsync();
}
}
// wwwroot/js/interop.js
export function initialize(element) {
// Set up the element
}
export function getValue(element) {
return element.value;
}
从 JavaScript 调用 .NET
// Instance method callback
private DotNetObjectReference<MyComponent>? dotNetRef;
protected override void OnInitialized()
{
dotNetRef = DotNetObjectReference.Create(this);
}
[JSInvokable]
public void OnJsEvent(string data)
{
message = data;
StateHasChanged();
}
public void Dispose()
{
dotNetRef?.Dispose();
}
// Call .NET from JS
export function registerCallback(dotNetRef) {
document.addEventListener('custom-event', (e) => {
dotNetRef.invokeMethodAsync('OnJsEvent', e.detail);
});
}
各渲染模式下的 JS 互操作
| 关注点 | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |
|---|---|---|---|---|
| JS 调用时机 | SignalR 确认渲染后 | WASM 运行时加载后 | 初始通过 SignalR,切换到 WASM 后直接调用 | WebView 加载后 |
OnAfterRender 是否可用 | 是 | 是 | 是 | 是 |
| IJSRuntime 同步调用 | 不支持(仅异步) | 可用 IJSInProcessRuntime | Server 阶段仅支持异步;切换到 WASM 后可用 IJSInProcessRuntime | 可用 IJSInProcessRuntime |
| 模块导入 | 通过 SignalR(有延迟) | 直接导入(速度快) | SignalR(Server 阶段),直接导入(WASM 阶段) | 直接导入(速度快) |
注意: 在 InteractiveServer 中,所有 JS 互操作调用都通过 SignalR 传输,会增加网络延迟。请将多个操作批量处理为一次 JS 函数调用,以尽量减少往返通信。
EditForm 验证
使用数据注释的基本 EditForm
<EditForm Model="product" OnValidSubmit="HandleSubmit" FormName="product-form">
<DataAnnotationsValidator />
<ValidationSummary />
<div>
<label for="name">Name:</label>
<InputText id="name" @bind-Value="product.Name" />
<ValidationMessage For="() => product.Name" />
</div>
<div>
<label for="price">Price:</label>
<InputNumber id="price" @bind-Value="product.Price" />
<ValidationMessage For="() => product.Price" />
</div>
<div>
<label for="category">Category:</label>
<InputSelect id="category" @bind-Value="product.Category">
<option value="">Select...</option>
<option value="Electronics">Electronics</option>
<option value="Clothing">Clothing</option>
</InputSelect>
<ValidationMessage For="() => product.Category" />
</div>
<button type="submit">Save</button>
</EditForm>
@code {
private ProductModel product = new();
private async Task HandleSubmit()
{
await ProductService.CreateAsync(product);
Navigation.NavigateTo("/products");
}
}
带有验证特性的模型
public sealed class ProductModel
{
[Required(ErrorMessage = "Product name is required")]
[StringLength(200, MinimumLength = 1)]
public string Name { get; set; } = "";
[Range(0.01, 1_000_000, ErrorMessage = "Price must be between {1} and {2}")]
public decimal Price { get; set; }
[Required(ErrorMessage = "Category is required")]
public string Category { get; set; } = "";
}
使用增强型表单处理的 EditForm(.NET 8+)
静态 SSR 表单需要 FormName,并使用 [SupplyParameterFromForm]:
@page "/products/create"
<EditForm Model="product" OnValidSubmit="HandleSubmit" FormName="create-product" Enhance>
<DataAnnotationsValidator />
<button type="submit">Create</button>
</EditForm>
@code {
[SupplyParameterFromForm]
private ProductModel product { get; set; } = new();
private async Task HandleSubmit()
{
await ProductService.CreateAsync(product);
Navigation.NavigateTo("/products");
}
}
Enhance 特性可启用增强型表单处理——表单通过 fetch 提交,并对 DOM 进行局部更新,无需完整重新加载页面。
注意: FormName 在页面上的所有表单中必须唯一。重复的 FormName 值会导致表单提交不明确错误。
QuickGrid
QuickGrid 是内置于 Blazor(.NET 8+)中的高性能网格组件。它支持排序、筛选、分页和虚拟化。
基本 QuickGrid
@using Microsoft.AspNetCore.Components.QuickGrid
<QuickGrid Items="products">
<PropertyColumn Property="p => p.Name" Sortable="true" />
<PropertyColumn Property="p => p.Price" Format="C2" Sortable="true" />
<PropertyColumn Property="p => p.Category" Sortable="true" />
<TemplateColumn Title="Actions">
<button @onclick="() => Edit(context)">Edit</button>
</TemplateColumn>
</QuickGrid>
@code {
private IQueryable<Product> products = Enumerable.Empty<Product>().AsQueryable();
protected override async Task OnInitializedAsync()
{
var list = await ProductService.GetAllAsync();
products = list.AsQueryable();
}
private void Edit(Product product) => Navigation.NavigateTo($"/products/{product.Id}/edit");
}
带分页的 QuickGrid
<QuickGrid Items="products" Pagination="pagination">
<PropertyColumn Property="p => p.Name" Sortable="true" />
<PropertyColumn Property="p => p.Price" Format="C2" />
</QuickGrid>
<Paginator State="pagination" />
@code {
private PaginationState pagination = new() { ItemsPerPage = 20 };
private IQueryable<Product> products = default!;
}
带虚拟化的 QuickGrid
对于大型数据集,虚拟化只会渲染可见行:
<QuickGrid Items="products" Virtualize="true" ItemSize="50">
<PropertyColumn Property="p => p.Name" />
<PropertyColumn Property="p => p.Price" Format="C2" />
</QuickGrid>
QuickGrid OnRowClick(.NET 11 Preview)
.NET 11 为 QuickGrid 添加了 OnRowClick,无需模板列即可处理行级点击:
<QuickGrid Items="products" OnRowClick="HandleRowClick">
<PropertyColumn Property="p => p.Name" />
<PropertyColumn Property="p => p.Price" Format="C2" />
</QuickGrid>
@code {
private void HandleRowClick(GridRowClickEventArgs<Product> args)
{
Navigation.NavigateTo($"/products/{args.Item.Id}");
}
}
备用方案(net10.0): 使用带有点击处理程序的 TemplateColumn,或将每一行包装在可点击元素中。
来源:ASP.NET Core .NET 11 Preview - QuickGrid enhancements
.NET 11 Preview 功能
EnvironmentBoundary 组件
EnvironmentBoundary 会根据托管环境(Development、Staging、Production)有条件地渲染内容:
<EnvironmentBoundary Include="Development">
<p>Debug panel -- only visible in Development</p>
<DebugToolbar />
</EnvironmentBoundary>
<EnvironmentBoundary Exclude="Production">
<p>Testing controls -- hidden in Production</p>
</EnvironmentBoundary>
备用方案(net10.0): 注入 IWebHostEnvironment,并在 @code 中使用条件渲染。
来源:ASP.NET Core .NET 11 Preview - EnvironmentBoundary
Label 和 DisplayName 支持
.NET 11 为输入组件添加了 [DisplayName] 支持,可自动生成 <label> 元素:
<EditForm Model="model" FormName="contact">
<InputText @bind-Value="model.FullName" />
<InputText @bind-Value="model.EmailAddress" />
</EditForm>
@code {
private ContactModel model = new();
}
// Model
public sealed class ContactModel
{
[DisplayName("Full Name")]
[Required]
public string FullName { get; set; } = "";
[DisplayName("Email Address")]
[EmailAddress]
public string EmailAddress { get; set; } = "";
}
备用方案(net10.0): 手动添加显式的 <label for="..."> 元素。
来源:ASP.NET Core .NET 11 Preview - Label/DisplayName
WebAssembly 中的 IHostedService
.NET 11 允许 IHostedService 实现运行于 Blazor WebAssembly 中,从而在浏览器中执行后台任务:
// Register in WASM Program.cs
builder.Services.AddHostedService<DataSyncService>();
public sealed class DataSyncService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await SyncDataFromServer();
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
}
备用方案(net10.0): 在组件中使用 Timer,或注入一个在首次使用时启动后台工作的单例服务。
来源:ASP.NET Core .NET 11 Preview - IHostedService in WASM
SignalR ConfigureConnection
.NET 11 为 Blazor Server circuit hub 添加了 ConfigureConnection,可自定义 SignalR 连接(例如,添加自定义标头、配置重新连接):
// Program.cs
app.MapBlazorHub(options =>
{
options.ConfigureConnection = connection =>
{
connection.Metadata["tenant"] = "default";
};
});
备用方案(net10.0): 使用 IHubFilter 或中间件,在 hub 层检查或修改连接。
来源:ASP.NET Core .NET 11 Preview - SignalR ConfigureConnection
Agent 易错点
- 不要在
OnInitializedAsync中调用 JS 互操作。 此时 DOM 尚不可用。对于需要 DOM 元素的 JS 调用,请使用OnAfterRenderAsync(firstRender: true)。 - 外部状态发生变化后,不要忘记调用
StateHasChanged()。 当状态在非 Blazor 上下文中变化(例如计时器、事件处理程序、JS 回调)时,请调用StateHasChanged()或InvokeAsync(StateHasChanged)以触发重新渲染。 - 预渲染期间不要使用
ProtectedBrowserStorage。 由于此时尚不存在交互式 circuit,使用它会引发异常。请仅在OnAfterRenderAsync中访问它。 - Static SSR 表单不要忘记设置
FormName。 否则,Static SSR 模式下的表单提交不会路由到正确的处理程序。 - JS 尚未用完
DotNetObjectReference时,不要将其释放。 过早释放会导致 JavaScript 尝试调用回调时引发JSException。请在Dispose()或DisposeAsync()中释放。 - 不要假设 Blazor Server 中的 Scoped 服务是按请求创建的。 Scoped 服务的生命周期贯穿整个 circuit。需要组件级服务生命周期时,请使用
OwningComponentBase<T>。
先决条件
- .NET 8.0+(QuickGrid、增强型表单处理、带有
IsFixed的级联值) - QuickGrid 所需的
Microsoft.AspNetCore.Components.QuickGrid包 - 用于 EnvironmentBoundary、Label/DisplayName、QuickGrid OnRowClick、WASM 中的 IHostedService 的 .NET 11 预览版
知识来源
此技能中的 Blazor 组件模式基于以下指南:
- Damian Edwards —— Razor 和 Blazor 组件设计模式、渲染模式架构以及性能最佳实践。ASP.NET 团队首席架构师。
这些来源为上述模式及其依据提供参考。此技能并不声称代表或代言任何个人。