🌐
🔍 100%
👁️

🎙️ Sélectionner une voix

🎙️ Select a voice

TP 8 : Architecture par Composants & Communication

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 :

  • Avoir terminé le TP 7 (Le CRUD fonctionne).
  • Important : S'assurer que le CDN de Bootstrap Icons est bien présent dans la balise <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 vers bootstrap.min.css, ajoutez cette ligne :

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">

Activité 1 : Création d'un Composant "Bête" (Dumb Component) (25 min)

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.

  1. Dans votre projet, le dossier 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.
  2. À l'intérieur de Components/UI/, créez un fichier KpiCard.razor.
  3. Écrivez le code suivant. Notez l'attribut [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 @using dans chaque page, vous pouvez l'ajouter une seule fois dans le fichier Components/_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.


Activité 2 : Passage d'Objets (Parent → Enfant) (25 min)

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.

  1. Créez le fichier Components/UI/SensorTable.razor.
  2. Déplacez toute la balise <table>...</table> depuis MyDashboard.razor vers ce nouveau fichier.
  3. Déclarez le paramètre qui va recevoir les données dans le bloc @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" />

Activité 3 : Communication Inverse (Enfant → Parent) (30 min)

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.


🚀 Exercices d'application (En autonomie)

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).

Exercice 1 : Création du composant <SensorCard />

  1. Créez un composant Components/UI/SensorCard.razor.
  2. Ce composant doit accepter un seul capteur en paramètre :
    [Parameter] public SensorData Sensor { get; set; }
  3. Dessinez une petite carte Bootstrap (<div class="card shadow-sm">...) affichant :
    • Une icône (bi-thermometer-half par exemple).
    • Le Nom et la Valeur du capteur.
    • Le nom du Lieu (Location).

Exercice 2 : Basculement de la vue (View Toggle)

Dans MyDashboard.razor, nous voulons laisser l'utilisateur choisir l'affichage (Tableau ou Grille de cartes).

  1. Ajoutez une variable booléenne : private bool ShowAsCards = false;
  2. Ajoutez un bouton en haut de la page pour inverser ce booléen :
<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 :


🏁 Synthèse du TP 8

Vous venez d'appliquer le principe de la "Séparation des Responsabilités" pour l'interface graphique (UI).

LAB 8: Component Architecture & Communication

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:

  • Have completed LAB 7 (CRUD is working).
  • Important: Make sure the Bootstrap Icons CDN is present in the <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.razor file.

2. In the <head> tag, just below the link to bootstrap.min.css, add this line:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">

Activity 1: Creating a "Dumb" Component (25 min)

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.

  1. In your project, the Components folder already exists (it contains Pages and Layout). Create a new subfolder named UI inside Components to store your reusable components.
  2. Inside Components/UI/, create a file KpiCard.razor.
  3. Write the following code. Note the [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 @using in every page, you can add it once in the Components/_Imports.razor file.

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.


Activity 2: Passing Objects (Parent → Child) (25 min)

We will now extract the large HTML table into a dedicated <SensorTable /> component. This component will need the entire list of sensors.

  1. Create the file Components/UI/SensorTable.razor.
  2. Move the entire <table>...</table> tag from MyDashboard.razor to this new file.
  3. Declare the parameter that will receive the data in the @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" />

Activity 3: Reverse Communication (Child → Parent) (30 min)

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.


🚀 Practice Exercises (On your own)

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).

Exercise 1: Creating the <SensorCard /> component

  1. Create a component Components/UI/SensorCard.razor.
  2. This component must accept a single sensor as parameter:
    [Parameter] public SensorData Sensor { get; set; }
  3. Design a small Bootstrap card (<div class="card shadow-sm">...) displaying:
    • An icon (bi-thermometer-half for example).
    • The Name and Value of the sensor.
    • The Location name.

Exercise 2: View Toggle

In MyDashboard.razor, we want to let the user choose the display (Table or Card Grid).

  1. Add a boolean variable: private bool ShowAsCards = false;
  2. Add a button at the top of the page to toggle this boolean:
<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:


🏁 LAB 8 Summary

You have just applied the principle of "Separation of Concerns" for the graphical user interface (UI).