Heredar componentes en Blazor

· 3 min de lectura

Estaba creando un proyecto que tenía un montón de páginas de formulario y cada una tenía la misma lógica de estado de carga, el mismo manejo de errores y las mismas notificaciones del sistema. Copiar y pegar todo eso me pareció incorrecto, así que analicé la herencia de componentes en Blazor. Resulta que es bastante sencillo ya que los componentes de Blazor son solo clases de C#.

Lo básico

Cada componente de Blazor hereda de ComponentBase de forma predeterminada. Puede crear su propia clase base que extienda ComponentBase y luego hacer que sus componentes hereden de ella.

Digamos que la mayoría de nuestras páginas necesitan estado de carga y manejo de errores. Podemos crear una clase 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 la clase base

Ahora en cualquier componente de la página, en lugar de heredar de ComponentBase, heredamos de nuestro 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");
        });
    }
}

La directiva @inherits PageBase es la clave. Le dice a Blazor que use nuestra clase base en lugar de la [[[TOK_7]] predeterminada. Ahora obtenemos IsLoading, ErrorMessage y LoadDataAsync() gratis en cada página que hereda de él.

Inyectar servicios en la clase base.

También puede inyectar servicios en la clase base para que estén disponibles para todos los componentes secundarios:

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 hereda de PageBase ahora tiene acceso a Navigation, Toast, NavigateBack() y ShowSuccess() sin tener que inyectar nada.

Profundizando con clases base genéricas

Incluso puedes crear clases base genéricas para patrones CRUD comunes:

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

Entonces tu página real se vuelve súper limpia:

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

Cuándo usarlo y cuándo no

La herencia de componentes es excelente para comportamientos compartidos como estados de carga, manejo de errores, comprobaciones de autenticación o patrones CRUD. Pero no se exceda con las jerarquías de herencia profundas: si se encuentra profundizando más de dos niveles, probablemente esté mejor con la composición (como el enfoque LoadingComponent de una publicación anterior).

Normalmente lo mantengo en una clase base por “tipo” de página: PageBase para páginas normales, FormPageBase para formularios, y eso es todo.

Espero que os haya gustado el post! No dudes en contactarme en cualquier red social en @emimontesdeoca.

Recursos