Liaison de données avec WPF

Important

Ce document est valide pour WPF sur le .NET Framework uniquement

Ce document décrit la liaison de données pour WPF sur le .NET Framework. Pour les nouveaux projets .NET Core, nous vous recommandons d’utiliser EF Core au lieu d’Entity Framework 6. La documentation relative à la liaison de données dans EF Core est ici : Prise en main de WPF.

Cette procédure pas à pas montre comment lier des types POCO aux contrôles WPF dans un formulaire « maître-détail ». L’application utilise les API Entity Framework pour remplir les objets avec les 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 : Catégorie (principal\master) et Product (dépendant\détail). Ensuite, les outils Visual Studio sont utilisés pour lier les types définis dans le modèle aux contrôles WPF. L’infrastructure de liaison de données WPF permet la navigation entre les objets associés : la sélection de lignes dans l’affichage maître entraîne la mise à jour de la vue des détails avec les données enfants correspondantes.

Les captures d’écran et les listes de code de cette procédure pas à pas sont extraites de Visual Studio 2013, mais vous pouvez effectuer cette procédure pas à pas avec Visual Studio 2012 ou Visual Studio 2010.

Utiliser l’option « Object » pour créer des sources de données WPF

Avec la version précédente d’Entity Framework, nous vous recommandons d’utiliser l’option base de données lors de la création d’une source de données basée sur un modèle créé avec le concepteur EF. Cela était dû au fait que le concepteur générerait un contexte dérivé de ObjectContext et de classes d’entité dérivées d’EntityObject. L’utilisation de l’option Base de données vous aidera à écrire le meilleur code pour interagir avec cette surface d’API.

Les concepteurs EF pour Visual Studio 2012 et Visual Studio 2013 génèrent un contexte qui dérive de DbContext avec des classes d’entités POCO simples. Avec Visual Studio 2010, nous vous recommandons d’échanger vers un modèle de génération de code qui utilise DbContext comme décrit plus loin dans cette procédure pas à pas.

Lorsque vous utilisez l’aire de l’API DbContext, vous devez utiliser l’option Object lors de la création d’une source de données, comme indiqué dans cette procédure pas à pas.

Si nécessaire, vous pouvez revenir à la génération de code basée sur ObjectContext pour les modèles créés avec EF Designer.

Conditions préalables

Vous devrez avoir Visual Studio 2013 ou Visual Studio 2012 ou Visual Studio 2010 installé pour effectuer cette procédure pas à pas.

Si vous utilisez Visual Studio 2010, vous devez également installer NuGet. Pour plus d’informations, voir Installation de NuGet.  

Création de l’application

  • Ouvrez Visual Studio.
  • Fichier>Nouveau -> Projet...
  • Sélectionnez Windows dans le volet gauche et WPFApplication dans le volet droit
  • Entrez WPFwithEFSample comme nom
  • Sélectionnez OK.

Installer le package NuGet Entity Framework

  • Dans l’Explorateur de solutions, cliquez avec le bouton droit sur le projet WinFormswithEFSample
  • Sélectionnez Gérer les packages NuGet…
  • Dans la boîte de dialogue Gérer les packages NuGet, sélectionnez l’onglet Online, puis choisissez le package EntityFramework
  • Cliquez sur Install.

    Remarque

    Outre l’assembly EntityFramework, une référence à System.ComponentModel.DataAnnotations est également ajoutée. Si le projet a une référence à System.Data.Entity, il est supprimé lorsque le package EntityFramework est installé. L’assembly System.Data.Entity n’est plus utilisé pour les applications Entity Framework 6.

Définir un modèle

Dans cette procédure pas à pas, vous pouvez choisir d’implémenter un modèle à l’aide de Code First ou du concepteur EF. Effectuez l’une des deux sections suivantes.

Option 1 : Définir un modèle à l’aide du code First

