Bien démarrer avec WPF

Cette procédure pas à pas montre comment lier des types POCO à des contrôles WPF dans un formulaire « principal-détail ». L’application utilise les API Entity Framework pour remplir les objets avec des données de la base de données, effectuer le suivi des modifications et conserver les données dans la base de données.

Le modèle définit deux types qui participent à une relation un-à-plusieurs : Category (Catégorie) (principal\« main ») et Product (Produit) (dépendant\détail). Le framework de liaison de données WPF permet la navigation entre les objets associés : la sélection de lignes dans l’affichage principal entraîne la mise à jour de la vue des détails avec les données enfants correspondantes.

Les captures d’écran et les parties de code de cette procédure pas à pas sont extraites de Visual Studio 2019 16.6.5.

Conseil

Vous pouvez afficher cet exemple sur GitHub.

Conditions préalables

Pour effectuer cette procédure pas à pas, vous devez avoir Visual Studio 2019 16.3 ou ultérieur installé avec la charge de travail .NET Desktop 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. Recherchez « WPF », choisissez Application WPF (.NET Core), puis choisissez Suivant.
  4. Sur l’écran suivant, nommez le projet, par exemple GetStartedWPF, puis choisissez Créer.

Installer le package NuGet Entity Framework

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

    Manage NuGet Packages

  2. Tapez entityframeworkcore.sqlite dans la zone de recherche.

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

  4. Cochez le projet dans le volet droit, puis cliquez sur Installer

    Sqlite Package

  5. Répétez les étapes pour rechercher entityframeworkcore.proxies et installer Microsoft.EntityFrameworkCore.Proxies.

Remarque

Quand vous avez installé le package SQLite, il a extrait automatiquement le package de base Microsoft.EntityFrameworkCore associé. Le package Microsoft.EntityFrameworkCore.Proxies prend en charge les données « à chargement différé ». Cela signifie que quand vous avez des entités avec des entités enfants, seuls les parents sont extraits lors du chargement initial. Les proxys détectent quand une tentative d’accès aux entités enfants est effectuée et les chargent automatiquement à la demande.

Définir un modèle

Dans cette procédure pas à pas, vous allez implémenter un modèle en utilisant l’approche « Code First » (Code en premier). Cela signifie qu’EF Core va créer les tables et le schéma de la base de données d’après les classes C# que vous définissez.

Ajoutez une nouvelle classe. Nommez-la Product.cs et remplissez-la comme suit :

Product.cs

namespace GetStartedWPF
{
    public class Product
    {
        public int ProductId { get; set; }
        public string Name { get; set; }

        public int CategoryId { get; set; }
        public virtual Category Category { get; set; }
    }
}

Ensuite, ajoutez une classe nommée Category.cs et remplissez-la avec le code suivant :

Category.cs

using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace GetStartedWPF
{
    public class Category
    {
        public int CategoryId { get; set; }
        public string Name { get; set; }

        public virtual ICollection<Product>
            Products
        { get; private set; } =
            new ObservableCollection<Product>();
    }
}

La propriété Products sur la classe Category et la propriété Category de la classe Product sont des propriétés de navigation. Dans Entity Framework, les propriétés de navigation offrent un moyen de parcourir une relation entre deux types d’entités.

En plus de définir des entités, vous devez définir une classe qui dérive de DbContext et expose les propriétés DbSet<TEntity>. Les propriétés DbSet<TEntity> indiquent au contexte quels types vous voulez inclure dans le modèle.

Une instance du type dérivé de DbContext gère les objets d’entité au moment de l’exécution, ce qui comprend le remplissage des objets avec les données d’une base de données, le suivi des modifications et la conservation des données dans la base de données.

Ajoutez une nouvelle classe ProductContext.cs au projet avec la définition suivante :

ProductContext.cs

using Microsoft.EntityFrameworkCore;

namespace GetStartedWPF
{
    public class ProductContext : 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");
            optionsBuilder.UseLazyLoadingProxies();
        }
    }
}
  • DbSet informe EF Core des entités C# qui doivent être mappées à la base de données.
  • Il existe plusieurs façons de configurer le DbContext d’EF Core. Vous pouvez en savoir plus sur ceux-ci dans : Configuration d’un DbContext.
  • Cet exemple utilise le remplacement de OnConfiguring pour spécifier un fichier de données SQLite.
  • L’appel de UseLazyLoadingProxies indique à EF Core d’implémenter le chargement différé, de sorte que les entités enfants sont chargées automatiquement quand leur accès est effectué depuis le parent.

Appuyez sur Ctrl+Maj+B ou accédez à Générer> Générer la solution pour compiler le projet.

