Blazor 中的继承组件

· 2 分钟阅读

我正在构建一个具有一堆表单页面的项目,每个表单页面都有相同的加载状态逻辑、相同的错误处理和相同的 toast 通知。复制粘贴所有这些感觉都是错误的,因此我研究了 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 指令是关键。它告诉 Blazor 使用我们的基类而不是默认的 ComponentBase。现在,我们在继承它的每个页面中免费获得 IsLoadingErrorMessageLoadDataAsync()

在基类中注入服务

您还可以在基类中注入服务,以便所有子组件都可以使用它们:

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 模式。但不要过度使用深层继承层次结构 - 如果您发现自己的深度超过两层,那么组合可能会更好(就像上一篇文章中的 LoadingComponent 方法)。

我通常将其保留为每个“类型”页面的一个基类:PageBase 用于常规页面,FormPageBase 用于表单,仅此而已。

希望你喜欢这篇文章!请随时通过任何社交媒体与我联系:@emimontesdeoca

资源