Cette section montre comment créer un modèle et sa base de données associée à l’aide de Code First. Passez à la section suivante (Option 2 : Définir un modèle à l’aide de la base de données First) si vous préférez utiliser Database First pour inverser l’ingénierie de votre modèle à partir d’une base de données à l’aide du concepteur EF

Lorsque vous utilisez le développement Code First, vous commencez généralement par écrire des classes .NET Framework qui définissent votre modèle conceptuel (domaine).

  • Ajoutez une nouvelle classe au WPFwithEFSample :
    • Cliquez avec le bouton droit sur le nom du projet
    • Sélectionnez Ajouter, puis nouvel élément
    • Sélectionnez classe et entrez Product pour le nom de la classe
  • Remplacez la définition de classe Product par le code suivant :
    namespace WPFwithEFSample
    {
        public class Product
        {
            public int ProductId { get; set; }
            public string Name { get; set; }

            public int CategoryId { get; set; }
            public virtual Category Category { get; set; }
        }
    }
  • Ajoutez une classe Category avec la définition suivante :
    using System.Collections.ObjectModel;

    namespace WPFwithEFSample
    {
        public class Category
        {
            public Category()
            {
                this.Products = new ObservableCollection<Product>();
            }

            public int CategoryId { get; set; }
            public string Name { get; set; }

            public virtual ObservableCollection<Product> Products { get; private set; }
        }
    }

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 naviguer dans 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 de<TEntity>. Les propriétés DbSet< TEntity> indiquent le contexte que vous souhaitez inclure dans le modèle.

Une instance du type dérivé 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 persistance des données dans la base de données.

  • Ajoutez une nouvelle classe ProductContext au projet avec la définition suivante :
    using System.Data.Entity;

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

Compilez le projet.

Option 2 : Définir un modèle à l’aide du code First

Cette section montre comment utiliser Database First pour inverser l’ingénierie de votre modèle à partir d’une base de données à l’aide du concepteur EF. Si vous avez terminé la section précédente (Option 1 : Définir un modèle à l’aide du code First), ignorez cette section et accédez directement à la section Chargement différé.

Créer une base de données existante

En règle générale, lorsque vous ciblez une base de données existante, elle sera déjà créée, mais pour cette procédure pas à pas, nous devons créer une base de données à accéder.

Le serveur de base de données installé avec Visual Studio est différent selon la version de Visual Studio que vous avez installée :

  • Si vous utilisez Visual Studio 2010, vous allez créer une base de données SQL Express.
  • Si vous utilisez Visual Studio 2012, vous allez créer une base de données LocalDB.

