Herdando componentes no Blazor

· 3 min de leitura

Eu estava construindo um projeto que tinha várias páginas de formulário e cada uma delas tinha a mesma lógica de estado de carregamento, o mesmo tratamento de erros e as mesmas notificações do sistema. Copiar e colar tudo isso parecia errado, então examinei a herança de componentes no Blazor. Acontece que é bastante simples, já que os componentes do Blazor são apenas classes C#.

O básico

Cada componente Blazor herda de ComponentBase por padrão. Você pode criar sua própria classe base que estende ComponentBase e então fazer com que seus componentes sejam herdados dela.

Digamos que a maioria de nossas páginas precise de estado de carregamento e tratamento de erros. Podemos criar uma classe base:

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();
        }
    }
}

#Usando a classe base

Agora, em qualquer componente de página, em vez de herdar de ComponentBase, herdamos de nosso 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");
        });
    }
}

A diretiva @inherits PageBase é a chave. Diz ao Blazor para usar nossa classe base em vez do padrão ComponentBase. Agora temos IsLoading, ErrorMessage e LoadDataAsync() gratuitamente em todas as páginas herdadas dele.

Injetando serviços na classe base

Você também pode injetar serviços na classe base para que estejam disponíveis para todos os componentes filhos:

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);
}

Cada componente que herda de PageBase agora tem acesso a Navigation, Toast, NavigateBack() e ShowSuccess() sem precisar injetar nada.

Indo mais fundo com classes base genéricas

Você pode até criar classes base genéricas para padrões CRUD comuns:

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.");
        });
    }
}

Então sua página real fica super limpa:

@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}");
}

Quando usar e quando não usar

A herança de componentes é ótima para comportamento compartilhado, como estados de carregamento, tratamento de erros, verificações de autenticação ou padrões CRUD. Mas não exagere com hierarquias de herança profundas - se você estiver indo mais de dois níveis de profundidade, provavelmente estará melhor com a composição (como a abordagem LoadingComponent de uma postagem anterior).

Eu normalmente mantenho uma classe base por “tipo” de página: PageBase para páginas normais, FormPageBase para formulários, e isso é tudo.

Espero que você tenha gostado da postagem! Sinta-se à vontade para entrar em contato comigo em qualquer mídia social em @emimontesdeoca.

Recursos