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 :
Include(s => s.Location) fonctionne).
[Required]).
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.
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);
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();
}
}
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.
Pages, créez un nouveau composant EditSensor.razor.@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>
}
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.
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");
}
}
Pages/MyDashboard.razor et ajoutez
les boutons pour naviguer vers ce nouveau formulaire.
<a href="/edit-sensor" class="btn btn-success mb-3">+ Nouveau Capteur</a>
@foreach du tableau) :
<td>
<a href="/edit-sensor/@sensor.Id" class="btn btn-sm btn-outline-primary">Éditer</a>
</td>
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 :
[StringLength(50, MinimumLength = 3, ErrorMessage="Le nom doit faire entre 3 et 50 caractères.")].
[Range(-50.0, 150.0)].[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 !
Complétez votre tableau dans MyDashboard.razor pour permettre la suppression.
DeleteSensor(int id) au clic :
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteSensor(sensor.Id)">Supprimer</button>
@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.
<EditForm> : Remplace la balise HTML <form>.
Il intercepte la soumission
(OnValidSubmit) pour lancer la
validation avant l'enregistrement.<DataAnnotationsValidator /> :
Composant "magique" à insérer dans l'EditForm. Il lit les attributs ([Required],
[Range]) de votre modèle C# et active les règles de validation correspondantes.
<ValidationMessage For="..." /> : Affiche le message d'erreur
spécifique à un champ (généralement en rouge) si la règle de validation n'est pas respectée.<InputText>, <InputSelect>, etc. : Composants
Blazor qui se lient automatiquement (@bind-Value) aux
propriétés de votre modèle, y compris les clés étrangères (LocationId).@page) et servir à la fois pour la création et l'édition grâce au paramètre optionnel
[Parameter] public int? Id.
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:
Include(s => s.Location) is
working).[Required]).
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.
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);
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();
}
}
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.
Pages folder, create a new component EditSensor.razor.@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>
}
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.
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");
}
}
Pages/MyDashboard.razor and add buttons
to navigate to this new form.
<a href="/edit-sensor" class="btn btn-success mb-3">+ New Sensor</a>
@foreach table loop):
<td>
<a href="/edit-sensor/@sensor.Id" class="btn btn-sm btn-outline-primary">Edit</a>
</td>
Open your Models/SensorData.cs model. Currently, nothing prevents a user from typing an
absurd value. Add the following validation attributes to your properties:
[StringLength(50, MinimumLength = 3, ErrorMessage="The name must be between 3 and 50 characters.")].
[Range(-50.0, 150.0)].[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!
Complete your table in MyDashboard.razor to allow deletion.
DeleteSensor(int id) method on click:
<button class="btn btn-sm btn-outline-danger" @onclick="() => DeleteSensor(sensor.Id)">Delete</button>
@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.<EditForm>: Replaces the HTML <form> tag. It
intercepts the submission
(OnValidSubmit) to trigger
validation prior to saving.<DataAnnotationsValidator />: "Magic"
component to insert into the EditForm. It reads attributes ([Required],
[Range]) from your C# model and activates corresponding validation rules.
<ValidationMessage For="..." />: Displays the specific error
message for a field (usually in red) if the validation rule is broken.<InputText>, <InputSelect>, etc.: Blazor
components that automatically bind (@bind-Value) to properties
of your model, including foreign keys (LocationId).@page)
and serve both creation and editing modes thanks to the optional parameter
[Parameter] public int? Id.