Allons-y et générons la base de données.

  • Affichage> Explorateur de serveurs

  • Cliquez avec le bouton droit sur connexions de données -> Ajouter une connexion…

  • Si vous n’avez pas connecté à une base de données à partir de Explorateur de serveurs avant de devoir sélectionner Microsoft SQL Server comme source de données

    Change Data Source

  • Connectez-vous à LocalDB ou SQL Express, selon celui que vous avez installé, puis entrez Products comme nom de base de données

    Add Connection LocalDB

    Add Connection Express

  • Sélectionnez OK et vous serez invité à créer une base de données, sélectionnez Oui

    Create Database

  • La nouvelle base de données s’affiche désormais dans l’Explorateur de serveurs, cliquez dessus avec le bouton droit et sélectionnez nouvelle requête

  • Copiez le code SQL suivant dans la nouvelle requête, puis cliquez avec le bouton droit sur la requête, puis sélectionnez Exécuter

    CREATE TABLE [dbo].[Categories] (
        [CategoryId] [int] NOT NULL IDENTITY,
        [Name] [nvarchar](max),
        CONSTRAINT [PK_dbo.Categories] PRIMARY KEY ([CategoryId])
    )

    CREATE TABLE [dbo].[Products] (
        [ProductId] [int] NOT NULL IDENTITY,
        [Name] [nvarchar](max),
        [CategoryId] [int] NOT NULL,
        CONSTRAINT [PK_dbo.Products] PRIMARY KEY ([ProductId])
    )

    CREATE INDEX [IX_CategoryId] ON [dbo].[Products]([CategoryId])

    ALTER TABLE [dbo].[Products] ADD CONSTRAINT [FK_dbo.Products_dbo.Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [dbo].[Categories] ([CategoryId]) ON DELETE CASCADE

Modèle d’ingénieur inverse

Nous allons utiliser Entity Framework Designer, qui est inclus dans Visual Studio, pour créer notre modèle.

  • Projet-> Ajouter un nouvel élément…

  • Sélectionnez Données dans le menu de gauche, puis ADO.NET Entity Data Model

  • Entrez ProductModel comme nom, puis cliquez sur OK

  • Cette opération lance l’Assistant Entity Data Model

  • Sélectionnez Générer à partir de la base de données, puis cliquez sur suivant

    Choose Model Contents

  • Sélectionnez la connexion à la base de données que vous avez créée dans la première section, entrez ProductContext comme nom de la chaîne de connexion, puis cliquez sur Suivant

    Choose Your Connection

  • Cochez la case en regard des ‘tables’ pour importer toutes les tables, puis cliquez sur ‘Terminer’

    Choose Your Objects

Une fois le processus de l’ingénieur inverse terminé, le nouveau modèle est ajouté à votre projet et ouvert pour vous permettre d’afficher dans Entity Framework Designer. Un fichier App.config a également été ajouté à votre projet avec les détails de connexion de la base de données.

Étapes supplémentaires dans Visual Studio 2010

Si vous travaillez dans Visual Studio 2010, vous devez mettre à jour le concepteur EF pour utiliser la génération de code EF6.

  • Cliquez avec le bouton droit sur un emplacement vide de votre modèle dans le Concepteur EF, puis sélectionnez Ajouter un élément de génération de code…
  • Sélectionnez modèles en ligne dans le menu de gauche et recherchez DbContext
  • Sélectionnez le générateur EF 6.x DbContext pour C#, entrez ProductsModel comme nom, puis cliquez sur Ajouter

Mise à jour de la génération de code pour la liaison de données

EF génère du code à partir de votre modèle à l’aide de modèles T4. Les modèles fournis avec Visual Studio ou téléchargés à partir de la galerie Visual Studio sont destinés à une utilisation générale. Cela signifie que les entités générées à partir de ces modèles ont des propriétés de ICollection simples<T>. Toutefois, lorsque vous effectuez une liaison de données à l’aide de WPF, il est souhaitable d’utiliser ObservableCollection pour les propriétés de collection afin que WPF puisse suivre les modifications apportées aux collections. À cette fin, nous allons modifier les modèles pour utiliser ObservableCollection.

  • Ouvrez l’Explorateur de solutions et recherchez le fichierProductModel.edmx

  • Recherchez le fichier ProductModel.tt qui sera imbriqué sous le fichier ProductModel.edmx

    WPF Product Model Template

  • Double-cliquez sur le fichier ProductModel.tt pour l’ouvrir dans l’éditeur Visual Studio

  • Recherchez et remplacez les deux occurrences de «ICollection» par «ObservableCollection». Ceux-ci se trouvent environ aux lignes 296 et 484.

  • Recherchez et remplacez la première occurrence de «hashSet» par «ObservableCollection». Cette occurrence se trouve environ à la ligne 50. Ne pas remplacer la deuxième occurrence de HashSet trouvée plus loin dans le code.

  • Recherchez et remplacez la seule occurrence de «System.Collections.Generic» par «System.Collections.ObjectModel». Il se trouve à peu près à la ligne 424.

  • Enregistrez le fichier ProductModel.tt. Cela doit entraîner la régénération du code des entités. Si le code ne se régénère pas automatiquement, cliquez avec le bouton droit sur ProductModel.tt et choisissez « Exécuter l’outil personnalisé ».

Si vous ouvrez maintenant le fichier Category.cs (qui est imbriqué sous ProductModel.tt), vous devez voir que la collection Products a le type ObservableCollection<Product>.

Compilez le projet.

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, les propriétés de navigation offrent un moyen de naviguer dans une relation entre deux types d’entités.

EF vous permet de charger automatiquement des entités associées à partir de 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é), sachez que la première fois que vous accédez à chaque propriété de navigation, une requête distincte sera exécutée sur la base de données si le contenu n’est pas déjà dans le contexte.

