Bien démarrer avec Windows Forms

Ce guide étape par étape montre comment construire une simple application Windows Forms (WinForms) soutenue par une base de données SQLite. L'application utilise Entity Framework Core (EF Core) pour charger les données de la base de données, suivre les changements apportés à ces données et persister ces changements dans la base de données.

Les captures d’écran et les listes de code de cette procédure pas à pas sont extraites de Visual Studio 2022 17.3.0.

Conseil

Vous pouvez afficher cet exemple sur GitHub.

Prérequis

Pour effectuer cette procédure pas à pas, vous devez avoir Visual Studio 2022 17.3 ou une version ultérieure installée avec la charge de travail de bureau .NET sélectionnée. Pour plus d’informations sur l’installation de la dernière version de Visual Studio, consultez Installer Visual Studio.

Création de l’application

  1. Ouvrez Visual Studio.

  2. Dans la fenêtre de démarrage, choisissez Créer un projet.

  3. Choisissez l’application Windows Forms, puis choisissez Suivant.

    Create a new Windows Forms project

  4. Sur l’écran suivant, nommez le projet, par exemple, GetStartedWinForms et choisissez Suivant.

  5. À l’écran suivant, choisissez la version .NET à utiliser. Cette procédure pas à pas a été créée avec .NET 7, mais elle doit également fonctionner avec les versions ultérieures.

  6. Cliquez sur Créer.

Installez les packages NuGet EF Core

  1. Cliquez avec le bouton droit sur la solution et choisissez Gérer les packages NuGet pour la solution...

    Manage NuGet Packages for Solution

  2. Choisissez l’onglet Parcourir et recherchez « Microsoft.EntityFrameworkCore.Sqlite ».

  3. Sélectionnez le package Microsoft.EntityFrameworkCore.Sqlite.

  4. Vérifiez le projet GetStartedWinForms dans le volet droit.

  5. Choisissez la version la plus récente. Pour utiliser une version préliminaire, vérifiez que la case Inclure la version préliminaire est cochée.

  6. Cliquez sur Install.

    Install the Microsoft.EntityFrameworkCore.Sqlite package

Remarque

Microsoft.EntityFrameworkCore.Sqlite est le package « fournisseur de base de données » pour l’utilisation d’EF Core avec une base de données SQLite. Des packages similaires sont disponibles pour d'autres systèmes de base de données. L'installation d'un package de fournisseurs de bases de données apporte automatiquement toutes les dépendances nécessaires à l'utilisation d'EF Core avec ce système de base de données. Cela inclut le package de base Microsoft.EntityFrameworkCore.

Définir un modèle

Dans ce guide, nous allons mettre en œuvre un modèle en utilisant « Code First ». Cela signifie que EF Core créera les tables et le schéma de la base de données sur la base des classes C# que vous définissez. Consultez Gestion des schémas de base de données pour voir comment utiliser une base de données existante à la place.

  1. Cliquez avec le bouton droit sur le projet, puis choisissez Ajouter, puis Classe... pour ajouter une nouvelle classe.

    Add new class

  2. Utilisez le nom de fichier Product.cs et remplacez le code de la classe par :

    using System.ComponentModel;
    
    namespace GetStartedWinForms;
    
    public class Product
    {
        public int ProductId { get; set; }
    
        public string? Name { get; set; }
    
        public int CategoryId { get; set; }
        public virtual Category Category { get; set; } = null!;
    }
    
  3. Répétez l’opération pour créer Category.cs à l'aide du code suivant :

    using Microsoft.EntityFrameworkCore.ChangeTracking;
    
    namespace GetStartedWinForms;
    
    public class Category
    {
        public int CategoryId { get; set; }
    
        public string? Name { get; set; }
    
        public virtual ObservableCollectionListSource<Product> Products { get; } = new();
    }
    

La propriété Products sur la classe Category et la propriété Category sur la classe Productsont appelées « navigations ». Dans EF Core, les navigations définissent une relation entre deux types d'entités. Dans ce cas, la navigation Product.Category fait référence à la catégorie à laquelle appartient un produit donné. De même, la navigation de collection Category.Products contient tous les produits d’une catégorie donnée.

Conseil

