name: dotnet-blazor-components description: “实现Blazor组件。生命周期、状态管理、JS互操作、EditForm、QuickGrid。” user-invocable: false
dotnet-blazor-components
Blazor组件架构:生命周期方法、状态管理(级联值、依赖注入、浏览器存储)、JavaScript互操作(AOT安全)、EditForm验证和QuickGrid。涵盖相关渲染模式的行为差异。
范围
- 组件生命周期方法(SetParametersAsync, OnInitialized, OnAfterRender)
- 状态管理(级联值、依赖注入、浏览器存储)
- JavaScript互操作(AOT安全模块导入)
- EditForm验证和输入组件
- QuickGrid数据绑定和虚拟化
- 每渲染模式行为差异(Static SSR, InteractiveServer, WASM)
超出范围
- 托管模型选择和渲染模式 – 参见 [skill:dotnet-blazor-patterns]
- 认证组件(AuthorizeView, CascadingAuthenticationState) – 参见 [skill:dotnet-blazor-auth]
- bUnit测试 – 参见 [skill:dotnet-blazor-testing]
- 独立SignalR hub模式 – 参见 [skill:dotnet-realtime-communication]
- 端到端测试 – 参见 [skill:dotnet-playwright]
- UI框架选择 – 参见 [skill:dotnet-ui-chooser]
- 可访问性模式(ARIA, 键盘导航) – 参见 [skill:dotnet-accessibility]
交叉引用: [skill:dotnet-blazor-patterns] 用于托管模型和渲染模式, [skill:dotnet-blazor-auth] 用于认证, [skill:dotnet-blazor-testing] 用于bUnit测试, [skill:dotnet-realtime-communication] 用于独立SignalR, [skill:dotnet-playwright] 用于端到端测试, [skill:dotnet-ui-chooser] 用于框架选择, [skill:dotnet-accessibility] 用于可访问性模式(ARIA, 键盘导航, 屏幕阅读器)。
组件生命周期
生命周期方法
@code {
// 1. 当参数被设置或更新时调用
public override async Task SetParametersAsync(ParameterView parameters)
{
// 在应用参数之前访问原始参数
await base.SetParametersAsync(parameters);
}
// 2. 参数分配后调用(同步)
protected override void OnInitialized()
{
// 一次性初始化(每个组件实例运行一次)
}
// 3. 参数分配后调用(异步)
protected override async Task OnInitializedAsync()
{
// 异步初始化(数据获取、服务调用)
products = await ProductService.GetProductsAsync();
}
// 4. 每次参数更改时调用
protected override void OnParametersSet()
{
// 响应参数变化
}
// 5. 每次渲染后调用
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
// JS互操作安全在这里 -- DOM可用
}
}
// 6. OnAfterRender的异步版本
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JSRuntime.InvokeVoidAsync("initializeChart", chartElement);
}
}
// 7. 清理
public void Dispose()
{
// 取消事件订阅,释放资源
}
// 8. 异步清理
public async ValueTask DisposeAsync()
{
// 异步清理(释放JS对象引用)
if (module is not null)
{
await module.DisposeAsync();
}
}
}
每渲染模式生命周期行为
| 生命周期事件 | Static SSR | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |
|---|---|---|---|---|---|
OnInitialized(Async) |
在服务器上运行 | 在服务器上运行 | 在浏览器中运行 | 首次加载在服务器上,WASM缓存后在浏览器中 | 在进程中运行 |
OnAfterRender(Async) |
从不调用 | 服务器在SignalR确认渲染后运行 | 浏览器在DOM更新后运行 | 服务器端然后浏览器端(匹配活动运行时) | 在WebView渲染后运行 |
Dispose(Async) |
响应后调用 | 电路结束时调用 | 组件移除时调用 | 电路结束时(服务器阶段)或移除时(WASM阶段)调用 | 组件移除时调用 |
注意: 在Static SSR中,OnAfterRender 从不执行,因为没有持久连接。不要在Static SSR页面的 OnAfterRender 中放置关键逻辑。
状态管理
级联值
级联值在不显式传递参数的情况下,将数据沿组件树向下流动。
<!-- 父组件:提供级联值 -->
<CascadingValue Value="@theme" Name="AppTheme">
<Router AppAssembly="typeof(App).Assembly">
<!-- 所有后代组件可以接收AppTheme -->
</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>
依赖注入
// 在Program.cs中注册服务
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddSingleton<AppState>();
// 在组件中注入
@inject IProductService ProductService
@inject AppState State
每渲染模式DI生命周期行为:
| 生命周期 | InteractiveServer | InteractiveWebAssembly | InteractiveAuto | Hybrid |
|---|---|---|---|---|
| 单例 | 服务器上所有电路共享 | 每个浏览器标签一个 | 服务器阶段共享;WASM切换后每标签 | 每个应用实例一个 |
| 作用域 | 每个电路一个(类似每用户) | 每个浏览器标签一个(与单例相同) | 每电路(服务器阶段),每标签(WASM阶段) – 状态不在阶段间传输 | 每个应用实例一个(与单例相同) |
| 瞬态 | 每次注入新实例 | 每次注入新实例 | 每次注入新实例 | 每次注入新实例 |
注意: 在Blazor Server中,Scoped 服务在整个电路持续时间内存活(不像MVC中每请求)。电路持续到用户导航离开或连接断开。长期作用域服务可能积累状态 – 使用 OwningComponentBase<T> 用于组件作用域DI。
浏览器存储
// ProtectedBrowserStorage -- 加密的每用户存储
// 仅在InteractiveServer中可用(不是WASM -- 服务器加密/解密)
@inject ProtectedSessionStorage SessionStorage
@inject ProtectedLocalStorage LocalStorage
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// 会话存储(标签关闭时清除)
await SessionStorage.SetAsync("cart", cartItems);
var result = await SessionStorage.GetAsync<List<CartItem>>("cart");
if (result.Success) { cartItems = result.Value!; }
// 本地存储(跨会话持久)
await LocalStorage.SetAsync("preferences", userPrefs);
}
}
对于InteractiveWebAssembly,使用JS互操作直接访问浏览器存储:
// WASM:通过JS互操作直接浏览器存储
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
// 调用全局JS函数
await JSRuntime.InvokeVoidAsync("console.log", "Hello from Blazor");
// 调用并获取返回值
var width = await JSRuntime.InvokeAsync<int>("getWindowWidth");
// 带超时(对于Server很重要,避免挂起电路)
var result = await JSRuntime.InvokeAsync<string>(
"expensiveOperation",
TimeSpan.FromSeconds(10),
inputData);
JavaScript模块导入(AOT安全)
// 导入JS模块 -- 裁剪安全,无反射
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);
}
}
// 总是释放模块引用
public async ValueTask DisposeAsync()
{
if (module is not null)
{
await module.DisposeAsync();
}
}
// wwwroot/js/interop.js
export function initialize(element) {
// 设置元素
}
export function getValue(element) {
return element.value;
}
从JavaScript调用.NET
// 实例方法回调
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();
}
// 从JS调用.NET
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 可用 |
服务器阶段仅异步;WASM切换后 IJSInProcessRuntime 可用 |
IJSInProcessRuntime 可用 |
| 模块导入 | 通过SignalR(延迟) | 直接(快速) | SignalR(服务器阶段),直接(WASM阶段) | 直接(快速) |
注意: 在InteractiveServer中,所有JS互操作调用通过SignalR传输,增加网络延迟。通过将操作批处理到单个JS函数调用中来最小化往返次数。
EditForm验证
基本EditForm与数据注解
<EditForm Model="product" OnValidSubmit="HandleSubmit" FormName="product-form">
<DataAnnotationsValidator />
<ValidationSummary />
<div>
<label for="name">名称:</label>
<InputText id="name" @bind-Value="product.Name" />
<ValidationMessage For="() => product.Name" />
</div>
<div>
<label for="price">价格:</label>
<InputNumber id="price" @bind-Value="product.Price" />
<ValidationMessage For="() => product.Price" />
</div>
<div>
<label for="category">类别:</label>
<InputSelect id="category" @bind-Value="product.Category">
<option value="">选择...</option>
<option value="Electronics">电子产品</option>
<option value="Clothing">服装</option>
</InputSelect>
<ValidationMessage For="() => product.Category" />
</div>
<button type="submit">保存</button>
</EditForm>
@code {
private ProductModel product = new();
private async Task HandleSubmit()
{
await ProductService.CreateAsync(product);
Navigation.NavigateTo("/products");
}
}
带验证属性的模型
public sealed class ProductModel
{
[Required(ErrorMessage = "产品名称是必需的")]
[StringLength(200, MinimumLength = 1)]
public string Name { get; set; } = "";
[Range(0.01, 1_000_000, ErrorMessage = "价格必须在{1}和{2}之间")]
public decimal Price { get; set; }
[Required(ErrorMessage = "类别是必需的")]
public string Category { get; set; } = "";
}
EditForm与增强表单处理(.NET 8+)
Static SSR表单需要 FormName 并使用 [SupplyParameterFromForm]:
@page "/products/create"
<EditForm Model="product" OnValidSubmit="HandleSubmit" FormName="create-product" Enhance>
<DataAnnotationsValidator />
<!-- 表单字段 -->
<button type="submit">创建</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="操作">
<button @onclick="() => Edit(context)">编辑</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>
<!-- net11-preview -->
QuickGrid OnRowClick(.NET 11预览)
.NET 11添加了 OnRowClick 到QuickGrid,用于行级点击处理,无需模板列:
<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
<!-- net11-preview -->
.NET 11预览功能
EnvironmentBoundary组件
EnvironmentBoundary 根据托管环境(开发、暂存、生产)条件渲染内容:
<EnvironmentBoundary Include="Development">
<p>调试面板 -- 仅在开发环境中可见</p>
<DebugToolbar />
</EnvironmentBoundary>
<EnvironmentBoundary Exclude="Production">
<p>测试控件 -- 在生产环境中隐藏</p>
</EnvironmentBoundary>
回退(net10.0): 注入 IWebHostEnvironment 并在 @code 中使用条件渲染。
来源: ASP.NET Core .NET 11 Preview - EnvironmentBoundary
Label和DisplayName支持
.NET 11添加了 [DisplayName] 支持输入组件,自动生成 <label> 元素:
<EditForm Model="model" FormName="contact">
<!-- 自动从[DisplayName]渲染<label> -->
<InputText @bind-Value="model.FullName" />
<InputText @bind-Value="model.EmailAddress" />
</EditForm>
@code {
private ContactModel model = new();
}
// 模型
public sealed class ContactModel
{
[DisplayName("全名")]
[Required]
public string FullName { get; set; } = "";
[DisplayName("电子邮件地址")]
[EmailAddress]
public string EmailAddress { get; set; } = "";
}
回退(net10.0): 手动添加显式 <label for="..."> 元素。
来源: ASP.NET Core .NET 11 Preview - Label/DisplayName
IHostedService在WebAssembly中
.NET 11允许 IHostedService 实现在Blazor WebAssembly中运行,启用浏览器中的后台任务:
// 在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
<!-- net11-preview -->
SignalR ConfigureConnection
.NET 11添加了 ConfigureConnection 到Blazor Server电路hub,允许自定义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
代理注意事项
- 不要在
OnInitializedAsync中调用JS互操作。 DOM尚不可用。对于需要DOM元素的JS调用,使用OnAfterRenderAsync(firstRender: true)。 - 不要忘记在外部状态更改后调用
StateHasChanged()。 当状态从非Blazor上下文(定时器、事件处理程序、JS回调)更改时,调用StateHasChanged()或InvokeAsync(StateHasChanged)以触发重新渲染。 - 不要在预渲染期间使用
ProtectedBrowserStorage。 它抛出异常,因为没有交互式电路存在。仅在OnAfterRenderAsync中访问它。 - 不要忘记在Static SSR表单上使用
FormName。 没有它,Static SSR模式中的表单提交不会路由到正确的处理程序。 - 不要过早处置
DotNetObjectReference。 过早处置导致JavaScript尝试调用回调时抛出JSException。在Dispose()或DisposeAsync()中处置。 - 不要假设在Blazor Server中作用域服务是每请求的。 作用域服务在整个电路持续时间内存活。当您需要组件作用域服务生命周期时,使用
OwningComponentBase<T>。
先决条件
- .NET 8.0+(QuickGrid、增强表单处理、带
IsFixed的级联值) Microsoft.AspNetCore.Components.QuickGrid包用于QuickGrid- .NET 11预览用于EnvironmentBoundary、Label/DisplayName、QuickGrid OnRowClick、WASM中的IHostedService
知识来源
此技能中的Blazor组件模式基于以下指导:
- Damian Edwards – Razor和Blazor组件设计模式、渲染模式架构和性能最佳实践。ASP.NET团队的首席架构师。
这些来源为上述模式和原理提供了信息。此技能不代表或代言任何个人。