Liaison de données avec WinForms

Cette procédure pas à pas montre comment lier des types POCO aux contrôles Windows Forms (WinForms) dans un formulaire « maître-détail ». L’application utilise 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 (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 WinForms. L’infrastructure de liaison de données WinForms 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 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.

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 Windows FormsApplication dans le volet droit
  • Entrez WinFormswithEFSample 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.

Implémenter IListSource pour les collections

Les propriétés de collection doivent implémenter l’interface IListSource pour activer la liaison de données bidirectionnelle avec le tri lors de l’utilisation de Windows Forms. Pour ce faire, nous allons étendre ObservableCollection afin d’ajouter des fonctionnalités IListSource.

  • Ajoutez une classe ObservableListSource au projet :
    • Cliquez avec le bouton droit sur le nom du projet
    • Sélectionnez Ajouter -> Nouvel élément
    • Sélectionnez Classe et entrez ObservableListSource pour le nom de la classe
  • Remplacez le code généré par défaut par le code suivant :

Cette classe active la liaison de données bidirectionnelle ainsi que le tri. La classe dérive de ObservableCollection<T> et ajoute une implémentation explicite de IListSource. La méthode GetList() de IListSource est implémentée pour retourner une implémentation IBindingList qui reste synchronisée avec ObservableCollection. L’implémentation IBindingList générée par ToBindingList prend en charge le tri. La méthode d’extension ToBindingList est définie dans l’assembly EntityFramework.

    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;
    using System.Data.Entity;

    namespace WinFormswithEFSample
    {
        public class ObservableListSource<T> : ObservableCollection<T>, IListSource
            where T : class
        {
            private IBindingList _bindingList;

            bool IListSource.ContainsListCollection { get { return false; } }

            IList IListSource.GetList()
            {
                return _bindingList ?? (_bindingList = this.ToBindingList());
            }
        }
    }

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 de 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).

  • Ajouter une nouvelle classe Product au projet
  • Remplacez le code généré par défaut par le code suivant :
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace WinFormswithEFSample
    {
        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 au projet.
  • Remplacez le code généré par défaut par le code suivant :
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace WinFormswithEFSample
    {
        public class Category
        {
            private readonly ObservableListSource<Product> _products =
                    new ObservableListSource<Product>();

            public int CategoryId { get; set; }
            public string Name { get; set; }
            public virtual ObservableListSource<Product> Products { get { return _products; } }
        }
    }

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 DbSetT<Entity>. Les propriétés DbSet indiquent au contexte quels types vous souhaitez inclure dans le modèle. Les types DbContext et DbSet sont définis dans l’assembly EntityFramework.

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.
  • Remplacez le code généré par défaut par le code suivant :
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;
    using System.Text;

    namespace WinFormswithEFSample
    {
        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 de Database 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 de Code First), ignorez cette section et accédez directement à la section Chargement différé.

Créer une base de données existante

Normalement, lorsque vous ciblez une base de données existante, elle est déjà créée, mais pour cette procédure pas à pas, nous devons créer une base de données pour y 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 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 compris 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 rétroconception terminé, le nouveau modèle est ajouté à votre projet et ouvert pour vous permettre de le voir 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<T> simples. Toutefois, lors de la liaison de données, il est souhaitable d’avoir des propriétés de collection qui implémentent IListSource. C’est pourquoi nous avons créé la classe ObservableListSource ci-dessus et nous allons maintenant modifier les modèles pour utiliser cette classe.

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

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

    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 « ObservableListSource ». Celles-ci se trouvent environ aux lignes 296 et 484.

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

  • 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 ObservableListSource<Product>.

Compilez le projet.

Chargement différé

La propriété Products sur la classe Category et la propriété Category sur 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 substituant les propriétés virtuelles dans vos classes pour ajouter le crochet 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 (Overridable dans Visual Basic), et votre classe ne doit pas être scellée (NotOverridable dans 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 WinForms.

  • 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 la fenêtre 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épliez WinFormswithEFSample deux fois et sélectionnez Category. 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 Category.

    Data Source

  • Cliquez sur Terminer. 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 Source 2

  • Dans l’Explorateur de solutions, double-cliquez sur le fichier Form1.cs pour ouvrir le formulaire principal dans le concepteur :

  • Sélectionnez la source de données Category et faites-la glisser sur le formulaire. Par défaut, un nouveau DataGridView (categoryDataGridView) et les contrôles de barre d’outils de Navigation sont ajoutés au concepteur. Ces contrôles sont liés aux composants BindingSource (categoryBindingSource) et Binding Navigator (categoryBindingNavigator) qui sont également créés.

  • Modifiez les colonnes de categoryDataGridView. Nous voulons définir la colonne CategoryId en lecture seule. La valeur de la propriété CategoryId est générée par la base de données après avoir enregistré les données.

    • Cliquez avec le bouton droit sur le contrôle DataGridView, puis sélectionnez Modifier les colonnes…
    • Sélectionnez la colonne CategoryId et définissez ReadOnly sur True
    • Appuyez sur OK
  • Sélectionnez la propriété Products sous la source de données Category et faites-la glisser sur le formulaire. productDataGridView et productBindingSource sont ajoutés au formulaire.

  • Modifiez les colonnes de productDataGridView. Nous voulons masquer les colonnes CategoryId et Category et définir ProductId en lecture seule. La valeur de la propriété ProductId est générée par la base de données après avoir enregistré les données.

    • Cliquez avec le bouton droit sur le contrôle DataGridView, puis sélectionnez Modifier les colonnes….
    • Sélectionnez la colonne ProductId et définissez ReadOnly sur True.
    • Sélectionnez la colonne CategoryId, puis appuyez sur le bouton Supprimer. Procédez de la même façon avec la colonne Category.
    • Appuyez sur OK.

    Jusqu’à présent, nous avons associé nos contrôles DataGridView aux composants BindingSource dans le concepteur. Dans la section suivante, nous allons ajouter du code au code-behind pour définir categoryBindingSource.DataSource sur la collection d’entités actuellement suivies par DbContext. Lorsque Products est glissé-déplacé sous Category, WinForms s’occupe de configurer la propriété productsBindingSource.DataSource sur categoryBindingSource et la propriété productsBindingSource.DataMember sur Products. En raison de cette liaison, seuls les produits appartenant à la Category actuellement sélectionnée sont affichés dans productDataGridView.

  • Activez le bouton Enregistrer dans la barre d’outils de Navigation en cliquant sur le bouton droit de la souris et en sélectionnant Activé.

    Form 1 Designer

  • Ajoutez le gestionnaire d’événements pour le bouton Enregistrer en double-cliquant dessus. Cela ajoute le gestionnaire d’événements et vous amène au code-behind du formulaire. Le code du gestionnaire d’événements categoryBindingNavigatorSaveItem_Click sera ajouté dans la section suivante.

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

Nous allons maintenant ajouter le code afin d’utiliser ProductContext pour effectuer l’accès aux données. Mettez à jour le code de pour la fenêtre du formulaire principal, 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. La méthode 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;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    using System.Data.Entity;

    namespace WinFormswithEFSample
    {
        public partial class Form1 : Form
        {
            ProductContext _context;
            public Form1()
            {
                InitializeComponent();
            }

            protected override void OnLoad(EventArgs e)
            {
                base.OnLoad(e);
                _context = new ProductContext();

                // Call the Load method to get the data for the given DbSet
                // from the database.
                // The data is materialized as entities. The entities are managed by
                // the DbContext instance.
                _context.Categories.Load();

                // Bind the categoryBindingSource.DataSource to
                // all the Unchanged, Modified and Added Category objects that
                // are currently tracked by the DbContext.
                // Note that we need to call ToBindingList() on the
                // ObservableCollection<TEntity> returned by
                // the DbSet.Local property to get the BindingList<T>
                // in order to facilitate two-way binding in WinForms.
                this.categoryBindingSource.DataSource =
                    _context.Categories.Local.ToBindingList();
            }

            private void categoryBindingNavigatorSaveItem_Click(object sender, EventArgs e)
            {
                this.Validate();

                // Currently, the Entity Framework doesn’t mark the entities
                // that are removed from a navigation property (in our example the Products)
                // as deleted in the context.
                // The following code uses LINQ to Objects against the Local collection
                // to find all products and marks any that do not have
                // a Category reference as deleted.
                // 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 do 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);
                    }
                }

                // Save the changes to the database.
                this._context.SaveChanges();

                // Refresh the controls to show the values         
                // that were generated by the database.
                this.categoryDataGridView.Refresh();
                this.productsDataGridView.Refresh();
            }

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

Tester l’application Windows Forms

  • Générez et exécutez l’application et vous pouvez tester les fonctionnalités.

    Form 1 Before Save

  • Une fois le magasin enregistré, les clés générées s’affichent à l’écran.

    Form 1 After Save

  • Si vous avez utilisé Code First, vous verrez également qu’une base de données WinFormswithEFSample.ProductContext est créée pour vous.

    Server Object Explorer