Data binding con WinForms

Questa procedura dettagliata illustra come associare tipi POCO ai controlli Window Form (WinForms) in un modulo "master-detail". L'applicazione usa Entity Framework per popolare oggetti con dati dal database, tenere traccia delle modifiche e rendere persistenti i dati nel database.

Il modello definisce due tipi che partecipano alla relazione uno-a-molti: Category (principal\master) e Product (dependent\detail). Gli strumenti di Visual Studio vengono quindi usati per associare i tipi definiti nel modello ai controlli WinForms. Il framework di data binding WinForms consente la navigazione tra oggetti correlati: la selezione di righe nella visualizzazione master determina l'aggiornamento della visualizzazione dettagli con i dati figlio corrispondenti.

Le schermate e gli elenchi di codice in questa procedura dettagliata sono tratti da Visual Studio 2013, ma è possibile completare questa procedura dettagliata con Visual Studio 2012 o Visual Studio 2010.

Prerequisiti

Per completare questa procedura dettagliata, è necessario che Visual Studio 2013, Visual Studio 2012 o Visual Studio 2010 sia installato.

Se si usa Visual Studio 2010, è anche necessario installare NuGet. Per altre informazioni, vedere Installazione di NuGet.

Creare l'applicazione

  • Aprire Visual Studio.
  • File -> Nuovo -> Progetto....
  • Selezionare Windows nel riquadro sinistro e Windows Form Application nel riquadro di destra
  • Immettere WinFormswithEFSample come nome
  • seleziona OK.

Installare il pacchetto NuGet di Entity Framework

  • In Esplora soluzioni fare clic con il pulsante destro del mouse sul progetto WinFormswithEFSample
  • Selezionare Gestisci pacchetti NuGet...
  • Nella finestra di dialogo Gestisci pacchetti NuGet selezionare la scheda Online e scegliere il pacchetto EntityFramework
  • Fai clic su Install (Installa).

    Nota

    Oltre all'assembly EntityFramework viene aggiunto anche un riferimento a System.ComponentModel.DataAnnotations. Se il progetto ha un riferimento a System.Data.Entity, verrà rimosso quando viene installato il pacchetto EntityFramework. L'assembly System.Data.Entity non viene più usato per le applicazioni Entity Framework 6.

Implementazione di IListSource per le raccolte

Le proprietà della raccolta devono implementare l'interfaccia IListSource per abilitare il data binding bidirezionale con l'ordinamento quando si usa Windows Form. A tale scopo, estendere ObservableCollection per aggiungere la funzionalità IListSource.

  • Aggiungere una classe ObservableListSource al progetto:
    • Fare clic con il pulsante destro del mouse sul nome del progetto
    • Selezionare Aggiungi -> Nuovo elemento
    • Selezionare Classe e immettere ObservableListSource come nome della classe
  • Sostituire il codice generato per impostazione predefinita con il codice seguente:

Questa classe abilita il data binding bidirezionale e l'ordinamento. La classe deriva da ObservableCollection<T> e aggiunge un'implementazione esplicita di IListSource. Il metodo GetList() di IListSource viene implementato per restituire un'implementazione IBindingList che rimane sincronizzata con ObservableCollection. L'implementazione IBindingList generata da ToBindingList supporta l'ordinamento. Il metodo di estensione ToBindingList viene definito nell'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());
            }
        }
    }

Definire un modello

In questa procedura dettagliata è possibile scegliere di implementare un modello usando Code First o Ef Designer. Completare una delle due sezioni seguenti.

Opzione 1: Definire un modello usando Code First

Questa sezione illustra come creare un modello e il relativo database associato usando Code First. Passare alla sezione successiva (opzione 2: Definire un modello con Database First) se si preferisce usare Database First per invertire il modello da un database usando la finestra di progettazione di Entity Framework

