Blazor でのコンポーネントの継承

· 2分で読める

私は多数のフォーム ページを含むプロジェクトを構築していましたが、どのページも同じ読み込み状態ロジック、同じエラー処理、同じトースト通知を持っていました。これらすべてをコピーして貼り付けるのは間違っていると感じたので、Blazor でのコンポーネントの継承について調べました。 Blazor コンポーネントは単なる C# クラスであるため、これは非常に簡単であることがわかります。

基本

すべての Blazor コンポーネントは、既定で ComponentBase から継承します。 ComponentBase を拡張する独自の基本クラスを作成し、それをコンポーネントに継承させることができます。

ほとんどのページで状態の読み込みとエラー処理が必要だとします。基本クラスを作成できます。

using Microsoft.AspNetCore.Components;

public abstract class PageBase : ComponentBase
{
    protected bool IsLoading { get; set; } = true;
    protected string? ErrorMessage { get; set; }

    protected async Task LoadDataAsync(Func<Task> action)
    {
        try
        {
            IsLoading = true;
            ErrorMessage = null;
            await action();
        }
        catch (Exception ex)
        {
            ErrorMessage = ex.Message;
        }
        finally
        {
            IsLoading = false;
            StateHasChanged();
        }
    }
}

基本クラスの使用

どのページ コンポーネントでも、ComponentBase から継承する代わりに、PageBase から継承します。

@page "/users"
@inherits PageBase

@if (IsLoading)
{
    <div class="spinner"></div>
}
else if (ErrorMessage is not null)
{
    <div class="alert alert-danger">@ErrorMessage</div>
}
else
{
    <ul>
        @foreach (var user in users)
        {
            <li>@user.Name</li>
        }
    </ul>
}

@code {
    private List<User> users = new();

    protected override async Task OnInitializedAsync()
    {
        await LoadDataAsync(async () =>
        {
            users = await Http.GetFromJsonAsync<List<User>>("api/users");
        });
    }
}

@inherits PageBase ディレクティブが鍵です。これは、デフォルトの ComponentBase の代わりに基本クラスを使用するように Blazor に指示します。これで、IsLoadingErrorMessage、および LoadDataAsync() を継承するすべてのページで無料で取得できるようになりました。

基本クラスにサービスを挿入する

基本クラスにサービスを挿入して、すべての子コンポーネントで利用できるようにすることもできます。

public abstract class PageBase : ComponentBase
{
    [Inject]
    protected NavigationManager Navigation { get; set; } = default!;

    [Inject]
    protected IToastService Toast { get; set; } = default!;

    protected bool IsLoading { get; set; } = true;
    protected string? ErrorMessage { get; set; }

    protected void NavigateBack() => Navigation.NavigateTo("javascript:history.back()");

    protected void ShowSuccess(string message) => Toast.ShowSuccess(message);
}

PageBase を継承するすべてのコンポーネントは、何も注入しなくても NavigationToastNavigateBack()、および ShowSuccess() にアクセスできるようになりました。

汎用基本クラスをさらに深く掘り下げる

一般的な CRUD パターンの汎用基本クラスを作成することもできます。

public abstract class CrudPageBase<T> : PageBase
{
    protected List<T> Items { get; set; } = new();
    protected T? SelectedItem { get; set; }

    protected abstract Task<List<T>> FetchItems();
    protected abstract Task DeleteItem(T item);

    protected override async Task OnInitializedAsync()
    {
        await LoadDataAsync(async () =>
        {
            Items = await FetchItems();
        });
    }

    protected async Task OnDelete(T item)
    {
        await LoadDataAsync(async () =>
        {
            await DeleteItem(item);
            Items = await FetchItems();
            ShowSuccess("Item deleted.");
        });
    }
}

そうすれば、実際のページは非常にきれいになります。

@page "/products"
@inherits CrudPageBase<Product>

@* just the markup, all logic lives in the base class *@

@code {
    protected override Task<List<Product>> FetchItems()
        => Http.GetFromJsonAsync<List<Product>>("api/products");

    protected override Task DeleteItem(Product item)
        => Http.DeleteAsync($"api/products/{item.Id}");
}

使用する場合と使用しない場合

コンポーネントの継承は、状態の読み込み、エラー処理、認証チェック、CRUD パターンなどの共有動作に最適です。ただし、継承階層を深くしすぎないでください。2 レベル以上の階層を深くする場合は、(前の投稿の LoadingComponent アプローチのような) 合成を使用したほうがよいでしょう。

私は通常、ページの「タイプ」ごとに 1 つの基本クラスに保ちます。通常のページの場合は PageBase、フォームの場合は FormPageBase です。それだけです。

投稿が気に入っていただければ幸いです!ソーシャルメディアで**@emimontesdeoca**までお気軽にご連絡ください。

リソース