Lorsque vous utilisez Windows Forms, le ObservableCollectionListSource, qui implémente IListSource, peut être utilisé pour les navigations de collection. Ce n'est pas nécessaire, mais cela améliore l'expérience de la liaison bidirectionnelle de données.

Définir DbContext

Dans EF Core, une classe dérivée de DbContext est utilisée pour configurer des types d’entités dans un modèle et agir comme une session pour interagir avec la base de données. Dans le cas le plus simple, une classe DbContext :

  • Contient les propriétés DbSet pour chaque type d'entité du modèle.
  • Remplace la méthode OnConfiguring pour configurer le fournisseur de base de données et la chaîne de connexion à utiliser. Consultez Configurer DbContext pour plus d’informations.

Dans ce cas, la classe DbContext remplace également la méthode OnModelCreating pour fournir des exemples de données pour l’application.

Ajoutez une nouvelle classe ProductsContext.cs au projet avec le code suivant :

using Microsoft.EntityFrameworkCore;

namespace GetStartedWinForms;

public class ProductsContext : DbContext
{
    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseSqlite("Data Source=products.db");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Category>().HasData(
            new Category { CategoryId = 1, Name = "Cheese" },
            new Category { CategoryId = 2, Name = "Meat" },
            new Category { CategoryId = 3, Name = "Fish" },
            new Category { CategoryId = 4, Name = "Bread" });

        modelBuilder.Entity<Product>().HasData(
            new Product { ProductId = 1, CategoryId = 1, Name = "Cheddar" },
            new Product { ProductId = 2, CategoryId = 1, Name = "Brie" },
            new Product { ProductId = 3, CategoryId = 1, Name = "Stilton" },
            new Product { ProductId = 4, CategoryId = 1, Name = "Cheshire" },
            new Product { ProductId = 5, CategoryId = 1, Name = "Swiss" },
            new Product { ProductId = 6, CategoryId = 1, Name = "Gruyere" },
            new Product { ProductId = 7, CategoryId = 1, Name = "Colby" },
            new Product { ProductId = 8, CategoryId = 1, Name = "Mozzela" },
            new Product { ProductId = 9, CategoryId = 1, Name = "Ricotta" },
            new Product { ProductId = 10, CategoryId = 1, Name = "Parmesan" },
            new Product { ProductId = 11, CategoryId = 2, Name = "Ham" },
            new Product { ProductId = 12, CategoryId = 2, Name = "Beef" },
            new Product { ProductId = 13, CategoryId = 2, Name = "Chicken" },
            new Product { ProductId = 14, CategoryId = 2, Name = "Turkey" },
            new Product { ProductId = 15, CategoryId = 2, Name = "Prosciutto" },
            new Product { ProductId = 16, CategoryId = 2, Name = "Bacon" },
            new Product { ProductId = 17, CategoryId = 2, Name = "Mutton" },
            new Product { ProductId = 18, CategoryId = 2, Name = "Pastrami" },
            new Product { ProductId = 19, CategoryId = 2, Name = "Hazlet" },
            new Product { ProductId = 20, CategoryId = 2, Name = "Salami" },
            new Product { ProductId = 21, CategoryId = 3, Name = "Salmon" },
            new Product { ProductId = 22, CategoryId = 3, Name = "Tuna" },
            new Product { ProductId = 23, CategoryId = 3, Name = "Mackerel" },
            new Product { ProductId = 24, CategoryId = 4, Name = "Rye" },
            new Product { ProductId = 25, CategoryId = 4, Name = "Wheat" },
            new Product { ProductId = 26, CategoryId = 4, Name = "Brioche" },
            new Product { ProductId = 27, CategoryId = 4, Name = "Naan" },
            new Product { ProductId = 28, CategoryId = 4, Name = "Focaccia" },
            new Product { ProductId = 29, CategoryId = 4, Name = "Malted" },
            new Product { ProductId = 30, CategoryId = 4, Name = "Sourdough" },
            new Product { ProductId = 31, CategoryId = 4, Name = "Corn" },
            new Product { ProductId = 32, CategoryId = 4, Name = "White" },
            new Product { ProductId = 33, CategoryId = 4, Name = "Soda" });
    }
}

Veillez à générer la solution à ce stade.

Ajout de contrôles au formulaire

