Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 618 minIntermédiaire

Liaison de données : BindingSource, BindingList et DataGridView

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • afficher une liste d'objets dans un DataGridView ;
  • choisir entre List, BindingList, BindingSource et DataTable ;
  • configurer les colonnes, leur titre et leur format ;
  • lier des zones de texte à la ligne sélectionnée ;
  • filtrer et trier une liste avec LINQ, sans recharger la grille depuis la base.

4 exercices corrigés1 schéma

La liaison de données (data binding) relie un contrôle à une source de données : le contrôle affiche les valeurs de la source, et, selon le réglage, la source reçoit les modifications faites dans le contrôle. En WinForms, elle permet d'afficher une liste d'objets dans un DataGridView en une ligne, de suivre la ligne sélectionnée et de garder plusieurs contrôles synchronisés, sans recopier les valeurs à la main.

La version la plus simple : DataSource

Affecter une liste à la propriété DataSource d'un DataGridView suffit à l'afficher : la grille crée une colonne par propriété publique et une ligne par objet.

csharp
List<Patient> patients = await _service.ListerAsync();
dgvPatients.DataSource = patients;

C'est pratique pour un premier essai, mais trois limites apparaissent vite :

  • la grille ne voit pas les ajouts et suppressions faits ensuite dans une List<T> : il faut réaffecter DataSource ;
  • les colonnes portent le nom des propriétés (DateNaissance, NumeroSecu) et s'affichent dans un ordre imprévu ;
  • pour savoir quel patient est sélectionné, il faut fouiller dans dgvPatients.CurrentRow.DataBoundItem.

Les trois outils de ce chapitre lèvent ces limites.

BindingList : une liste qui prévient la grille

BindingList<T> (espace de noms System.ComponentModel) est une liste qui déclenche l'événement ListChanged à chaque ajout, suppression ou remplacement. Les contrôles liés l'écoutent et se mettent à jour seuls.

csharp
private readonly BindingList<Patient> _affiches = [];

_affiches.Add(nouveauPatient);   // la grille affiche la nouvelle ligne immédiatement
_affiches.Remove(patient);       // la ligne disparaît

Elle prévient de changements dans la liste, pas de changements dans un objet : si tu modifies patient.Nom, la grille ne le sait pas. Deux solutions : appeler _affiches.ResetItem(index) (ou ResetBindings() pour tout redessiner), ou faire implémenter à la classe l'interface INotifyPropertyChanged, que BindingList écoute.

csharp
public class PatientObservable : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    public string Nom
    {
        get;
        set
        {
            if (field == value) return;
            field = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Nom)));
        }
    } = "";
}

Dans GSB Ordonnances, les objets ne sont jamais modifiés dans la grille : on recharge la liste après chaque enregistrement. BindingList suffit, sans INotifyPropertyChanged.

BindingSource : l'intermédiaire entre la liste et les contrôles

Le composant BindingSource se place entre la source de données et les contrôles. Il connaît la ligne courante, la partage entre tous les contrôles qui lui sont liés, et signale les changements de sélection.

ListerAsync()le service renvoieList<Patient>BindingList<Patient>signale les ajoutset les suppressionsbsPatientsBindingSourceCurrent, PositiondgvPatientstoutes les lignestxtNomDetailNom de la ligne courantelblNaissancesa date de naissanceSélectionner une ligne de la grille change Current :la zone de texte et le label suivent sans une ligne de code.
De la liste aux contrôles : BindingList, BindingSource et contrôles liés

Pose un BindingSource depuis la Boîte à outils (rubrique Données), renomme-le bsPatients, puis relie le tout dans le constructeur :

csharp
private readonly BindingList<Patient> _affiches = [];

public FrmPatients(PatientService service, Utilisateur utilisateur)
{
    InitializeComponent();
    _service = service;

    bsPatients.DataSource = _affiches;
    dgvPatients.DataSource = bsPatients;
}

Ce que BindingSource t'apporte :

MembreRôle
CurrentL'objet de la ligne sélectionnée (object?), ou null si la liste est vide
PositionL'indice de la ligne courante
MoveNext(), MovePrevious(), MoveFirst(), MoveLast()Naviguer, par exemple avec des boutons « Suivant » et « Précédent »
CountLe nombre d'éléments
CurrentChangedÉvénement : la ligne courante a changé
ResetBindings(false)Redessiner les contrôles liés

Lire le patient sélectionné devient une ligne, sûre même quand la grille est vide :

csharp
if (bsPatients.Current is not Patient patient) return;