Conseil

Découvrez les différents moyens de maintenir la synchronisation de votre base de données et de vos modèles EF Core : Gestion des schémas de base de données.

Chargement différé

La propriété Products sur la classe Category et la propriété Category de la classe Product sont des propriétés de navigation. Dans Entity Framework Core, les propriétés de navigation offrent un moyen de naviguer dans une relation entre deux types d’entités.

EF Core vous donne la possibilité de charger automatiquement des entités associées depuis la base de données la première fois que vous accédez à la propriété de navigation. Avec ce type de chargement (appelé « chargement différé »), notez bien que la première fois que vous accédez à chaque propriété de navigation, une requête distincte va être exécutée sur la base de données si le contenu n’est pas déjà dans le contexte.

Quand vous utilisez des types d’entités POCO (« Plain Old C# Object »), EF Core effectue un chargement différé en créant des instances de types proxy dérivés lors de l’exécution, puis en remplaçant les propriétés virtuelles dans vos classes pour ajouter le raccordement de chargement. Pour obtenir le chargement différé d’objets associés, vous devez déclarer les getters de propriété de navigation en tant que public et virtual (Overridable dans Visual Basic), et votre classe ne doit pas être scellée (NotOverridable dans Visual Basic). Quand vous utilisez l’approche Database First, les propriétés de navigation sont automatiquement rendues virtuelles pour activer le chargement différé.

Lier un objet à des contrôles

Ajoutez les classes définies dans le modèle en tant que sources de données pour cette application WPF.

  1. Double-cliquez sur MainWindow.xaml dans l’Explorateur de solutions pour ouvrir le formulaire principal.

  2. Choisissez l’onglet XAML pour modifier le code XAML.

  3. Immédiatement après la balise ouvrante Window, ajoutez les sources suivantes à connecter aux entités EF Core.

    <Window x:Class="GetStartedWPF.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:GetStartedWPF"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800" Loaded="Window_Loaded">
        <Window.Resources>
            <CollectionViewSource x:Key="categoryViewSource"/>
            <CollectionViewSource x:Key="categoryProductsViewSource" 
                                  Source="{Binding Products, Source={StaticResource categoryViewSource}}"/>
        </Window.Resources>
    
  4. Ceci configure la source pour les catégories « parent » et la deuxième source pour les produits « detail ».

  5. Ensuite, ajoutez le balisage suivant à votre code XAML après la balise ouvrante Grid.

    <DataGrid x:Name="categoryDataGrid" AutoGenerateColumns="False" 
              EnableRowVirtualization="True" 
              ItemsSource="{Binding Source={StaticResource categoryViewSource}}" 
              Margin="13,13,43,229" RowDetailsVisibilityMode="VisibleWhenSelected">
        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding CategoryId}"
                                Header="Category Id" Width="SizeToHeader"
                                IsReadOnly="True"/>
            <DataGridTextColumn Binding="{Binding Name}" Header="Name" 
                                Width="*"/>
        </DataGrid.Columns>
    </DataGrid>
    
  6. Notez que la CategoryId est définie sur ReadOnly, car elle est affectée par la base de données et ne peut pas être modifiée.

Ajout d’une grille de détails

Maintenant que la grille existe pour afficher les catégories, la grille de détails peut être ajoutée pour afficher les produits. Ajoutez-la à l’intérieur de l’élément Grid, après l’élément DataGrid des catégories.

MainWindow.xaml

<DataGrid x:Name="productsDataGrid" AutoGenerateColumns="False" 
          EnableRowVirtualization="True" 
          ItemsSource="{Binding Source={StaticResource categoryProductsViewSource}}" 
          Margin="13,205,43,108" RowDetailsVisibilityMode="VisibleWhenSelected" 
          RenderTransformOrigin="0.488,0.251">
    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding CategoryId}" 
                            Header="Category Id" Width="SizeToHeader"
                            IsReadOnly="True"/>
        <DataGridTextColumn Binding="{Binding ProductId}" Header="Product Id" 
                            Width="SizeToHeader" IsReadOnly="True"/>
        <DataGridTextColumn Binding="{Binding Name}" Header="Name" Width="*"/>
    </DataGrid.Columns>
</DataGrid>

Enfin, ajoutez un bouton Save et connectez l’événement de clic à Button_Click.

<Button Content="Save" HorizontalAlignment="Center" Margin="0,240,0,0" 
        Click="Button_Click" Height="20" Width="123"/>

Votre vue de conception doit ressembler à ceci :

Screenshot of WPF Designer

Ajouter du code qui gère l’interaction des données