L’application affiche une liste de catégories et une liste de produits. Lorsqu'une catégorie est sélectionnée dans la première liste, la deuxième liste change pour afficher les produits de cette catégorie. Ces listes peuvent être modifiées pour ajouter, supprimer ou modifier des produits et des catégories, et ces modifications peuvent être enregistrées dans la base de données SQLite en cliquant sur un bouton Enregistrer.

  1. Remplacez le nom du formulaire principal de Form1 à MainForm.

    Rename Form1 to MainForm

  2. Et remplacez le titre par « Produits et catégories ».

    Title MainForm as

  3. À l’aide de la boîte à outils, ajoutez deux contrôles DataGridView, organisés en regard des uns des autres.

    Add DataGridView

  4. Dans les propriétés du premier DataGridView, remplacez le nom par dataGridViewCategories.

  5. Dans les propriétés du deuxième DataGridView, remplacez le nom par dataGridViewProducts.

  6. En outre, à l’aide de la boîte à outils, ajoutez un contrôle Button.

  7. Nommez le bouton buttonSave et donnez-lui le texte « Enregistrer ». Le formulaire doit ressembler à ceci :

    Form layout

Liaison de données

L’étape suivante consiste à connecter les types Product et Category du modèle aux contrôles DataGridView. Les données chargées par EF Core sont ainsi liées aux contrôles, de sorte que les entités suivies par EF Core sont synchronisées avec celles affichées dans les contrôles.

  1. Cliquez sur le Glyphe d’action du concepteur sur le premier DataGridView. Il s’agit du petit bouton situé en haut à droite du contrôle.

    The Designer Action Glyph

  2. Cette opération ouvre la liste d’actions à partir de laquelle la liste déroulante de la source de données Choisie est accessible. Nous n’avons pas encore créé de source de données, donc accédez au bas et choisissez Ajouter une nouvelle source de données d’objet....

    Add new Object Data Source

  3. Choisissez Catégorie pour créer une source de données d’objet pour les catégories, puis cliquez sur OK.

    Choose Category data source type

    Conseil

    Si aucun type de source de données n’apparaît ici, assurez-vous que Product.cs, Category.cs et ProductsContext.cs n’ont jamais été ajoutés au projet et que la solution a été générée.

  4. Maintenant, la liste déroulante Choisir une source de données contient la source de données objet que nous venons de créer. Développez Autres sources de données, puis Sources de données du projet, puis choisissez Catégorie.

    Choose Category data source

    La deuxième DataGridView sera liée aux produits. Toutefois, au lieu de la liaison au type de niveau supérieur Product, elle sera liée à la navigation Products à partir de la liaison Category du premier DataGridView. Cela signifie que lorsqu'une catégorie est sélectionnée dans la première vue, les produits de cette catégorie seront automatiquement utilisés dans la seconde vue.

  5. À l’aide du Glyphe d’action du concepteur sur la deuxième DataGridView, choisissez Choisir une source de données, puis développez le categoryBindingSource et choisissez Products.

    Choose Products data source

Configuration de ce qui s’affiche

Par défaut, une colonne est créée dans la DataGridView de chaque propriété des types liés. De plus, les valeurs de chacune de ces propriétés peuvent être modifiées par l'utilisateur. Cependant, certaines valeurs, telles que les valeurs de la clé primaire, sont conceptuellement en lecture seule et ne doivent donc pas être modifiées. En outre, certaines propriétés, telles que la propriété de clé étrangère CategoryId et la navigation Category ne sont pas utiles à l’utilisateur, et doivent donc être masquées.

Conseil

Dans une application réelle, il est courant de masquer les propriétés des clés primaires. Ils sont laissés visibles ici pour permettre de voir facilement ce que fait EF Core en coulisses.

  1. Cliquez avec le bouton droit sur le premier DataGridView et choisissez Modifier les colonnes....

    Edit DataGridView columns

  2. Définissez la colonne CategoryId, qui représente la clé primaire, en lecture seule, puis cliquez sur OK.

    Make CategoryId column read-only

  3. Cliquez avec le bouton droit sur le deuxième DataGridView et choisissez Modifier les colonnes.... Définissez la colonne ProductId en lecture seule et supprimez les colonnes CategoryId et Category, puis cliquez sur OK.

    Make ProductId column read-only and remove CategoryId and Category columns

Connexion à EF Core