Configurer les colonnes du DataGridView

Pour choisir les colonnes, leur titre, leur ordre et leur format, désactive la génération automatique et crée les colonnes dans le concepteur (clic sur la petite flèche du DataGridView, puis Modifier les colonnes). Chaque colonne a une propriété DataPropertyName : le nom de la propriété de l'objet à afficher.

csharp
dgvPatients.AutoGenerateColumns = false;   // à placer avant DataSource

colNom.DataPropertyName = nameof(Patient.Nom);
colPrenom.DataPropertyName = nameof(Patient.Prenom);
colNaissance.DataPropertyName = nameof(Patient.DateNaissance);
colNaissance.DefaultCellStyle.Format = "dd/MM/yyyy";
colSecu.DataPropertyName = nameof(Patient.NumeroSecu);

Le titre visible se règle avec HeaderText (« Prénom », « N° de sécurité sociale ») ; Visible = false cache une colonne sans la supprimer (l'identifiant, par exemple). DefaultCellStyle.Format applique un format .NET : dd/MM/yyyy pour une date, C2 pour un montant en euros, N0 pour un entier avec séparateur de milliers.

Lier une zone de texte à la ligne courante

Un écran maître-détail affiche la liste en haut et le détail de la ligne sélectionnée en dessous. Avec un BindingSource, c'est une ligne par contrôle :

csharp
txtNomDetail.DataBindings.Add(nameof(TextBox.Text), bsPatients, nameof(Patient.Nom));
lblNaissance.DataBindings.Add(nameof(Label.Text), bsPatients, nameof(Patient.DateNaissance),
    formattingEnabled: true, DataSourceUpdateMode.Never, nullValue: "inconnue", formatString: "dd/MM/yyyy");

Le premier argument est la propriété du contrôle, le deuxième la source, le troisième la propriété de l'objet. Quand la sélection change dans la grille, Current change et les deux contrôles se mettent à jour seuls. Avec le mode par défaut (OnValidation), une modification de txtNomDetail est recopiée dans l'objet quand la zone perd le focus ; DataSourceUpdateMode.Never rend la liaison en lecture seule.

Pour réagir toi-même au changement de ligne (charger les ordonnances du patient sélectionné, par exemple), abonne-toi à CurrentChanged :

csharp
private async void bsPatients_CurrentChanged(object? sender, EventArgs e)
{
    if (bsPatients.Current is Patient patient)
    {
        dgvOrdonnances.DataSource = await _ordonnanceService.ListerPourPatientAsync(patient.NumPatient);
    }
}

LINQ en bref : filtrer et trier une liste

LINQ (Language Integrated Query) est un ensemble de méthodes de C# qui interrogent une collection en mémoire comme une requête SQL interroge une table : filtrer, trier, transformer, compter. Chaque méthode reçoit une expression lambda, une petite fonction écrite en ligne : p => p.Nom se lit « pour un patient p, son nom ». Le cours de programmation objet présente LINQ plus en détail (collections, génériques et LINQ).

Les exemples portent sur la liste _tous, qui contient les trois patients du jeu d'essai : Marie Dupont (née le 12/05/1990), Léa Petit (née le 30/09/2012, sans numéro de sécurité sociale) et Hugo Bernard (né le 03/11/2008).

csharp
DateTime jour = new DateTime(2026, 11, 2);

List<Patient> mineurs = _tous.Where(p => p.Age(jour) < 18).ToList();        // Petit, Bernard
var sansSecu = _tous.Where(p => p.NumeroSecu is null);                       // Petit
var parNom = _tous.OrderBy(p => p.Nom);                                      // Bernard, Dupont, Petit
var plusJeunesDabord = _tous.OrderByDescending(p => p.DateNaissance);        // Petit, Bernard, Dupont
var noms = _tous.Select(p => $"{p.Nom} {p.Prenom}");                         // "Dupont Marie", "Petit Léa", "Bernard Hugo"
bool existe = _tous.Any(p => p.Nom == "Petit");                              // true
int avecSecu = _tous.Count(p => p.NumeroSecu is not null);                   // 2
Patient? deux = _tous.FirstOrDefault(p => p.NumPatient == 2);                // Léa Petit (null si aucun)
MéthodeRôleÉquivalent SQL
Where(p => condition)garde les éléments qui vérifient la conditionWHERE
OrderBy(p => clé), OrderByDescending, ThenBytrieORDER BY … ASC / DESC
Select(p => valeur)transforme chaque élémentla liste des colonnes du SELECT
Any(p => condition)y a-t-il au moins un élément qui vérifie ?EXISTS
Count(p => condition)combien d'éléments vérifient ?COUNT(*) avec un WHERE
FirstOrDefault(p => condition)le premier qui vérifie, ou nullLIMIT 1
ToList()exécute la requête et range le résultat dans une liste

Age renvoie un int? : pour un patient sans date de naissance, p.Age(jour) < 18 vaut false, et il n'est pas compté parmi les mineurs. Where, OrderBy et Select ne calculent rien tout de suite : ils renvoient une requête (IEnumerable<T>) qui s'exécute quand on la parcourt avec foreach ou qu'on appelle ToList(). Le même LINQ sert à interroger la base avec Entity Framework, qui le traduit en SQL (chapitre 10).

Rechercher sans tout recharger

BindingSource a une propriété Filter, mais elle ne fonctionne qu'avec une source qui sait filtrer, comme un DataView ; avec une BindingList<T>, elle n'a aucun effet. Pour une liste d'objets, filtre avec LINQ : garde la liste complète dans un champ, et recopie dans la BindingList affichée les éléments qui correspondent.

csharp
private List<Patient> _tous = [];                         // tout ce que la base a renvoyé
private readonly BindingList<Patient> _affiches = [];     // ce que la grille montre

private void Filtrer()
{
    string texte = txtRecherche.Text.Trim();
    IEnumerable<Patient> resultat = _tous.Where(p =>
        p.Nom.Contains(texte, StringComparison.CurrentCultureIgnoreCase)
        || (p.Prenom ?? "").Contains(texte, StringComparison.CurrentCultureIgnoreCase));

    _affiches.RaiseListChangedEvents = false;   // pas de redessin à chaque Add
    _affiches.Clear();
    foreach (Patient p in resultat) _affiches.Add(p);
    _affiches.RaiseListChangedEvents = true;
    _affiches.ResetBindings();                  // un seul redessin à la fin

    lblStatut.Text = $"{_affiches.Count} patient(s) sur {_tous.Count}";
}

private void txtRecherche_TextChanged(object? sender, EventArgs e) => Filtrer();
  • Contains(texte, StringComparison.CurrentCultureIgnoreCase) ignore la casse : « dup » trouve « Dupont ».
  • Un texte vide est contenu dans toutes les chaînes : effacer la recherche réaffiche tout le monde.
  • Couper RaiseListChangedEvents pendant le remplissage évite des centaines de redessins pour une liste longue.

Filtrer en mémoire convient à quelques milliers de lignes. Au-delà, la recherche se fait dans la base, avec un WHERE nom LIKE @debut paramétré (chapitre 9).

L'autre approche : DataTable et DataView

Beaucoup de projets et de sujets anciens remplissent la grille avec un DataTable, une table en mémoire chargée directement depuis la base par un adaptateur :

csharp
var adaptateur = new MySqlDataAdapter("SELECT numPatient, nom, prenom FROM patient", connexion);
var table = new DataTable();
adaptateur.Fill(table);

bsPatients.DataSource = table;
bsPatients.Filter = "nom LIKE 'Du%'";   // ici, Filter fonctionne : la source est un DataView
bsPatients.Sort = "nom ASC";
dgvPatients.DataSource = bsPatients;
Liste d'objets (BindingList<Patient>)DataTable
CodeDes classes métier typées, vérifiées par le compilateurDes colonnes désignées par des chaînes (row["nom"])
CouchesLe formulaire ignore tout de la baseLe formulaire manipule la structure de la table
Filtre et tri intégrésNon (LINQ)Oui (Filter, Sort, tri par clic sur l'en-tête)
Quand l'utiliserTon projet, avec une architecture en couchesUn écran de consultation rapide, du code existant à maintenir

Pour ton projet d'E6, préfère les objets : ils s'intègrent à l'architecture en couches du chapitre 8 et se testent. Sache lire un DataTable : tu en croiseras.

Exercices

Exercice 1 sur 4 Le patient sélectionné Réussi À revoir

Écris le gestionnaire CurrentChanged de bsPatients qui affiche dans lblAge « 36 ans » (l'âge calculé par patient.Age(DateTime.Today)), ou « âge inconnu » quand la date de naissance manque, ou un texte vide quand aucun patient n'est sélectionné.

Voir la réponseMasquer la réponse de l'exercice 1
csharp
private void bsPatients_CurrentChanged(object? sender, EventArgs e)
{
    if (bsPatients.Current is not Patient patient)
    {
        lblAge.Text = "";
        return;
    }
    int? age = patient.Age(DateTime.Today);
    lblAge.Text = age is null ? "âge inconnu" : $"{age} ans";
}

Age renvoie un int? : null quand la date de naissance est inconnue.

Tu avais trouvé ?

Exercice 2 sur 4 Pourquoi la grille ne bouge pas ? Réussi À revoir

Un étudiant a écrit ce code. Après un clic sur « Ajouter », le patient est bien enregistré en base, mais il n'apparaît pas dans la grille. Explique, et propose deux corrections.

csharp
private List<Patient> _patients = [];

private async void FrmPatients_Load(object? sender, EventArgs e)
{
    _patients = await _service.ListerAsync();
    dgvPatients.DataSource = _patients;
}

private async void btnAjouter_Click(object? sender, EventArgs e)
{
    var p = new Patient { Nom = txtNom.Text };
    await _service.EnregistrerAsync(p);
    _patients.Add(p);
}
Voir la réponseMasquer la réponse de l'exercice 2

Une List<T> ne prévient personne quand on lui ajoute un élément : la grille ne sait pas qu'elle doit afficher une ligne de plus.

  • Correction 1 : déclarer le champ en BindingList<Patient> (private BindingList<Patient> _patients = [];), puis, au chargement, _patients = new BindingList<Patient>(await _service.ListerAsync()); dgvPatients.DataSource = _patients;. Add déclenche alors ListChanged et la grille ajoute la ligne.
  • Correction 2 : recharger la liste depuis la base après l'enregistrement (await ChargerAsync();). C'est ce que fait GSB Ordonnances : la grille montre exactement ce que contient la base, y compris les patients ajoutés par un autre poste.

Tu avais trouvé ?

Exercice 3 sur 4 Les colonnes de la grille des ordonnances Réussi À revoir

La méthode ListerLisiblesAsync() renvoie une List<OrdonnanceLisible>, où OrdonnanceLisible est le record (int NumOrdonnance, DateTime DateEmission, string Patient, string Medecin). La grille dgvOrdonnances doit afficher trois colonnes, dans cet ordre : « Date » au format 15/10/2026 09:30, « Patient », « Médecin ». Le numéro ne s'affiche pas. Écris le code de configuration, avec des colonnes colDate, colPatient et colMedecin déjà créées dans le concepteur.

Voir la réponseMasquer la réponse de l'exercice 3
csharp
dgvOrdonnances.AutoGenerateColumns = false;
colDate.DataPropertyName = nameof(OrdonnanceLisible.DateEmission);
colDate.HeaderText = "Date";
colDate.DefaultCellStyle.Format = "dd/MM/yyyy HH:mm";
colPatient.DataPropertyName = nameof(OrdonnanceLisible.Patient);
colPatient.HeaderText = "Patient";
colMedecin.DataPropertyName = nameof(OrdonnanceLisible.Medecin);
colMedecin.HeaderText = "Médecin";

dgvOrdonnances.DataSource = await _ordonnanceService.ListerLisiblesAsync();

HH (majuscules) donne l'heure sur 24 heures ; hh donnerait 09 pour 21 h. Les propriétés d'un record sont des propriétés publiques ordinaires : la liaison fonctionne.

Tu avais trouvé ?

Exercice 4 sur 4 Filtrer par médecin Réussi À revoir

Dans la fenêtre des ordonnances, _toutes contient toutes les OrdonnanceLisible chargées, _affichees est la BindingList<OrdonnanceLisible> liée à la grille, et cboFiltreMedecin propose « Tous » suivi des noms des médecins (des chaînes). Écris la méthode Filtrer() appelée par SelectedIndexChanged.

Voir la réponseMasquer la réponse de l'exercice 4
csharp
private void Filtrer()
{
    string? choix = cboFiltreMedecin.SelectedItem as string;
    IEnumerable<OrdonnanceLisible> resultat = choix is null || choix == "Tous"
        ? _toutes
        : _toutes.Where(o => o.Medecin == choix);

    _affichees.RaiseListChangedEvents = false;
    _affichees.Clear();
    foreach (OrdonnanceLisible o in resultat) _affichees.Add(o);
    _affichees.RaiseListChangedEvents = true;
    _affichees.ResetBindings();
}

Filtrer sur le nom affiché marche, mais deux médecins homonymes seraient confondus. Dans une vraie application, on lie plutôt la ComboBox à des objets Medecin (ValueMember = NumMedecin) et on filtre sur le numéro, que le record devrait alors contenir.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : async et await en WinForms : une interface qui ne se fige pasRevoir le sommaire du cours