dotnet-blazor-components について
Blazor 開発者向けのコンポーネント実装スキルです。ライフサイクル、状態管理、JavaScript 相互運用、フォーム検証、QuickGrid を扱い、レンダリングモードごとの動作も説明します。
主な機能
- 01コンポーネントのライフサイクルとレンダリングモードの違い
- 02カスケード値、依存性注入、ブラウザーストレージの管理
- 03JavaScript 相互運用とモジュールインポートの実装
- 04EditForm とデータ注釈による検証の設定
- 05QuickGrid のソート、フィルター、ページング、仮想化
- 06.NET 11 プレビューのコンポーネント機能の解説
活用シーン
- 01静的 SSR でのライフサイクルコールバックの調査
- 02コンポーネント間での状態とサービスの共有
- 03JavaScript モジュールと .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) | レスポンス後に呼び出されます | 回線が終了したときに呼び出されます | コンポーネントの削除時に呼び出されます | 回線が終了したとき(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 | サーバー上のすべての回線で共有されます | ブラウザーのタブごとに 1 つです | Server フェーズではサーバー全体で共有され、WASM への切り替え後はタブごとに 1 つです | アプリインスタンスごとに 1 つです |
| Scoped | 回線ごとに 1 つです(ユーザーごとのように動作します) | ブラウザーのタブごとに 1 つです(Singleton と同じです) | Server フェーズでは回線ごと、WASM フェーズではタブごとです。フェーズ間で状態は引き継がれません | アプリインスタンスごとに 1 つです(Singleton と同じです) |
| Transient | 注入のたびに新しいインスタンスが作成されます | 注入のたびに新しいインスタンスが作成されます | 注入のたびに新しいインスタンスが作成されます | 注入のたびに新しいインスタンスが作成されます |
注意点: Blazor Server では、Scoped サービスはリクエストごと(MVC の場合)ではなく、回線が続く間ずっと存続します。回線は、ユーザーがページを離れるか、接続が切断されるまで維持されます。有効期間の長いスコープ付きサービスに状態が蓄積する場合は、コンポーネントスコープの DI に OwningComponentBase<T> を使用してください。
ブラウザーストレージ
// 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 経由で行われるため、ネットワーク遅延が発生します。複数の操作を 1 つの JS 関数呼び出しにまとめ、往復回数を最小限に抑えてください。
EditForm の検証
Data Annotations を使用した基本的な 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 のサーキットハブに ConfigureConnection が追加され、SignalR 接続をカスタマイズできるようになりました(例:カスタムヘッダーの追加、再接続の設定)。
// Program.cs
app.MapBlazorHub(options =>
{
options.ConfigureConnection = connection =>
{
connection.Metadata["tenant"] = "default";
};
});
代替方法(net10.0): IHubFilter またはミドルウェアを使用して、ハブレベルで接続を調査・変更します。
出典: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 モードでのフォーム送信が正しいハンドラーにルーティングされません。 - JS の処理が終わる前に
DotNetObjectReferenceを破棄しないでください。 早まって破棄すると、JavaScript がコールバックを呼び出そうとしたときにJSExceptionが発生します。Dispose()またはDisposeAsync()で破棄してください。 - Blazor Server で Scoped サービスがリクエストごとに作成されると思い込まないでください。 Scoped サービスは回路全体を通じて存続します。コンポーネントスコープのサービスの有効期間が必要な場合は、
OwningComponentBase<T>を使用してください。
前提条件
- .NET 8.0 以降(QuickGrid、強化されたフォーム処理、
IsFixedを使用するカスケード値) - QuickGrid 用の
Microsoft.AspNetCore.Components.QuickGridパッケージ - EnvironmentBoundary、Label/DisplayName、QuickGrid OnRowClick、WASM での IHostedService 用の .NET 11 preview
ナレッジソース
このスキルの Blazor コンポーネントのパターンは、次のガイダンスに基づいています。
- Damian Edwards -- Razor と Blazor のコンポーネント設計パターン、レンダーモードのアーキテクチャ、パフォーマンスに関するベストプラクティス。ASP.NET チームの主任アーキテクトです。
これらの情報源は、上記のパターンとその根拠の参考にしています。このスキルは、特定の個人を代表したり、その人物の見解を代弁したりするものではありません。