L'application a maintenant besoin d'une petite quantité de code pour connecter EF Core aux contrôles liés aux données.

  1. Ouvrez le code MainForm en cliquant avec le bouton droit sur le fichier et en choisissant Afficher le code.

    View Code

  2. Ajoutez un champ privé pour contenir la DbContext pour la session et ajoutez des remplacements pour les méthodes et les méthodes OnLoad et OnClosing. Le code doit se présenter comme ceci :

using Microsoft.EntityFrameworkCore;
using System.ComponentModel;

namespace GetStartedWinForms
{
    public partial class MainForm : Form
    {
        private ProductsContext? dbContext;

        public MainForm()
        {
            InitializeComponent();
        }

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            this.dbContext = new ProductsContext();

            // Uncomment the line below to start fresh with a new database.
            // this.dbContext.Database.EnsureDeleted();
            this.dbContext.Database.EnsureCreated();

            this.dbContext.Categories.Load();

            this.categoryBindingSource.DataSource = dbContext.Categories.Local.ToBindingList();
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            base.OnClosing(e);

            this.dbContext?.Dispose();
            this.dbContext = null;
        }
    }
}

La méthode OnLoad est appelée lorsque le formulaire est chargé. À ce stade

  • Une instance du ProductsContext est créée et sera utilisée pour charger et suivre les modifications apportées aux produits et aux catégories affichés par l’application.
  • EnsureCreated est appelé sur DbContext pour créer la base de données SQLite si elle n’existe pas déjà. Il s'agit d'un moyen rapide de créer une base de données lors du prototypage ou du test d'applications. Toutefois, si le modèle change, la base de données devra être supprimée pour pouvoir être recréée. (La ligne EnsureDeleted peut être non commentée pour supprimer et recréer facilement la base de données lors de l’exécution de l’application.) Vous pouvez plutôt utiliser EF Core Migrations pour modifier et mettre à jour le schéma de base de données sans perdre de données.
  • EnsureCreated remplira également la nouvelle base de données avec les données définies dans la méthode ProductsContext.OnModelCreating.
  • La méthode d’extension Load est utilisée pour charger toutes les catégories de la base de données dans le DbContext. Ces entités sont désormais suivies par le DbContext, qui détecte les modifications apportées lorsque les catégories sont modifiées par l’utilisateur.
  • La propriété categoryBindingSource.DataSource est initialisée dans les catégories qui sont suivies par le DbContext. Cela se fait en appelant Local.ToBindingList() sur la propriété CategoriesDbSet. Local fournit l’accès à une vue locale des catégories suivies, avec des événements connectés pour garantir que les données locales restent synchronisées avec les données affichées, et vice versa. ToBindingList() expose ces données en tant que IBindingList, qui est comprise par la liaison de données Windows Forms.

La méthode OnClosing est appelée lorsque le formulaire est fermé. À ce stade, le fichier DbContext est supprimé, ce qui garantit que toutes les ressources de base de données seront libérées et que le champ dbContext est défini sur Null afin qu’il ne puisse pas être utilisé à nouveau.

Remplissage de la vue Produits

Si l'application est lancée à ce stade, elle devrait ressembler à ceci :

Fist run of the application

Notez que les catégories ont été chargées à partir de la base de données, mais que la table des produits reste vide. En outre, le bouton Enregistrer ne fonctionne pas.

Pour remplir la table des produits, EF Core doit charger les produits de la base de données pour la catégorie sélectionnée. Procédure à suivre :

  1. Dans le concepteur du formulaire principal, sélectionnez les DataGridView pour les catégories.

  2. Dans les propriétés du DataGridView, choisissez les événements (bouton éclair), puis double-cliquez sur l’événement SelectionChanged.

    Add the SelectionChanged event

    Cela créera un stub dans le code du formulaire principal pour qu'un événement soit déclenché chaque fois que la sélection de la catégorie change.

  3. Renseignez le code de l’événement :

private void dataGridViewCategories_SelectionChanged(object sender, EventArgs e)
{
    if (this.dbContext != null)
    {
        var category = (Category)this.dataGridViewCategories.CurrentRow.DataBoundItem;

        if (category != null)
        {
            this.dbContext.Entry(category).Collection(e => e.Products).Load();
        }
    }
}

