🌐
🔍 100%
👁️

🎙️ Sélectionner une voix

🎙️ Select a voice

TP 7 : Formulaires & Validation de Données (Écriture)

Module : Programmation .Net C#

Durée : 2h

Objectif : Créer un formulaire interactif (EditForm) unifié pour ajouter ou modifier des données (CRUD). Mettre en place la validation automatique (champs requis, plages de valeurs) via les Data Annotations et lier la clé étrangère LocationId.

Pré-requis :

  • Avoir terminé le TP 6 (L'affichage du Dashboard avec les jointures Include(s => s.Location) fonctionne).
  • Comprendre le rôle des Data Annotations (attributs comme [Required]).

Activité 1 : Préparation du Service pour l'écriture (20 min)

Dans le TP 6, nous avons géré la lecture (Read). Pour avoir un CRUD complet (Create, Read, Update, Delete), notre service doit pouvoir interroger la liste des lieux (Locations), récupérer un capteur spécifique, et mettre à jour la base de données.

  1. Ouvrez l'interface Services/ISensorService.cs et ajoutez ces signatures :
    Task<List<Location>> GetLocationsAsync();
    Task<SensorData?> GetSensorByIdAsync(int id);
    Task AddSensorAsync(SensorData sensor);
    Task UpdateSensorAsync(SensorData sensor);
    Task DeleteSensorAsync(int id);
  2. Ouvrez Services/SensorService.cs et implémentez ces méthodes. Notez l'utilisation de FindAsync qui est très optimisé pour chercher par Clé Primaire :
    public async Task<List<Location>> GetLocationsAsync()
    {
        return await _context.Locations.ToListAsync();
    }
    
    public async Task<SensorData?> GetSensorByIdAsync(int id)
    {
        // FindAsync cherche directement par la Clé Primaire (Id)
        return await _context.Sensors.FindAsync(id);
    }
    
    public async Task AddSensorAsync(SensorData sensor)
    {
        sensor.LastUpdate = DateTime.Now;
        
        // Historisation de la valeur initiale (TP5)
        sensor.Values.Add(new SensorValueHistory {
            MeasuredValue = sensor.Value,
            Date = DateTime.Now
        });
    
        _context.Sensors.Add(sensor);
        await _context.SaveChangesAsync();
    }
    
    public async Task UpdateSensorAsync(SensorData sensor)
    {
        sensor.LastUpdate = DateTime.Now; // Mise à jour de la date
        
        // Ajout à l'historique lors d'une modification (TP5)
        sensor.Values.Add(new SensorValueHistory {
            MeasuredValue = sensor.Value,
            Date = DateTime.Now
        });
    
        _context.Sensors.Update(sensor);
        await _context.SaveChangesAsync();
    }
    
    public async Task DeleteSensorAsync(int id)
    {
        var sensor = await _context.Sensors.FindAsync(id);
        if (sensor != null)
        {
            _context.Sensors.Remove(sensor);
            await _context.SaveChangesAsync();
        }
    }

Activité 2 : Le Formulaire Unifié (EditSensor) (35 min)

En Blazor, on n'utilise pas la balise HTML standard <form>, mais le composant <EditForm> qui gère nativement la validation. Nous allons créer une seule page capable de gérer à la fois la création et la modification grâce au passage d'un paramètre optionnel dans l'URL.

  1. Dans le dossier Pages, créez un nouveau composant EditSensor.razor.
  2. Mettez en place l'architecture du formulaire en déclarant les deux routes possibles :
    @page "/edit-sensor"
    @page "/edit-sensor/{Id:int}"
    @rendermode InteractiveServer
    @using DashboardData.Models
    @using DashboardData.Services
    @inject ISensorService SensorService
    @inject NavigationManager NavigationManager
    
    <!-- Titre dynamique selon le mode -->
    <h3>@(Id.HasValue ? "Modifier le Capteur" : "Ajouter un Nouveau Capteur")</h3>
    
    @if (currentSensor == null)
    {
        <p><em>Chargement du formulaire...</em></p>
    }
    else
    {
        <EditForm Model="currentSensor" OnValidSubmit="HandleValidSubmit">
            <!-- Active la validation basée sur les[Data Annotations] du modèle -->
            <DataAnnotationsValidator />
            <ValidationSummary class="text-danger" />
    
            <div class="mb-3">
                <label class="form-label">Nom du capteur :</label>
                <!-- InputText remplace <input type="text"> -->
                <InputText @bind-Value="currentSensor.Name" class="form-control" />
                <ValidationMessage For="@(() => currentSensor.Name)" class="text-danger" />
            </div>
    
            <div class="mb-3">
                <label class="form-label">Localisation :</label>
                <!-- InputSelect se lie à la clé étrangère LocationId -->
                <InputSelect @bind-Value="currentSensor.LocationId" class="form-select">
                    <option value="0">-- Sélectionnez un lieu --</option>
                    @foreach (var loc in Locations)
                    {
                        <option value="@loc.Id">@loc.Name (@loc.Building)</option>
                    }
                </InputSelect>
                <ValidationMessage For="@(() => currentSensor.LocationId)" class="text-danger" />
            </div>
    
            <div class="mb-3">
                <label class="form-label">Valeur encours :</label>
                <InputNumber @bind-Value="currentSensor.Value" class="form-control" />
                <ValidationMessage For="@(() => currentSensor.Value)" class="text-danger" />
            </div>
    
            <button type="submit" class="btn btn-primary">Enregistrer</button>
            <a href="/dashboard" class="btn btn-secondary">Annuler</a>
        </EditForm>
    }

Activité 3 : Optimisation de l'Initialisation et Sauvegarde (25 min)

Nous allons maintenant gérer l'état de notre variable currentSensor. Si un Id est fourni dans l'URL, on le charge depuis la base. Sinon, on instancie un nouvel objet vide.

  1. Toujours dans EditSensor.razor, ajoutez le bloc @code :
    @code {
        [Parameter]
        public int? Id { get; set; }
    
        private SensorData? currentSensor;
        private List<Location> Locations = new();
    
        protected override async Task OnInitializedAsync()
        {
            // 1. Chargement des lieux pour remplir la liste déroulante
            Locations = await SensorService.GetLocationsAsync();
    
            // 2. Initialisation du capteur courant
            if (Id.HasValue)
            {
                // Mode Édition : On récupère les données de la base
                currentSensor = await SensorService.GetSensorByIdAsync(Id.Value);
            }
            
            // Mode Création (ou si l\'ID fourni est introuvable) : on instancie un objet vide
            if (currentSensor == null)
            {
                currentSensor = new SensorData();
            }
        }
    
        private async Task HandleValidSubmit()
        {
            // Cette méthode n\'est appelée QUE si le formulaire est valide
            if (Id.HasValue)
            {
                // Mise à jour (UPDATE)
                await SensorService.UpdateSensorAsync(currentSensor);
            }
            else
            {
                // Ajout (INSERT)
                await SensorService.AddSensorAsync(currentSensor);
            }
            
            // Redirection vers le Dashboard après succès
            NavigationManager.NavigateTo("/dashboard");
        }
    }
  2. Lien depuis le Dashboard : Ouvrez Pages/MyDashboard.razor et ajoutez les boutons pour naviguer vers ce nouveau formulaire.
    • Bouton d'ajout (au-dessus du tableau) :
      <a href="/edit-sensor" class="btn btn-success mb-3">+ Nouveau Capteur</a>
    • Bouton de modification (dans la boucle @foreach du tableau) :
      <td>
          <a href="/edit-sensor/@sensor.Id" class="btn btn-sm btn-outline-primary">Éditer</a>
      </td>

🚀 Exercices d'application (En autonomie)

Exercice 1 : Sécurisation avec les Data Annotations

Ouvrez votre modèle Models/SensorData.cs. Actuellement, rien n'empêche un utilisateur de saisir une valeur aberrante. Ajoutez les attributs de validation suivants sur vos propriétés :

  1. Le Name doit avoir une limite : [StringLength(50, MinimumLength = 3, ErrorMessage="Le nom doit faire entre 3 et 50 caractères.")].
  2. La Value doit avoir une limite logique : [Range(-50.0, 150.0)].
  3. Le LocationId doit forcer un choix : [Range(1, int.MaxValue, ErrorMessage = "Veuillez sélectionner un lieu valide.")] pour forcer le choix dans la liste déroulante (qui a la valeur "0" par défaut).

Testez votre formulaire : Essayez de valider avec un nom trop court, une valeur de 200, ou sans sélectionner de lieu. Blazor affichera les erreurs en rouge automatiquement sans code JavaScript additionnel !

Exercice 2 : Suppression d'un capteur (Delete)

Complétez votre tableau dans MyDashboard.razor pour permettre la suppression.

  1. Ajoutez un bouton "Supprimer" à côté du bouton "Éditer".
  2. Ce bouton doit déclencher une méthode locale DeleteSensor(int id) au clic :
    <button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteSensor(sensor.Id)">Supprimer</button>
  3. Dans la zone @code de MyDashboard, écrivez la méthode DeleteSensor : elle doit appeler SensorService.DeleteSensorAsync(id) puis recharger la liste des capteurs (await LoadAll();) pour que la ligne disparaisse de l'écran instantanément.

🏁 Synthèse du TP 7

LAB 7: Forms & Data Validation (Writing)

Module: .Net C# Programming

Duration: 2h

Objective: Create a unified interactive form (EditForm) to add or modify data (CRUD). Implement automatic validation (required fields, value ranges) using Data Annotations and bind the foreign key LocationId.

Prerequisites:

  • Completed LAB 6 (Displaying the Dashboard with JOINs Include(s => s.Location) is working).
  • Understanding the role of Data Annotations (attributes like [Required]).

Activity 1: Preparing the Service for Writing (20 min)

In LAB 6, we managed reading data (Read). To have a complete CRUD (Create, Read, Update, Delete), our service must be able to query the list of locations, retrieve a specific sensor, and update the database.

  1. Open the interface Services/ISensorService.cs and add these signatures:
    Task<List<Location>> GetLocationsAsync();
    Task<SensorData?> GetSensorByIdAsync(int id);
    Task AddSensorAsync(SensorData sensor);
    Task UpdateSensorAsync(SensorData sensor);
    Task DeleteSensorAsync(int id);
  2. Open Services/SensorService.cs and implement these methods. Note the use of FindAsync which is highly optimized for searching by Primary Key:
    public async Task<List<Location>> GetLocationsAsync()
    {
        return await _context.Locations.ToListAsync();
    }
    
    public async Task<SensorData?> GetSensorByIdAsync(int id)
    {
        // FindAsync searches directly by Primary Key (Id)
        return await _context.Sensors.FindAsync(id);
    }
    
    public async Task AddSensorAsync(SensorData sensor)
    {
        sensor.LastUpdate = DateTime.Now;
        
        // Archiving the initial value (from LAB 5)
        sensor.Values.Add(new SensorValueHistory {
            MeasuredValue = sensor.Value,
            Date = DateTime.Now
        });
    
        _context.Sensors.Add(sensor);
        await _context.SaveChangesAsync();
    }
    
    public async Task UpdateSensorAsync(SensorData sensor)
    {
        sensor.LastUpdate = DateTime.Now; // Update the date
        
        // Adding to the history upon modification (from LAB 5)
        sensor.Values.Add(new SensorValueHistory {
            MeasuredValue = sensor.Value,
            Date = DateTime.Now
        });
    
        _context.Sensors.Update(sensor);
        await _context.SaveChangesAsync();
    }
    
    public async Task DeleteSensorAsync(int id)
    {
        var sensor = await _context.Sensors.FindAsync(id);
        if (sensor != null)
        {
            _context.Sensors.Remove(sensor);
            await _context.SaveChangesAsync();
        }
    }

Activity 2: The Unified Form (EditSensor) (35 min)

In Blazor, we don't use the standard HTML <form> tag, but the <EditForm> component which natively handles validation. We will create a single page completely capable of handling both creation and modification through passing an optional parameter in the URL.

  1. In the Pages folder, create a new component EditSensor.razor.
  2. Set up the form architecture by declaring both possible routes:
    @page "/edit-sensor"
    @page "/edit-sensor/{Id:int}"
    @rendermode InteractiveServer
    @using DashboardData.Models
    @using DashboardData.Services
    @inject ISensorService SensorService
    @inject NavigationManager NavigationManager
    
    <!-- Dynamic title based on mode -->
    <h3>@(Id.HasValue ? "Edit Sensor" : "Add New Sensor")</h3>
    
    @if (currentSensor == null)
    {
        <p><em>Loading form...</em></p>
    }
    else
    {
        <EditForm Model="currentSensor" OnValidSubmit="HandleValidSubmit">
            <!-- Activates validation based on model\'s [Data Annotations] -->
            <DataAnnotationsValidator />
            <ValidationSummary class="text-danger" />
    
            <div class="mb-3">
                <label class="form-label">Sensor Name:</label>
                <!-- InputText replaces <input type="text"> -->
                <InputText @bind-Value="currentSensor.Name" class="form-control" />
                <ValidationMessage For="@(() => currentSensor.Name)" class="text-danger" />
            </div>
    
            <div class="mb-3">
                <label class="form-label">Location:</label>
                <!-- InputSelect binds to the LocationId foreign key -->
                <InputSelect @bind-Value="currentSensor.LocationId" class="form-select">
                    <option value="0">-- Select a location --</option>
                    @foreach (var loc in Locations)
                    {
                        <option value="@loc.Id">@loc.Name (@loc.Building)</option>
                    }
                </InputSelect>
                <ValidationMessage For="@(() => currentSensor.LocationId)" class="text-danger" />
            </div>
    
            <div class="mb-3">
                <label class="form-label">Current Value:</label>
                <InputNumber @bind-Value="currentSensor.Value" class="form-control" />
                <ValidationMessage For="@(() => currentSensor.Value)" class="text-danger" />
            </div>
    
            <button type="submit" class="btn btn-primary">Save</button>
            <a href="/dashboard" class="btn btn-secondary">Cancel</a>
        </EditForm>
    }

Activity 3: Optimization of Initialization and Saving (25 min)

We will now manage the state of our currentSensor variable. If an Id is provided in the URL, we load it from the database. Otherwise, we instantiate a new empty object.

  1. Still in EditSensor.razor, add the @code block:
    @code {
        [Parameter]
        public int? Id { get; set; }
    
        private SensorData? currentSensor;
        private List<Location> Locations = new();
    
        protected override async Task OnInitializedAsync()
        {
            // 1. Loading locations to fill the dropdown list
            Locations = await SensorService.GetLocationsAsync();
    
            // 2. Initializing the current sensor
            if (Id.HasValue)
            {
                // Edit Mode: Fetch data from the database
                currentSensor = await SensorService.GetSensorByIdAsync(Id.Value);
            }
            
            // Creation Mode (or if the provided ID is not found): instantiate an empty object
            if (currentSensor == null)
            {
                currentSensor = new SensorData();
            }
        }
    
        private async Task HandleValidSubmit()
        {
            // This method is called ONLY if the form is valid
            if (Id.HasValue)
            {
                // Update (UPDATE)
                await SensorService.UpdateSensorAsync(currentSensor);
            }
            else
            {
                // Add (INSERT)
                // (Assuming AddSensorAsync is implemented in ISensorService)
                await SensorService.AddSensorAsync(currentSensor);
            }
            
            // Redirect to Dashboard after success
            NavigationManager.NavigateTo("/dashboard");
        }
    }
  2. Link from the Dashboard: Open Pages/MyDashboard.razor and add buttons to navigate to this new form.
    • Add button (above the table):
      <a href="/edit-sensor" class="btn btn-success mb-3">+ New Sensor</a>
    • Edit button (inside the @foreach table loop):
      <td>
          <a href="/edit-sensor/@sensor.Id" class="btn btn-sm btn-outline-primary">Edit</a>
      </td>

🚀 Application Exercises (Self-study)

Exercise 1: Securing with Data Annotations

Open your Models/SensorData.cs model. Currently, nothing prevents a user from typing an absurd value. Add the following validation attributes to your properties:

  1. The Name must have a limit: [StringLength(50, MinimumLength = 3, ErrorMessage="The name must be between 3 and 50 characters.")].
  2. The Value must have a logical limit: [Range(-50.0, 150.0)].
  3. The LocationId must force a choice: [Range(1, int.MaxValue, ErrorMessage = "Please select a valid location.")] to force the dropdown list choice (which defaults to "0").

Test your form: Try submitting with a name that is too short, a value of 200, or without selecting a location. Blazor will display the error messages in red automatically, with zero extra JavaScript!

Exercise 2: Deleting a sensor (Delete)

Complete your table in MyDashboard.razor to allow deletion.

  1. Add a "Delete" button next to the "Edit" button.
  2. This button must trigger a local DeleteSensor(int id) method on click:
    <button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteSensor(sensor.Id)">Delete</button>
  3. In the @code zone of MyDashboard, write the DeleteSensor method: it must call SensorService.DeleteSensorAsync(id) then reload the list of sensors (await LoadAll();) so the row instantly disappears from the screen.

🏁 LAB 7 Synthesis