Quando si usa lo sviluppo Code First, in genere si inizia scrivendo classi .NET Framework che definiscono il modello concettuale (dominio).

  • Aggiungere una nuova classe Product al progetto
  • Sostituire il codice generato per impostazione predefinita con il codice seguente:
    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; }
        }
    }
  • Aggiungere una classe Category al progetto.
  • Sostituire il codice generato per impostazione predefinita con il codice seguente:
    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; } }
        }
    }

Oltre a definire le entità, è necessario definire una classe che deriva da DbContext ed espone le proprietà DbSet<TEntity> . Le proprietà DbSet consentono al contesto di conoscere i tipi da includere nel modello. I tipi DbContext e DbSet sono definiti nell'assembly EntityFramework.

Un'istanza del tipo derivato DbContext gestisce gli oggetti entità durante l'esecuzione, che include il popolamento di oggetti con dati di un database, il rilevamento delle modifiche e la persistenza dei dati nel database.

  • Aggiungere una nuova classe ProductContext al progetto.
  • Sostituire il codice generato per impostazione predefinita con il codice seguente:
    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; }
        }
    }

Compilare il progetto.

Opzione 2: Definire un modello con Database First

Questa sezione illustra come usare Database First per invertire il modello da un database usando la finestra di progettazione di Entity Framework. Se è stata completata la sezione precedente (opzione 1: Definire un modello usando Code First), ignorare questa sezione e passare direttamente alla sezione Caricamento differita.

Creare un database esistente

In genere, quando si ha come destinazione un database esistente, verrà già creato, ma per questa procedura dettagliata è necessario creare un database per l'accesso.

Il server di database installato con Visual Studio è diverso a seconda della versione di Visual Studio installata:

  • Se si usa Visual Studio 2010, si creerà un database SQL Express.
  • Se si usa Visual Studio 2012, si creerà un database Local DB.

Procedere e generare il database.

  • Visualizzazione -> Esplora server

  • Fare clic con il pulsante destro del mouse su Data Connessione ions -> Aggiungi Connessione ion...

  • Se non si è connessi a un database da Esplora server prima di dover selezionare Microsoft SQL Server come origine dati

    Change Data Source

  • Connessione a Local DB o SQL Express, a seconda di quale sia stato installato e immettere Products come nome del database

    Add Connection LocalDB

    Add Connection Express

  • Selezionare OK e verrà chiesto se si vuole creare un nuovo database, selezionare

    Create Database

  • Il nuovo database verrà visualizzato in Esplora server, fare clic con il pulsante destro del mouse su di esso e selezionare Nuova query

  • Copiare il codice SQL seguente nella nuova query, quindi fare clic con il pulsante destro del mouse sulla query e selezionare Esegui

    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

Modello di reverse engineer

Si userà Entity Framework Designer, incluso come parte di Visual Studio, per creare il modello.

  • Progetto -> Aggiungi nuovo elemento...

  • Selezionare Dati dal menu a sinistra e quindi ADO.NET Entity Data Model

  • Immettere ProductModel come nome e fare clic su OK

  • Verrà avviata la Creazione guidata modello di dati di entità

  • Selezionare Genera dal database e fare clic su Avanti

    Choose Model Contents

  • Selezionare la connessione al database creato nella prima sezione, immettere ProductContext come nome del stringa di connessione e fare clic su Avanti

    Choose Your Connection

  • Fare clic sulla casella di controllo accanto a "Tabelle" per importare tutte le tabelle e fare clic su "Fine"

    Choose Your Objects

Dopo aver completato il processo di reverse engineer, il nuovo modello viene aggiunto al progetto e aperto per la visualizzazione in Entity Framework Designer. È stato aggiunto anche un file App.config al progetto con i dettagli di connessione per il database.

Passaggi aggiuntivi in Visual Studio 2010

Se si lavora in Visual Studio 2010, sarà necessario aggiornare la finestra di progettazione ef per usare la generazione di codice EF6.

  • Fare clic con il pulsante destro del mouse su un punto vuoto del modello in Ef Designer e selezionare Aggiungi elemento generazione codice...
  • Selezionare Modelli online dal menu a sinistra e cercare DbContext
  • Selezionare EF 6.x DbContext Generator per C#, immettere ProductsModel come nome e fare clic su Aggiungi

