mcpField
D
dotnet-blazor-components

dotnet-blazor-components

github.com/wshaddix開発8687 回インストール35 週間前に更新
リポジトリ

dotnet-blazor-components について

Blazor 開発者向けのコンポーネント実装スキルです。ライフサイクル、状態管理、JavaScript 相互運用、フォーム検証、QuickGrid を扱い、レンダリングモードごとの動作も説明します。

主な機能

  1. 01コンポーネントのライフサイクルとレンダリングモードの違い
  2. 02カスケード値、依存性注入、ブラウザーストレージの管理
  3. 03JavaScript 相互運用とモジュールインポートの実装
  4. 04EditForm とデータ注釈による検証の設定
  5. 05QuickGrid のソート、フィルター、ページング、仮想化
  6. 06.NET 11 プレビューのコンポーネント機能の解説

活用シーン

  1. 01静的 SSR でのライフサイクルコールバックの調査
  2. 02コンポーネント間での状態とサービスの共有
  3. 03JavaScript モジュールと .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)レスポンス後に呼び出されます回線が終了したときに呼び出されますコンポーネントの削除時に呼び出されます回線が終了したとき(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サーバー上のすべての回線で共有されますブラウザーのタブごとに 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 相互運用
項目InteractiveServerInteractiveWebAssemblyInteractiveAutoHybrid
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


エージェントでの注意事項

  1. OnInitializedAsync で JS 相互運用を呼び出さないでください。 DOM はまだ利用できません。DOM 要素を必要とする JS 呼び出しには、OnAfterRenderAsync(firstRender: true) を使用してください。
  2. 外部で状態が変更された後に StateHasChanged() を呼び出すのを忘れないでください。 Blazor 以外のコンテキスト(タイマー、イベントハンドラー、JS コールバック)から状態が変更された場合は、StateHasChanged() または InvokeAsync(StateHasChanged) を呼び出して再レンダリングを発生させてください。
  3. プリレンダリング中に ProtectedBrowserStorage を使用しないでください。 インタラクティブな回路がまだ存在しないため、例外が発生します。アクセスするのは OnAfterRenderAsync のみにしてください。
  4. Static SSR フォームに FormName を設定するのを忘れないでください。 設定されていないと、Static SSR モードでのフォーム送信が正しいハンドラーにルーティングされません。
  5. JS の処理が終わる前に DotNetObjectReference を破棄しないでください。 早まって破棄すると、JavaScript がコールバックを呼び出そうとしたときに JSException が発生します。Dispose() または DisposeAsync() で破棄してください。
  6. 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 チームの主任アーキテクトです。

これらの情報源は、上記のパターンとその根拠の参考にしています。このスキルは、特定の個人を代表したり、その人物の見解を代弁したりするものではありません。


参考資料

コメント