Dans ce code, s’il existe une session active DbContext (non nulle), nous obtenons l’instance Category liée à la ligne actuellement sélectionnée du DataViewGrid. (Cela peut être null si la ligne finale de la vue est sélectionnée, qui est utilisée pour créer de nouvelles catégories.) S’il existe une catégorie sélectionnée, il est demandé à DbContext de charger les produits associés à cette catégorie. Pour ce faire :

  • Obtention d’une EntityEntry pour l’instance Category (dbContext.Entry(category))
  • Informer EF Core que nous voulons opérer sur la navigation de collection Products de ce Category (.Collection(e => e.Products))
  • Enfin, en disant à EF Core que nous voulons charger cette collection de produits à partir de la base de données (.Load();)

Conseil

Lorsque Load est appelé, EF Core accède uniquement à la base de données pour charger les produits s’ils n’ont pas déjà été chargés.

Si l'application est à nouveau exécutée, elle devrait charger les produits appropriés chaque fois qu'une catégorie est sélectionnée :

Products are loaded

Enregistrement des modifications

Enfin, le bouton Enregistrer peut être connecté à EF Core afin que toutes les modifications apportées aux produits et catégories soient enregistrées dans la base de données.

  1. Dans le concepteur du formulaire principal, sélectionnez le bouton Enregistrer.

  2. Dans les propriétés du Button, choisissez les événements (bouton éclair), puis double-cliquez sur l’événement Click.

    Add the Click event for Save

  3. Renseignez le code de l’événement :

private void buttonSave_Click(object sender, EventArgs e)
{
    this.dbContext!.SaveChanges();

    this.dataGridViewCategories.Refresh();
    this.dataGridViewProducts.Refresh();
}

Ce code appelle SaveChanges sur le DbContext, qui enregistre les modifications apportées à la base de données SQLite. Si aucune modification n'a été apportée, il n'y a pas d'opération et aucun appel à la base de données n'est effectué. Après l’enregistrement, les contrôles DataGridView sont actualisés. En effet, EF Core lit dans la base de données les valeurs de clé primaire générées pour tous les nouveaux produits et catégories. Appeler Refresh met à jour l’affichage avec ces valeurs générées.

L’application finale

Voici le code complet du formulaire principal :

using Microsoft.EntityFrameworkCore;
using System.ComponentModel;

namespace GetStartedWinForms
{
    public partial class MainForm : Form
    {
        private ProductsContext? dbContext;

        public MainForm()
        {
            InitializeComponent();
        }

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            this.dbContext = new ProductsContext();

            // Uncomment the line below to start fresh with a new database.
            // this.dbContext.Database.EnsureDeleted();
            this.dbContext.Database.EnsureCreated();

            this.dbContext.Categories.Load();

            this.categoryBindingSource.DataSource = dbContext.Categories.Local.ToBindingList();
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            base.OnClosing(e);

            this.dbContext?.Dispose();
            this.dbContext = null;
        }

        private void dataGridViewCategories_SelectionChanged(object sender, EventArgs e)
        {
            if (this.dbContext != null)
            {
                var category = (Category)this.dataGridViewCategories.CurrentRow.DataBoundItem;

                if (category != null)
                {
                    this.dbContext.Entry(category).Collection(e => e.Products).Load();
                }
            }
        }

        private void buttonSave_Click(object sender, EventArgs e)
        {
            this.dbContext!.SaveChanges();

            this.dataGridViewCategories.Refresh();
            this.dataGridViewProducts.Refresh();
        }
    }
}

L'application peut maintenant être exécutée et les produits et catégories peuvent être ajoutés, supprimés et modifiés. Notez que si le bouton Enregistrer est cliqué avant de fermer l’application, toutes les modifications apportées seront stockées dans la base de données et rechargées lorsque l’application est redémarré. Si vous ne cliquez pas sur Enregistrer, les modifications sont perdues lorsque l’application est redémarrée.

Conseil

Une nouvelle catégorie ou un produit peut être ajouté à une DataViewControl en utilisant la ligne vide en bas du contrôle. Une ligne peut être supprimée en la sélectionnant et en appuyant sur la touche Sup.

Avant l’enregistrement

The running application before clicking Save

Après l’enregistrement

The running application after clicking Save

Notez que les valeurs de clé primaire pour la catégorie ajoutée et les produits sont remplies lorsque vous cliquez sur Enregistrer.

En savoir plus