🌐
🔍 100%
👁️

TP 4 : Architecture & Injection de Dépendances

Module : Programmation .Net C#

Niveau : 4ème Génie Informatique

Objectif : Nettoyer le code ! Séparer la logique métier (Services) de l'interface graphique (Vues). Comprendre le pattern Injection de Dépendance (DI) et la programmation asynchrone (async/await).

Pré-requis

Activité 1 : Création du Service "Métier" (20 min)

Dans le TP 3, notre liste de capteurs était définie directement dans la page MyDashboard.razor. C'est une mauvaise pratique (couplage fort). Nous allons déplacer cette logique dans une classe dédiée.

1. Dans le dossier Data (ou créez un dossier Services à la racine), créez une nouvelle classe SensorService.cs.

2. Déplacez la logique de création de données ici. Nous allons ajouter quelques capteurs supplémentaires pour rendre les calculs intéressants :

using DashboardData.Models; // Import du namespace où se trouve SensorData

namespace DashboardData.Services;

public class SensorService
{
    // Notre "Base de données" en mémoire pour l'instant
    private List<SensorData> _sensors = new()
    {
        new SensorData { Name = "Temp_Salon", Value = 22.5 },
        new SensorData { Name = "Hum_Cuisine", Value = 45.0 },
        new SensorData { Name = "CO2_Bureau", Value = 800 },
        new SensorData { Name = "Temp_Bureau", Value = 24.0 }, 
        new SensorData { Name = "Temp_Ext", Value = 12.0 }
    };

    // Méthode pour récupérer les données
    public List<SensorData> GetSensors()
    {
        return _sensors;
    }

    // Méthode pour ajouter une donnée (simulée)
    public void AddSensor(SensorData sensor)
    {
        _sensors.Add(sensor);
    }
}

Activité 2 : Définir l'Interface (10 min)

Une Interface est un contrat. Elle définit ce que le service fait, mais pas comment. Cela permet de changer l'implémentation (ex: passer d'une liste en mémoire à une vraie base de données) sans modifier le code de la page.

1. Dans le même dossier Services, créez une interface ISensorService.cs :

using DashboardData.Models;

namespace DashboardData.Services;

public interface ISensorService
{
    List<SensorData> GetSensors();
    void AddSensor(SensorData sensor);
}

2. Modifiez SensorService pour implémenter cette interface :

public class SensorService : ISensorService
{
    // ... le reste du code ne change pas
}

Activité 3 : Enregistrer le Service dans le Conteneur DI (5 min)

C'est dans Program.cs que nous disons au moteur Blazor : "Quand une page demande un ISensorService, donne-lui une instance de SensorService".

1. Ouvrez Program.cs et ajoutez cette ligne avant builder.Build() :

// Enregistrement du service avec une durée de vie "Scoped"
builder.Services.AddScoped<ISensorService, SensorService>();

📝 Les 3 Durées de Vie

Durée de Vie Description Cas d'usage typique
Singleton Une seule instance pour toute l'application. Configuration, cache partagé.
Scoped Une instance par "circuit" (connexion utilisateur Blazor Server). Données utilisateur, contexte de base de données.
Transient Une nouvelle instance à chaque injection. Services légers, calculs sans état.

Activité 4 : Injecter le Service dans la Page (10 min)

Maintenant, notre page MyDashboard.razor ne doit plus créer la liste elle-même. Elle la demande.

1. En haut de MyDashboard.razor, ajoutez la directive d'injection :

@page "/dashboard"
@rendermode InteractiveServer
@using DashboardData.Services // Import du namespace
@inject ISensorService SensorService // "Injecte-moi le service !"

2. Modifiez le bloc @code pour utiliser le service :

@code {
    private List<SensorData> _sensors = new();

    protected override void OnInitialized()
    {
        // Au lieu de créer la liste ici, on la demande au service
        _sensors = SensorService.GetSensors();
    }
}

3. Lancez l'application (dotnet watch). L'affichage doit être identique, mais le code est bien mieux organisé.

Activité 5 : Async/Await - Simuler un Chargement (20 min)

En situation réelle, les données viennent d'une API ou d'une base de données. Ces opérations prennent du temps. Il est impératif de les rendre asynchrones pour ne pas bloquer l'interface.

1. Dans ISensorService.cs, modifiez la signature de GetSensors :

// Avant :
List<SensorData> GetSensors();

// Après :
Task<List<SensorData>> GetSensorsAsync();

2. Implémentez-la dans SensorService.cs avec un délai simulé :