Lorsque vous utilisez des types d’entités POCO, EF effectue un chargement différé en créant des instances de types proxy dérivés pendant l’exécution, puis en remplaçant les propriétés virtuelles dans vos classes pour ajouter le hook 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 virtuel (substituable en Visual Basic), et votre classe ne doit pas être scellée (NotOverridable en Visual Basic). Lors de l’utilisation des propriétés de navigation Database First, elles sont automatiquement rendues virtuelles pour activer le chargement différé. Dans la section Code First, nous avons choisi de rendre les propriétés de navigation virtuelles pour la même raison.

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.

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

  • Dans le menu principal, sélectionnez Projet -> Ajouter une nouvelle source de données (dans Visual Studio 2010, vous devez sélectionner Données -> Ajouter une nouvelle source de données...)

  • Dans le volet Choisir un type de source de données, sélectionnez Objet, puis cliquez sur Suivant

  • Dans la boîte de dialogue Sélectionner les objets de données, déployez le WPFwithEFSample deux fois et sélectionnez Catégorie
    Il n’est pas nécessaire de sélectionner la source de données Product, car nous y accéderons via la propriété Product sur la source de données Catégorie

    Select Data Objects

  • Cliquez sur Terminer.

  • La fenêtre Sources de données est ouverte en regard de la fenêtre MainWindow.xaml Si la fenêtre Sources de données n’apparaît pas, sélectionnez Affichage -> Autres fenêtres >Sources de données

  • Appuyez sur l’icône épingle, de sorte que la fenêtre Sources de données ne se masque pas automatiquement. Vous devrez peut-être appuyer sur le bouton Actualiser si la fenêtre était déjà visible.

    Data Sources

  • Sélectionnez la source de données Catégorie et faites-la glisser sur le formulaire.

Voici ce qui s’est produit lorsque nous avons déplacé cette source :

  • La ressource categoryViewSource et le contrôle categoryDataGrid ont été ajoutés au code XAML
  • La propriété DataContext sur l’élément Grid parent a été définie sur « {StaticResource categoryViewSource } ». La ressource categoryViewSource sert de source de liaison pour l’élément outer\parent Grid. Les éléments Grid internes héritent ensuite de la valeur DataContext de la grille parente (la propriété ItemsSource de categoryDataGrid’est définie sur « {Binding} »)
    <Window.Resources>
        <CollectionViewSource x:Key="categoryViewSource"
                                d:DesignSource="{d:DesignInstance {x:Type local:Category}, CreateList=True}"/>
    </Window.Resources>
    <Grid DataContext="{StaticResource categoryViewSource}">
        <DataGrid x:Name="categoryDataGrid" AutoGenerateColumns="False" EnableRowVirtualization="True"
                    ItemsSource="{Binding}" Margin="13,13,43,191"
                    RowDetailsVisibilityMode="VisibleWhenSelected">
            <DataGrid.Columns>
                <DataGridTextColumn x:Name="categoryIdColumn" Binding="{Binding CategoryId}"
                                    Header="Category Id" Width="SizeToHeader"/>
                <DataGridTextColumn x:Name="nameColumn" Binding="{Binding Name}"
                                    Header="Name" Width="SizeToHeader"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>

Ajout d’une grille de détails

Maintenant que nous avons une grille pour afficher les catégories, nous allons ajouter une grille de détails pour afficher les produits associés.

  • Sélectionnez la propriété Products sous la source de données Catégorie et faites-la glisser sur le formulaire.
    • La categoryProductsViewSource ressource et la grille productDataGrid sont ajoutées au code XAML
    • Le chemin de liaison de cette ressource est défini sur Products
    • L’infrastructure de liaison de données WPF garantit que seuls les produits liés à la catégorie sélectionnée apparaissent dans productDataGrid
  • À partir de la boîte à outils, faites glisser bouton sur le formulaire. Définissez la propriété Name sur buttonSave et la propriété Content sur Enregistrer.

