Héritage de composants dans Blazor
Je construisais un projet qui comportait un tas de pages de formulaire, et chacune avait la même logique d’état de chargement, la même gestion des erreurs et les mêmes notifications toast. Copier-coller tout cela me semblait faux, alors j’ai examiné l’héritage des composants dans Blazor. Il s’avère que c’est assez simple puisque les composants Blazor ne sont que des classes C#.
Les bases
Chaque composant Blazor hérite de ComponentBase par défaut. Vous pouvez créer votre propre classe de base qui étend ComponentBase, puis en faire hériter vos composants.
Disons que la plupart de nos pages nécessitent un état de chargement et une gestion des erreurs. Nous pouvons créer une classe de 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();
}
}
}
Utiliser la classe de base
Désormais, dans n’importe quel composant de page, au lieu d’hériter de ComponentBase, nous héritons de notre 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 directive @inherits PageBase est la clé. Il indique à Blazor d’utiliser notre classe de base au lieu de la classe par défaut ComponentBase. Nous obtenons désormais IsLoading, ErrorMessage et LoadDataAsync() gratuitement dans chaque page qui en hérite.
Injection de services dans la classe de base
Vous pouvez également injecter des services dans la classe de base afin qu’ils soient disponibles pour tous les composants enfants :
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);
}
Chaque composant qui hérite de PageBase a désormais accès à Navigation, Toast, NavigateBack() et ShowSuccess() sans avoir à injecter quoi que ce soit.
Aller plus loin avec les classes de base génériques
Vous pouvez même créer des classes de base génériques pour les modèles CRUD courants :
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.");
});
}
}
Ensuite, votre page réelle devient super propre :
@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}");
}
Quand l’utiliser et quand ne pas l’utiliser
L’héritage de composants est idéal pour les comportements partagés tels que les états de chargement, la gestion des erreurs, les contrôles d’authentification ou les modèles CRUD. Mais n’allez pas trop loin avec les hiérarchies d’héritage profondes - si vous vous retrouvez à plus de deux niveaux de profondeur, vous êtes probablement mieux loti avec la composition (comme l’approche LoadingComponent d’un article précédent).
Je le garde généralement à une classe de base par “type” de page : PageBase pour les pages normales, FormPageBase pour les formulaires, et c’est tout.
J’espère que vous avez aimé le message ! N’hésitez pas à me contacter sur tous les réseaux sociaux à @emimontesdeoca.