public async Task<List<SensorData>> GetSensorsAsync()
{
    // Simule 2 secondes de latence réseau
    await Task.Delay(2000);
    return _sensors;
}

3. Mettez à jour MyDashboard.razor pour utiliser cette méthode asynchrone :

@code {
    private List<SensorData> _sensors = new();
    private bool _isLoading = true;

    // OnInitializedAsync au lieu de OnInitialized
    protected override async Task OnInitializedAsync()
    {
        _sensors = await SensorService.GetSensorsAsync();
        _isLoading = false; // Le chargement est terminé
    }
}

4. Ajoutez un indicateur de chargement dans le HTML :

@if (_isLoading)
{
    <p><em>Chargement des données...</em></p>
}
else
{
    <!-- Le tableau de données -->
    <table> ... </table>
}

5. Relancez. Vous devriez voir le message "Chargement..." pendant 2 secondes avant l'affichage des données !

🚀 Exercices d'application (En autonomie)

Exercice 1 : Ajout de données via une Nouvelle Page

Objectif : Créer une page dédiée pour l'ajout d'un capteur, avec navigation.

  1. Créez une nouvelle page AddSensor.razor avec la route @page "/add-sensor".
  2. Dans MyDashboard.razor, ajoutez un bouton pour naviguer vers cette page.
  3. Dans AddSensor.razor, créez le formulaire (inputs + bouton de validation).
  4. Injectez NavigationManager pour gérer la redirection.
  5. Au clic, appelez le service puis retournez au dashboard.

💡 Indice : Navigation

<!-- En haut du fichier -->
@inject NavigationManager NavManager

@code {
    private void NavigateBack()
    {
        // Retour au tableau de bord
        NavManager.NavigateTo("/dashboard");
    }
}

Activité Bonus : Expérimenter les Durées de Vie

Pour mieux comprendre, créez un nouveau service simple :

  1. Créez UserCounterService.cs avec une propriété Count et une méthode Increment().
  2. Enregistrez-le dans Program.cs avec différentes durées de vie.
  3. Injectez-le dans votre Dashboard.

🔎 Expérience 1 : Singleton

Utilisez builder.Services.AddSingleton<UserCounterService>().

Ouvrez votre dashboard dans deux onglets différents (ou un en navigation privée). Incrémentez dans l'un, rafraîchissez l'autre.

❓ Question : Le compteur est-il partagé ?

🔎 Expérience 2 : Scoped

Changez pour builder.Services.AddScoped<UserCounterService>() et redémarrez.

❓ Question : Le compteur est-il partagé ?

🔎 Expérience 3 : Transient

Utilisez builder.Services.AddTransient<UserCounterService>().

Injectez le service deux fois dans la même page : @inject UserCounterService S1 et @inject UserCounterService S2. Affichez les compteurs de S1 et S2.

❓ Question : S1 et S2 partagent-ils la même valeur ?

Synthèse du TP 4

LAB 4: Architecture & Dependency Injection

Module: .Net C# Programming

Level: 4th Year Computer Engineering

Objective: Clean up the code! Separate business logic (Services) from the UI (Views). Understand the Dependency Injection (DI) pattern and asynchronous programming (async/await).

Prerequisites

Activity 1: Creating the "Business" Service (20 min)

In LAB 3, our sensor list was defined directly in the MyDashboard.razor page. This is bad practice (tight coupling). We will move this logic into a dedicated class.

1. In the Data folder (or create a Services folder at the root), create a new class SensorService.cs.

2. Move the data creation logic here. We will add a few extra sensors to make calculations interesting:

using DashboardData.Models; // Import namespace where SensorData is located

namespace DashboardData.Services;

public class SensorService
{
    // Our "database" in memory for now
    private List<SensorData> _sensors = new()
    {
        new SensorData { Name = "Temp_LivingRoom", Value = 22.5 },
        new SensorData { Name = "Hum_Kitchen", Value = 45.0 },
        new SensorData { Name = "CO2_Office", Value = 800 },
        new SensorData { Name = "Temp_Office", Value = 24.0 }, 
        new SensorData { Name = "Temp_Outdoor", Value = 12.0 }
    };

    // Method to retrieve data
    public List<SensorData> GetSensors()
    {
        return _sensors;
    }

    // Method to add data (simulated)
    public void AddSensor(SensorData sensor)
    {
        _sensors.Add(sensor);
    }
}

Activity 2: Define the Interface (10 min)

An Interface is a contract. It defines what the service does, but not how. This allows changing the implementation (e.g., from an in-memory list to a real database) without modifying the page code.

