mcpField
D
dotnet-blazor-components

dotnet-blazor-components

github.com/wshaddix开发8687 次安装35 周前更新
代码仓库

关于 dotnet-blazor-components

面向 Blazor 开发者的组件实现技能,涵盖生命周期、状态管理、JavaScript 互操作、表单验证和 QuickGrid。内容还说明不同渲染模式下的行为差异,适用于构建和维护 Blazor 组件。

主要功能

  1. 01掌握组件生命周期与渲染模式差异
  2. 02管理级联值、依赖注入和浏览器存储
  3. 03实现 JavaScript 互操作与模块导入
  4. 04配置 EditForm 与数据注解验证
  5. 05使用 QuickGrid 排序、筛选、分页和虚拟化
  6. 06了解 .NET 11 预览组件功能

适用场景

  1. 01排查静态 SSR 下生命周期回调行为
  2. 02在组件间共享状态与服务
  3. 03连接 JavaScript 模块和 .NET 回调
  4. 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 SSRInteractiveServerInteractiveWebAssemblyInteractiveAutoHybrid
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 生命周期行为:

生命周期InteractiveServerInteractiveWebAssemblyInteractiveAutoHybrid
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 互操作
关注点InteractiveServerInteractiveWebAssemblyInteractiveAutoHybrid
JS 调用时机SignalR 确认渲染后WASM 运行时加载后初始通过 SignalR,切换到 WASM 后直接调用WebView 加载后
OnAfterRender 是否可用是是是是
IJSRuntime 同步调用不支持(仅异步)可用 IJSInProcessRuntimeServer 阶段仅支持异步;切换到 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 易错点

  1. 不要在 OnInitializedAsync 中调用 JS 互操作。 此时 DOM 尚不可用。对于需要 DOM 元素的 JS 调用,请使用 OnAfterRenderAsync(firstRender: true)。
  2. 外部状态发生变化后,不要忘记调用 StateHasChanged()。 当状态在非 Blazor 上下文中变化(例如计时器、事件处理程序、JS 回调)时,请调用 StateHasChanged() 或 InvokeAsync(StateHasChanged) 以触发重新渲染。
  3. 预渲染期间不要使用 ProtectedBrowserStorage。 由于此时尚不存在交互式 circuit,使用它会引发异常。请仅在 OnAfterRenderAsync 中访问它。
  4. Static SSR 表单不要忘记设置 FormName。 否则,Static SSR 模式下的表单提交不会路由到正确的处理程序。
  5. JS 尚未用完 DotNetObjectReference 时,不要将其释放。 过早释放会导致 JavaScript 尝试调用回调时引发 JSException。请在 Dispose() 或 DisposeAsync() 中释放。
  6. 不要假设 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 团队首席架构师。

这些来源为上述模式及其依据提供参考。此技能并不声称代表或代言任何个人。


参考资料

评论

支持 Markdown