Il est temps d’ajouter des gestionnaires d’événements à la fenêtre principale.

  1. Dans la fenêtre XAML, cliquez sur l’élément <Window> pour sélectionner la fenêtre principale.

  2. Dans la fenêtre Propriétés, choisissez Événements en haut à droite, puis double-cliquez sur la zone de texte à droite de l’étiquette Chargé.

    Main Window Properties

Ceci vous amène au code-behind du formulaire ; nous allons maintenant modifier le code de façon à utiliser ProductContext pour effectuer l’accès aux données. Mettez à jour le code comme indiqué ci-dessous.

Le code déclare une instance d’exécution longue de ProductContext. L’objet ProductContext est utilisé pour interroger et enregistrer des données dans la base de données. La méthode Dispose() sur l’instance de ProductContext est ensuite appelée depuis la méthode OnClosing remplacée. Les commentaires de code expliquent ce que fait chaque étape.

MainWindow.xaml.cs

using Microsoft.EntityFrameworkCore;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;

namespace GetStartedWPF
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly ProductContext _context =
            new ProductContext();

        private CollectionViewSource categoryViewSource;

        public MainWindow()
        {
            InitializeComponent();
            categoryViewSource =
                (CollectionViewSource)FindResource(nameof(categoryViewSource));
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // this is for demo purposes only, to make it easier
            // to get up and running
            _context.Database.EnsureCreated();

            // load the entities into EF Core
            _context.Categories.Load();

            // bind to the source
            categoryViewSource.Source =
                _context.Categories.Local.ToObservableCollection();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            // all changes are automatically tracked, including
            // deletes!
            _context.SaveChanges();

            // this forces the grid to refresh to latest values
            categoryDataGrid.Items.Refresh();
            productsDataGrid.Items.Refresh();
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            // clean up database connections
            _context.Dispose();
            base.OnClosing(e);
        }
    }
}

Remarque

Le code utilise un appel à EnsureCreated() pour créer la base de données lors de la première exécution. Ceci est acceptable pour des démonstrations, mais dans les applications de production, vous devez examiner migrations pour gérer votre schéma. Le code s’exécute également de façon synchrone, car il utilise une base de données SQLite locale. Pour les scénarios de production qui impliquent généralement un serveur distant, envisagez d’utiliser les versions asynchrones des méthodes Load et SaveChanges.

Tester l’application WPF

Compilez et exécutez l’application en appuyant sur F5 ou en sélectionnant Déboguer> Démarrer le débogage. La base de données doit être créée automatiquement avec un fichier nommé products.db. Entrez un nom de catégorie et appuyez sur Entrée, puis ajoutez des produits dans la grille du bas. Cliquez sur enregistrer et observez l’actualisation de la grille avec les ID fournis par la base de données. Mettez une ligne en surbrillance et appuyez sur Delete (Supprimer) pour supprimer la ligne. L’entité est supprimée quand vous cliquez sur Save (Enregistrer).

Running application

Notification de modification de propriété

Cet exemple s’appuie sur quatre étapes pour synchroniser les entités avec l’interface utilisateur.

  1. L’appel initial _context.Categories.Load() charge les données des catégories.
  2. Les proxys de chargement différé chargent les données des produits dépendants.
  3. Le suivi des modifications intégré d’EF Core apporte les modifications nécessaires aux entités, y compris les insertions et les suppressions, quand _context.SaveChanges() est appelé.
  4. Les appels à DataGridView.Items.Refresh() forcent un rechargement avec les ID nouvellement générés.

Ceci fonctionne pour notre exemple de prise en main, mais vous pouvez avoir besoin de code supplémentaire pour d’autres scénarios. Les contrôles WPF rendent l’interface utilisateur en lisant les champs et les propriétés sur vos entités. Quand vous modifiez une valeur dans l’interface utilisateur, cette valeur est passée à votre entité. Quand vous modifiez la valeur d’une propriété directement sur votre entité, comme la charger depuis la base de données, WPF ne reflète pas immédiatement les modifications dans l’interface utilisateur. Le moteur de rendu doit être informé des modifications. Le projet fait cela via un appel manuel de Refresh(). Un moyen simple d’automatiser cette notification consiste à implémenter l’interface INotifyPropertyChanged. Les composants WPF détectent automatiquement l’interface et s’inscrivent pour les événements de modification. L’entité est chargée de déclencher ces événements.

Conseil

Pour en savoir plus sur la gestion des modifications, consultez Comment implémenter la notification des modifications des propriétés.

Étapes suivantes

En savoir plus sur la Configuration d’un DbContext.