1. In the same Services folder, create an interface ISensorService.cs:

using DashboardData.Models;

namespace DashboardData.Services;

public interface ISensorService
{
    List<SensorData> GetSensors();
    void AddSensor(SensorData sensor);
}

2. Modify SensorService to implement this interface:

public class SensorService : ISensorService
{
    // ... rest of code remains the same
}

Activity 3: Register the Service in the DI Container (5 min)

It's in Program.cs that we tell the Blazor engine: "When a page requests an ISensorService, give it an instance of SensorService".

1. Open Program.cs and add this line before builder.Build():

// Register service with "Scoped" lifetime
builder.Services.AddScoped<ISensorService, SensorService>();

📝 The 3 Lifetimes

Lifetime Description Typical Use Case
Singleton A single instance for the entire application. Configuration, shared cache.
Scoped One instance per "circuit" (Blazor Server user connection). User data, database context.
Transient A new instance on every injection. Lightweight services, stateless computations.

Activity 4: Inject the Service into the Page (10 min)

Now, our MyDashboard.razor page no longer creates the list itself. It requests it.

1. At the top of MyDashboard.razor, add the injection directive:

@page "/dashboard"
@rendermode InteractiveServer
@using DashboardData.Services // Import namespace
@inject ISensorService SensorService // "Inject the service!"

2. Modify the @code block to use the service:

@code {
    private List<SensorData> _sensors = new();

    protected override void OnInitialized()
    {
        // Instead of creating the list here, we request it from the service
        _sensors = SensorService.GetSensors();
    }
}

3. Run the app (dotnet watch). Display should be identical, but the code is much better organized.

Activity 5: Async/Await - Simulating Loading (20 min)

In real situations, data comes from an API or database. These operations take time. It is imperative to make them asynchronous to avoid blocking the UI.

1. In ISensorService.cs, modify the GetSensors signature:

// Before:
List<SensorData> GetSensors();

// After:
Task<List<SensorData>> GetSensorsAsync();

2. Implement it in SensorService.cs with simulated delay:

public async Task<List<SensorData>> GetSensorsAsync()
{
    // Simulate 2 seconds of network latency
    await Task.Delay(2000);
    return _sensors;
}

3. Update MyDashboard.razor to use this asynchronous method:

@code {
    private List<SensorData> _sensors = new();
    private bool _isLoading = true;

    // OnInitializedAsync instead of OnInitialized
    protected override async Task OnInitializedAsync()
    {
        _sensors = await SensorService.GetSensorsAsync();
        _isLoading = false; // Loading is complete
    }
}

4. Add a loading indicator in HTML:

@if (_isLoading)
{
    <p><em>Loading data...</em></p>
}
else
{
    <!-- The data table -->
    <table> ... </table>
}

5. Restart. You should see the "Loading..." message for 2 seconds before the data displays!

🚀 Application Exercises (Autonomous)

Exercise 1: Adding Data via a New Page

Goal: Create a dedicated page for adding a sensor, with navigation.

  1. Create a new page AddSensor.razor with the route @page "/add-sensor".
  2. In MyDashboard.razor, add a button to navigate to this page.
  3. In AddSensor.razor, create the form (inputs + submit button).
  4. Inject NavigationManager to handle redirection.
  5. On click, call the service and then return to the dashboard.

💡 Hint: Navigation

<!-- At the top of the file -->
@inject NavigationManager NavManager

@code {
    private void NavigateBack()
    {
        // Return to dashboard
        NavManager.NavigateTo("/dashboard");
    }
}

Bonus Activity: Experimenting with Lifetimes

To better understand, create a simple new service:

  1. Create UserCounterService.cs with a Count property and an Increment() method.
  2. Register it in Program.cs with different lifetimes.
  3. Inject it into your Dashboard.

🔎 Experiment 1: Singleton

Use builder.Services.AddSingleton<UserCounterService>().

Open your dashboard in two different tabs. Increment in one, refresh the other.

❓ Question: Is the counter shared?

🔎 Experiment 2: Scoped

Change to builder.Services.AddScoped<UserCounterService>() and restart.

❓ Question: Is the counter shared?

🔎 Experiment 3: Transient

Use builder.Services.AddTransient<UserCounterService>().

Inject the service twice in the same page: @inject UserCounterService S1 and @inject UserCounterService S2. Display counters for S1 and S2.

❓ Question: Do S1 and S2 share the same value?

LAB 4 Synthesis

🎙️ Sélectionner une voix

🎙️ Select a voice