Module : Programmation .Net C#
Durée : 2h
Objectif : Refactoriser l'application pour la rendre modulaire. Créer des composants
réutilisables (UI) et maîtriser la communication Parent ↔ Enfant via les
[Parameter] et les
EventCallback.
Pré-requis :
<head> de votre fichier App.razor.Comment ajouter les Bootstrap Icons dans .NET 10 ?
1. Ouvrez le fichier principal qui contient la structure HTML de votre application. Dans une Blazor Web App récente, c'est le fichier
Components/App.razor.2. Dans la balise
<head>, juste en dessous du lien versbootstrap.min.css, ajoutez cette ligne :
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
Actuellement, nos cartes "KPI" (Total, Moyenne, Max) utilisent du code HTML répétitif. Nous allons créer
un composant générique <KpiCard /> pour éviter de copier-coller le même HTML.
Components existe déjà (il contient Pages et
Layout). Créez un nouveau sous-dossier nommé UI à l'intérieur de
Components pour y ranger vos composants réutilisables.
Components/UI/, créez un fichier KpiCard.razor.[Parameter] qui
permet au composant d'accepter des données de l'extérieur (depuis la page Parent).<!-- Components/UI/KpiCard.razor -->
<div class="card text-white @BackgroundColorClass h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title mb-0">@Title</h6>
<h3 class="my-2">@Value</h3>
</div>
<!-- Affichage de l'icône Bootstrap Icons (bi) -->
<i class="bi @IconClass" style="font-size: 2.5rem; opacity: 0.5;"></i>
</div>
</div>
</div>
@code {
//[Parameter] indique que ces valeurs seront fournies par la balise parente
[Parameter]
public string Title { get; set; } = "Titre KPI";
[Parameter]
public string Value { get; set; } = "0";
[Parameter]
public string BackgroundColorClass { get; set; } = "bg-primary";
[Parameter]
public string IconClass { get; set; } = "bi-graph-up"; // Icône par défaut
}
4. Utilisation dans le Parent : Ouvrez Components/Pages/MyDashboard.razor.
5. Tout en haut du fichier (sous les @using), ajoutez la directive pour importer vos
composants :
@using DashboardData.Components.UI
💡 Astuce : Pour éviter de répéter ce
@usingdans chaque page, vous pouvez l'ajouter une seule fois dans le fichierComponents/_Imports.razor.
6. Remplacez l'ancien HTML de vos 3 KPIs par l'utilisation de votre nouveau composant avec les icônes Bootstrap appropriées :
<!-- Dans MyDashboard.razor -->
<div class="row mb-4">
<div class="col-md-4 mb-3">
<!-- On passe les paramètres comme de simples attributs HTML -->
<KpiCard Title="Total Sondes" Value="@TotalSondes.ToString()"
BackgroundColorClass="bg-primary" IconClass="bi-box-seam" />
</div>
<div class="col-md-4 mb-3">
<KpiCard Title="Moyenne Globale" Value="@($"{Moyenne:F1} °C")"
BackgroundColorClass="bg-success" IconClass="bi-bullseye" />
</div>
<div class="col-md-4 mb-3">
<KpiCard Title="Alerte Max" Value="@($"{Max:F1} °C")"
BackgroundColorClass="bg-danger" IconClass="bi-exclamation-triangle-fill" />
</div>
</div>
Testez votre page : Le rendu visuel doit être identique (avec de plus jolies icônes !), mais votre code principal est maintenant beaucoup plus propre.
Nous allons maintenant extraire le gros tableau HTML dans un composant dédié
<SensorTable />. Ce composant aura besoin de la liste entière des capteurs.
Components/UI/SensorTable.razor.<table>...</table> depuis
MyDashboard.razor vers ce nouveau fichier.
@code :<!-- Components/UI/SensorTable.razor -->
@using DashboardData.Models
<table class="table table-striped table-hover mt-3">
<thead class="table-dark">
<tr>
<th>Nom</th>
<th>Lieu</th>
<th>Valeur</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@if (Sensors == null || !Sensors.Any())
{
<tr><td colspan="4" class="text-center">Aucune donnée disponible.</td></tr>
}
else
{
@foreach (var sensor in Sensors)
{
<tr>
<td>@sensor.Name</td>
<td>@(sensor.Location != null ? sensor.Location.Name : "N/A")</td>
<td>@sensor.Value</td>
<td>
<a href="/edit-sensor/@sensor.Id" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i> Éditer
</a>
<button class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i> Supprimer
</button>
</td>
</tr>
}
}
</tbody>
</table>
@code {
[Parameter]
public List<SensorData> Sensors { get; set; } = new();
}
4. Dans MyDashboard.razor, appelez le composant à l'endroit où se trouvait le tableau :
<SensorTable Sensors="Sensors" />
Le Problème : Le bouton "Supprimer" est maintenant dans l'Enfant
(SensorTable). Mais la logique de suppression (appel à la BDD et rechargement de la liste)
se trouve dans le Parent (MyDashboard). L'Enfant ne doit pas supprimer la donnée lui-même
(il n'a pas accès au SensorService par principe de conception).
La Solution : L'EventCallback. L'Enfant va "crier"
(déclencher un événement) en disant : "Hé Parent, l'utilisateur a cliqué sur supprimer pour l'ID 5
!". Et le Parent fera le travail.
1. Dans Components/UI/SensorTable.razor, ajoutez un
EventCallback :
@code {
[Parameter]
public List<SensorData> Sensors { get; set; } = new();
// EventCallback permet de remonter une information (ici un int, l'Id) vers le parent
[Parameter]
public EventCallback<int> OnDeleteClicked { get; set; }
// Méthode appelée par le clic du bouton
private async Task TriggerDelete(int id)
{
// On notifie le Parent et on lui passe l'ID du capteur à supprimer
await OnDeleteClicked.InvokeAsync(id);
}
}
2. Modifiez le bouton "Supprimer" dans le HTML du même fichier pour appeler cette méthode :
<button class="btn btn-sm btn-outline-danger" @onclick="() => TriggerDelete(sensor.Id)">
<i class="bi bi-trash"></i> Supprimer
</button>
3. Côté Parent (MyDashboard.razor) : "Abonnez-vous" à cet événement. Mettez
à jour l'appel du composant :
<!-- On relie l'événement de l'enfant à la méthode DeleteSensor du parent -->
<SensorTable Sensors="Sensors" OnDeleteClicked="DeleteSensor" />
4. Vérifiez que votre méthode DeleteSensor dans MyDashboard.razor fait bien le
travail (vue au TP7) :
private async Task DeleteSensor(int id)
{
await SensorService.DeleteSensorAsync(id);
await LoadAll(); // Recharge la liste
}
Testez ! Le clic dans l'enfant déclenche la suppression dans le parent de manière totalement transparente.
L'architecture par composants permet de créer différentes vues pour les mêmes données. En Data Science, on propose souvent une vue "Tableau" (détaillée) et une vue "Cartes" (résumée).
<SensorCard />Components/UI/SensorCard.razor.[Parameter] public SensorData Sensor { get; set; }
<div class="card shadow-sm">...) affichant :
bi-thermometer-half par exemple).Dans MyDashboard.razor, nous voulons laisser l'utilisateur choisir l'affichage (Tableau ou
Grille de cartes).
private bool ShowAsCards = false;
<button class="btn btn-outline-secondary mb-3" @onclick="() => ShowAsCards = !ShowAsCards">
<i class="bi bi-arrow-left-right"></i> Changer de Vue
</button>
3. Utilisez un bloc @if / @else dans le HTML :
ShowAsCards est vrai : Faites une boucle foreach sur
Sensors dans une <div class="row"> et appelez votre composant
<SensorCard Sensor="s" /> dans des colonnes (col-md-3).
<SensorTable Sensors="Sensors" OnDeleteClicked="DeleteSensor" />.
Vous venez d'appliquer le principe de la "Séparation des Responsabilités" pour l'interface graphique (UI).
KpiCard) : N'a aucune logique. Il reçoit des données
via [Parameter] et les affiche. C'est un composant de
présentation.
SensorTable) : Reçoit une liste d'objets
complexes via [Parameter].EventCallback : Le mécanisme standard de
Blazor pour la communication Enfant → Parent. L'enfant notifie, le parent
agit.
Module: .Net C# Programming
Duration: 2h
Objective: Refactor the application to make it modular. Create reusable UI
components and master Parent ↔ Child communication via
[Parameter] and
EventCallback.
Prerequisites:
<head> tag of your App.razor file.
How to add Bootstrap Icons in .NET 10?
1. Open the main file that contains your application's HTML structure. In a recent Blazor Web App, this is the
Components/App.razorfile.2. In the
<head>tag, just below the link tobootstrap.min.css, add this line:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
Currently, our "KPI" cards (Total, Average, Max) use repetitive HTML code. We will create a generic
<KpiCard /> component to avoid copy-pasting the same HTML.
Components folder already exists (it contains Pages
and Layout). Create a new subfolder named UI inside
Components to store your reusable components.
Components/UI/, create a file KpiCard.razor.[Parameter] attribute which
allows the component to accept data from outside (from the Parent page).<!-- Components/UI/KpiCard.razor -->
<div class="card text-white @BackgroundColorClass h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title mb-0">@Title</h6>
<h3 class="my-2">@Value</h3>
</div>
<!-- Bootstrap Icons (bi) icon display -->
<i class="bi @IconClass" style="font-size: 2.5rem; opacity: 0.5;"></i>
</div>
</div>
</div>
@code {
//[Parameter] indicates these values will be provided by the parent tag
[Parameter]
public string Title { get; set; } = "KPI Title";
[Parameter]
public string Value { get; set; } = "0";
[Parameter]
public string BackgroundColorClass { get; set; } = "bg-primary";
[Parameter]
public string IconClass { get; set; } = "bi-graph-up"; // Default icon
}
4. Usage in the Parent: Open Components/Pages/MyDashboard.razor.
5. At the very top of the file (below the @using directives), add the directive to import
your components:
@using DashboardData.Components.UI
💡 Tip: To avoid repeating this
@usingin every page, you can add it once in theComponents/_Imports.razorfile.
6. Replace the old HTML for your 3 KPIs with your new component using the appropriate Bootstrap icons:
<!-- In MyDashboard.razor -->
<div class="row mb-4">
<div class="col-md-4 mb-3">
<!-- Parameters are passed like simple HTML attributes -->
<KpiCard Title="Total Sensors" Value="@TotalSondes.ToString()"
BackgroundColorClass="bg-primary" IconClass="bi-box-seam" />
</div>
<div class="col-md-4 mb-3">
<KpiCard Title="Global Average" Value="@($"{Moyenne:F1} °C")"
BackgroundColorClass="bg-success" IconClass="bi-bullseye" />
</div>
<div class="col-md-4 mb-3">
<KpiCard Title="Max Alert" Value="@($"{Max:F1} °C")"
BackgroundColorClass="bg-danger" IconClass="bi-exclamation-triangle-fill" />
</div>
</div>
Test your page: The visual output should be identical (with nicer icons!), but your main code is now much cleaner.
We will now extract the large HTML table into a dedicated <SensorTable /> component.
This component will need the entire list of sensors.
Components/UI/SensorTable.razor.<table>...</table> tag from MyDashboard.razor
to this new file.@code block:<!-- Components/UI/SensorTable.razor -->
@using DashboardData.Models
<table class="table table-striped table-hover mt-3">
<thead class="table-dark">
<tr>
<th>Name</th>
<th>Location</th>
<th>Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@if (Sensors == null || !Sensors.Any())
{
<tr><td colspan="4" class="text-center">No data available.</td></tr>
}
else
{
@foreach (var sensor in Sensors)
{
<tr>
<td>@sensor.Name</td>
<td>@(sensor.Location != null ? sensor.Location.Name : "N/A")</td>
<td>@sensor.Value</td>
<td>
<a href="/edit-sensor/@sensor.Id" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i> Edit
</a>
<button class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i> Delete
</button>
</td>
</tr>
}
}
</tbody>
</table>
@code {
[Parameter]
public List<SensorData> Sensors { get; set; } = new();
}
4. In MyDashboard.razor, call the component where the table used to be:
<SensorTable Sensors="Sensors" />
The Problem: The "Delete" button is now in the Child (SensorTable). But the
deletion logic (database call and list reload) lives in the Parent (MyDashboard). The Child
should not delete the data itself (it does not have access to the SensorService, by design
principle).
The Solution: The EventCallback. The Child will
"shout" (trigger an event) saying: "Hey Parent, the user clicked delete for ID 5!". And the
Parent will do the work.
1. In Components/UI/SensorTable.razor, add an
EventCallback:
@code {
[Parameter]
public List<SensorData> Sensors { get; set; } = new();
// EventCallback sends information (here an int, the Id) back to the parent
[Parameter]
public EventCallback<int> OnDeleteClicked { get; set; }
// Method called by the button click
private async Task TriggerDelete(int id)
{
// Notify the Parent and pass the sensor ID to delete
await OnDeleteClicked.InvokeAsync(id);
}
}
2. Modify the "Delete" button in the same file's HTML to call this method:
<button class="btn btn-sm btn-outline-danger" @onclick="() => TriggerDelete(sensor.Id)">
<i class="bi bi-trash"></i> Delete
</button>
3. On the Parent side (MyDashboard.razor): "Subscribe" to this event.
Update the component call:
<!-- Link the child's event to the parent's DeleteSensor method -->
<SensorTable Sensors="Sensors" OnDeleteClicked="DeleteSensor" />
4. Verify that your DeleteSensor method in MyDashboard.razor does the work
(seen in LAB 7):
private async Task DeleteSensor(int id)
{
await SensorService.DeleteSensorAsync(id);
await LoadAll(); // Reload the list
}
Test it! The click in the child triggers the deletion in the parent completely transparently.
Component architecture allows creating different views for the same data. In Data Science, we often provide a "Table" view (detailed) and a "Cards" view (summarized).
<SensorCard /> componentComponents/UI/SensorCard.razor.[Parameter] public SensorData Sensor { get; set; }
<div class="card shadow-sm">...) displaying:
bi-thermometer-half for example).In MyDashboard.razor, we want to let the user choose the display (Table or Card Grid).
private bool ShowAsCards = false;
<button class="btn btn-outline-secondary mb-3" @onclick="() => ShowAsCards = !ShowAsCards">
<i class="bi bi-arrow-left-right"></i> Toggle View
</button>
3. Use an @if / @else block in the HTML:
ShowAsCards is true: Loop foreach over Sensors in a
<div class="row"> and call your new <SensorCard Sensor="s" />
component in columns (col-md-3).
<SensorTable Sensors="Sensors" OnDeleteClicked="DeleteSensor" /> component.
You have just applied the principle of "Separation of Concerns" for the graphical user interface (UI).
KpiCard): Has no logic. It receives data via
[Parameter] and displays it. It is a
presentation component.
SensorTable): Receives a list of complex objects via
[Parameter].
EventCallback: The standard Blazor mechanism
for Child → Parent communication. The child notifies, the parent
acts.