Aggiornamento della generazione di codice per il data binding

Ef genera codice dal modello usando i modelli T4. I modelli forniti con Visual Studio o scaricati dalla raccolta di Visual Studio sono destinati all'uso generico. Ciò significa che le entità generate da questi modelli hanno semplici proprietà ICollection<T> . Tuttavia, quando si esegue il data binding, è consigliabile avere proprietà di raccolta che implementano IListSource. Questo è il motivo per cui è stata creata la classe ObservableListSource precedente e ora verranno modificati i modelli per usare questa classe.

  • Aprire il Esplora soluzioni e trovare il file ProductModel.edmx

  • Trovare il file ProductModel.tt che verrà annidato nel file ProductModel.edmx

    Product Model Template

  • Fare doppio clic sul file ProductModel.tt per aprirlo nell'editor di Visual Studio

  • Trovare e sostituire le due occorrenze di "ICollection" con "ObservableListSource". Si trovano a circa le linee 296 e 484.

  • Trovare e sostituire la prima occorrenza di "HashSet" con "ObservableListSource". Questa occorrenza si trova a circa la riga 50. Non sostituire la seconda occorrenza di HashSet trovata più avanti nel codice.

  • Salvare il file ProductModel.tt. Questo dovrebbe causare la rigenerazione del codice per le entità. Se il codice non viene rigenerato automaticamente, fare clic con il pulsante destro del mouse su ProductModel.tt e scegliere "Esegui strumento personalizzato".

Se si apre ora il file Category.cs (annidato in ProductModel.tt), si noterà che l'insieme Products ha il tipo ObservableListSource<Product>.

Compilare il progetto.

Caricamento lazy

La proprietà Products nella classe Category e nella proprietà Category della classe Product sono proprietà di navigazione. In Entity Framework le proprietà di spostamento consentono di spostarsi tra due tipi di entità per spostarsi tra due tipi di entità.

Entity Framework consente di caricare automaticamente le entità correlate dal database alla prima volta che si accede alla proprietà di navigazione. Con questo tipo di caricamento (denominato caricamento differita), tenere presente che la prima volta che si accede a ogni proprietà di navigazione verrà eseguita una query separata sul database se il contenuto non è già presente nel contesto.

Quando si usano tipi di entità POCO, EF ottiene il caricamento differita creando istanze di tipi proxy derivati durante il runtime e quindi eseguendo l'override delle proprietà virtuali nelle classi per aggiungere l'hook di caricamento. Per ottenere il caricamento differita degli oggetti correlati, è necessario dichiarare i getter della proprietà di navigazione come public e virtual (Overridable in Visual Basic) e la classe non deve essere sealed (NotOverridable in Visual Basic). Quando si usano le proprietà di navigazione Database First vengono automaticamente rese virtuali per abilitare il caricamento differita. Nella sezione Code First abbiamo scelto di rendere virtuali le proprietà di spostamento per lo stesso motivo

Associare l'oggetto ai controlli