Le formulaire doit ressembler à ceci :

Designer Form

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.

  • Dans la fenêtre XAML, cliquez sur l’élément <Window, ce qui sélectionne la fenêtre principale

  • 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ée

    Main Window Properties

  • Ajoutez également l’événement Click pour le bouton Enregistrer en double-cliquant sur le bouton Enregistrer dans le concepteur.

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

Le code déclare une instance longue de ProductContext. L’objet ProductContext est utilisé pour interroger et enregistrer des données dans la base de données. Dispose() sur l’instance ProductContext est ensuite appelée à partir de la méthode OnClosing substituée. Les commentaires de code fournissent des détails sur ce que fait le code.

    using System.Data.Entity;
    using System.Linq;
    using System.Windows;

    namespace WPFwithEFSample
    {
        public partial class MainWindow : Window
        {
            private ProductContext _context = new ProductContext();
            public MainWindow()
            {
                InitializeComponent();
            }

            private void Window_Loaded(object sender, RoutedEventArgs e)
            {
                System.Windows.Data.CollectionViewSource categoryViewSource =
                    ((System.Windows.Data.CollectionViewSource)(this.FindResource("categoryViewSource")));

                // Load is an extension method on IQueryable,
                // defined in the System.Data.Entity namespace.
                // This method enumerates the results of the query,
                // similar to ToList but without creating a list.
                // When used with Linq to Entities this method
                // creates entity objects and adds them to the context.
                _context.Categories.Load();

                // After the data is loaded call the DbSet<T>.Local property
                // to use the DbSet<T> as a binding source.
                categoryViewSource.Source = _context.Categories.Local;
            }

            private void buttonSave_Click(object sender, RoutedEventArgs e)
            {
                // When you delete an object from the related entities collection
                // (in this case Products), the Entity Framework doesn’t mark
                // these child entities as deleted.
                // Instead, it removes the relationship between the parent and the child
                // by setting the parent reference to null.
                // So we manually have to delete the products
                // that have a Category reference set to null.

                // The following code uses LINQ to Objects
                // against the Local collection of Products.
                // The ToList call is required because otherwise the collection will be modified
                // by the Remove call while it is being enumerated.
                // In most other situations you can use LINQ to Objects directly
                // against the Local property without using ToList first.
                foreach (var product in _context.Products.Local.ToList())
                {
                    if (product.Category == null)
                    {
                        _context.Products.Remove(product);
                    }
                }

                _context.SaveChanges();
                // Refresh the grids so the database generated values show up.
                this.categoryDataGrid.Items.Refresh();
                this.productsDataGrid.Items.Refresh();
            }

            protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
            {
                base.OnClosing(e);
                this._context.Dispose();
            }
        }

    }

Tester l’application WPF

  • Compilez et exécutez l'application. Si vous avez utilisé Code First, vous verrez qu’une base de données WPFwithEFSample.ProductContext est créée pour vous.

  • Entrez un nom de catégorie dans la grille supérieure et les noms de produits dans la grille inférieure N’entrez rien dans les colonnes d’ID, car la clé primaire est générée par la base de données

    Main Window with new categories and products

  • Appuyez sur le bouton Enregistrer pour enregistrer les données dans la base de données

Après l’appel à DbContext SaveChanges(), les ID sont renseignés avec les valeurs générées par la base de données. Comme nous avons appelé refresh() après SaveChanges() les contrôles DataGrid sont également mis à jour avec les nouvelles valeurs.

Main Window with IDs populated

Ressources supplémentaires

Pour en savoir plus sur la liaison de données aux regroupements à l’aide de WPF, consultez cette rubrique dans la documentation WPF.