Aggiungere le classi definite nel modello come origini dati per questa applicazione WinForms.

  • Dal menu principale selezionare Progetto -> Aggiungi nuova origine dati ... (in Visual Studio 2010 è necessario selezionare Dati -> Aggiungi nuova origine dati...)

  • Nella finestra Scegliere un tipo di origine dati selezionare Oggetto e fare clic su Avanti

  • Nella finestra di dialogo Seleziona oggetti dati aprire winFormswithEFSample due volte e selezionare Categoria Non è necessario selezionare l'origine dati Product, perché verrà visualizzata tramite la proprietà Product nell'origine dati Category.

    Data Source

  • Fare clic su Fine. Se la finestra Origini dati non viene visualizzata, selezionare Visualizza -> Altre finestre-> Origini dati

  • Premere l'icona a forma di puntina, in modo che la finestra Origini dati non venga nascosta automaticamente. Potrebbe essere necessario premere il pulsante aggiorna se la finestra era già visibile.

    Data Source 2

  • In Esplora soluzioni fare doppio clic sul file Form1.cs per aprire il modulo principale nella finestra di progettazione.

  • Selezionare l'origine dati Categoria e trascinarla nel form. Per impostazione predefinita, alla finestra di progettazione vengono aggiunti nuovi controlli DataGridView (categoryDataGridView) e barra degli strumenti di spostamento. Questi controlli sono associati ai componenti BindingSource (categoryBindingSource) e Binding Navigator (categoryBindingNavigator) creati anche.

  • Modificare le colonne nella categoriaDataGridView. Si vuole impostare la colonna CategoryId su sola lettura. Il valore per la proprietà CategoryId viene generato dal database dopo il salvataggio dei dati.

    • Fare clic con il pulsante destro del mouse sul controllo DataGridView e scegliere Modifica colonne...
    • Selezionare la colonna CategoryId e impostare ReadOnly su True
    • Premere OK
  • Selezionare Products (Prodotti) nell'origine dati Category (Categoria) e trascinarlo nel form. Il prodottoDataGridView e productBindingSource vengono aggiunti al modulo.

  • Modificare le colonne nel productDataGridView. Si vogliono nascondere le colonne CategoryId e Category e impostare ProductId su sola lettura. Il valore della proprietà ProductId viene generato dal database dopo il salvataggio dei dati.

    • Fare clic con il pulsante destro del mouse sul controllo DataGridView e scegliere Modifica colonne.
    • Selezionare la colonna ProductId e impostare ReadOnly su True.
    • Selezionare la colonna CategoryId e premere il pulsante Rimuovi . Eseguire la stessa operazione con la colonna Categoria .
    • Scegliere OK.

    Finora, i controlli DataGridView sono stati associati ai componenti BindingSource nella finestra di progettazione. Nella sezione successiva si aggiungerà codice al code-behind per impostare categoryBindingSource.DataSource sulla raccolta di entità attualmente rilevate da DbContext. Quando abbiamo trascinato prodotti da sotto la categoria, WinForms si è occupato della configurazione della proprietà productsBindingSource.DataSource nella proprietà categoryBindingSource e productsBindingSource.DataMember su Products. A causa di questa associazione, solo i prodotti che appartengono alla categoria attualmente selezionata verranno visualizzati nel prodottoDataGridView.

  • Abilitare il pulsante Salva sulla barra degli strumenti di spostamento facendo clic sul pulsante destro del mouse e selezionando Abilitato.

    Form 1 Designer

  • Aggiungere il gestore eventi per il pulsante Salva facendo doppio clic sul pulsante. Verrà aggiunto il gestore eventi e verrà visualizzato il code-behind per il modulo. Il codice per il gestore eventi categoryBindingNavigatorSaveItem_Click verrà aggiunto nella sezione successiva.

Aggiungere il codice che gestisce l'interazione dei dati

A questo punto si aggiungerà il codice per usare ProductContext per eseguire l'accesso ai dati. Aggiornare il codice per la finestra del modulo principale, come illustrato di seguito.

Il codice dichiara un'istanza a esecuzione prolungata di ProductContext. L'oggetto ProductContext viene utilizzato per eseguire query e salvare i dati nel database. Il metodo Dispose() nell'istanza productContext viene quindi chiamato dal metodo OnClosing sottoposto a override. I commenti del codice forniscono informazioni dettagliate sulle operazioni del codice.

    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();
            }
        }
    }

Testare l'applicazione Windows Form

  • Compilare ed eseguire l'applicazione ed è possibile testare la funzionalità.

    Form 1 Before Save

  • Dopo aver salvato le chiavi generate dall'archivio vengono visualizzate sullo schermo.

    Form 1 After Save

  • Se hai usato Code First, vedrai anche che viene creato automaticamente un database WinFormswithEFSample.ProductContext .

    Server Object Explorer