Databindning med WinForms

Den här stegvisa genomgången visar hur du binder POCO-typer till Windows Forms-kontroller (WinForms) i ett "master-detail"-formulär. Programmet använder Entity Framework för att fylla i objekt med data från databasen, spåra ändringar och spara data i databasen.

Modellen definierar två typer som deltar i en-till-många-relation: Kategori (huvud\master) och Produkt (beroende\detalj). Sedan används Visual Studio-verktygen för att binda de typer som definierats i modellen till WinForms-kontrollerna. WinForms-databindningsramverket möjliggör navigering mellan relaterade objekt: om du väljer rader i huvudvyn uppdateras detaljvyn med motsvarande underordnade data.

Skärmbilderna och kodlistorna i den här genomgången är hämtade från Visual Studio 2013, men du kan slutföra den här genomgången med Visual Studio 2012 eller Visual Studio 2010.

Förutsättningar

Du måste ha Visual Studio 2013, Visual Studio 2012 eller Visual Studio 2010 installerat för att slutföra den här genomgången.

Om du använder Visual Studio 2010 måste du också installera NuGet. Mer information finns i Installera NuGet.

Skapa programmet

  • Öppna Visual Studio
  • Fil –> Ny –> Projekt....
  • Välj Windows i den vänstra rutan och Windows FormsApplication i den högra rutan
  • Ange WinFormswithEFSample som namn
  • Välj OK

Installera Entity Framework NuGet-paketet

  • Högerklicka på Projektet WinFormswithEFSample i Solution Explorer
  • Välj Hantera NuGet-paket...
  • I dialogrutan Hantera NuGet-paket väljer du fliken Online och väljer EntityFramework-paketet
  • Klicka på Installera

    Anmärkning

    Förutom EntityFramework-sammansättningen läggs även en referens till System.ComponentModel.DataAnnotations. Om projektet har en referens till System.Data.Entity tas det bort när EntityFramework-paketet har installerats. Sammansättningen System.Data.Entity används inte längre för Entity Framework 6-program.

Implementera IListSource för samlingar

Samlingsegenskaper måste implementera IListSource-gränssnittet för att aktivera dubbelriktad databindning med sortering när du använder Windows Forms. För att göra detta kommer vi att utöka ObservableCollection för att lägga till IListSource-funktioner.

  • Lägg till en ObservableListSource-klass i projektet:
    • Högerklicka på projektnamnet
    • Välj Lägg till –> nytt objekt
    • Välj Klass och ange ObservableListSource som klassnamn
  • Ersätt koden som genereras som standard med följande kod:

Den här klassen möjliggör dubbelriktad databindning samt sortering. Klassen härleds från ObservableCollection<T> och lägger till en explicit implementering av IListSource. Metoden GetList() för IListSource implementeras för att returnera en IBindingList-implementering som förblir synkroniserad med ObservableCollection. IBindingList-implementeringen som genereras av ToBindingList stöder sortering. Metoden ToBindingList-tillägg definieras i EntityFramework-sammansättningen.

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

Definiera en modell

I den här genomgången kan du välja att implementera en modell med hjälp av Code First eller EF Designer. Slutför något av följande två avsnitt.

Alternativ 1: Definiera en modell med kod först

Det här avsnittet visar hur du skapar en modell och dess associerade databas med hjälp av Code First. Gå vidare till nästa avsnitt (Alternativ 2: Definiera en modell med hjälp av Database First) om du hellre vill använda Database First för att omvänt konstruera din modell från en databas med hjälp av EF-designern

När du använder Code First-utveckling börjar du vanligtvis med att skriva .NET Framework-klasser som definierar din konceptuella modell (domän).

  • Lägga till en ny produktklass i projektet
  • Ersätt koden som genereras som standard med följande kod:
    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; }
        }
    }
  • Lägg till en kategoriklass i projektet.
  • Ersätt koden som genereras som standard med följande kod:
    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; } }
        }
    }

Förutom att definiera entiteter måste du definiera en klass som härleds från DbContext och exponerar DbSet<TEntity-egenskaper> . DbSet-egenskaperna låter kontexten veta vilka typer du vill inkludera i modellen. Typerna DbContext och DbSet definieras i EntityFramework-sammansättningen.

En instans av den härledda typen DbContext hanterar entitetsobjekten under körningen, vilket omfattar att fylla i objekt med data från en databas, ändra spårning och spara data i databasen.

  • Lägg till en ny ProductContext-klass i projektet.
  • Ersätt koden som genereras som standard med följande kod:
    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; }
        }
    }

Kompilera projektet.

Alternativ 2: Definiera en modell med hjälp av Databasen först

Det här avsnittet visar hur du använder Database First för att bakåtkompilera din modell från en databas med hjälp av EF-designern. Om du har slutfört föregående avsnitt (Alternativ 1: Definiera en modell med kod först) hoppar du över det här avsnittet och går direkt till avsnittet Lazy Loading .

Skapa en befintlig databas

Vanligtvis när du riktar in dig på en befintlig databas skapas den redan, men för den här genomgången måste vi skapa en databas för åtkomst.

Databasservern som är installerad med Visual Studio skiljer sig beroende på vilken version av Visual Studio du har installerat:

  • Om du använder Visual Studio 2010 skapar du en SQL Express-databas.
  • Om du använder Visual Studio 2012 skapar du en LocalDB-databas .

Nu ska vi gå vidare och generera databasen.

  • Visa –> ServerUtforskaren

  • Högerklicka på Dataanslutningar –> Lägg till anslutning...

  • Om du inte har anslutit till en databas från Server Explorer innan du måste välja Microsoft SQL Server som datakälla

    Ändra datakälla

  • Anslut till antingen LocalDB eller SQL Express, beroende på vilken du har installerat, och ange Produkter som databasnamn

    Lägg till LocalDB för anslutning

    Lägg till Connection Express

  • Välj OK så tillfrågas du om du vill skapa en ny databas och väljer Ja

    Skapa databas

  • Den nya databasen visas nu i Server Explorer, högerklickar på den och väljer Ny fråga

  • Kopiera följande SQL till den nya frågan och högerklicka sedan på frågan och välj Kör

    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

Reverse Engineering-modell

Vi ska använda Entity Framework Designer, som ingår som en del av Visual Studio, för att skapa vår modell.

  • Projekt –> Lägg till nytt objekt...

  • Välj Data på den vänstra menyn och sedan ADO.NET entitetsdatamodell

  • Ange ProductModel som namn och klicka på OK

  • Detta startar Entitetsdatamodellguiden

  • Välj Generera från databas och klicka på Nästa

    Välj modellinnehåll

  • Välj anslutningen till databasen som du skapade i det första avsnittet, ange ProductContext som namnet på anslutningssträngen och klicka på Nästa

    Välj din anslutning

  • Klicka på kryssrutan bredvid Tabeller för att importera alla tabeller och klicka på Slutför

    Välj dina objekt

När processen för omvänd ingenjörsarbete har slutförts läggs den nya modellen till i projektet och öppnas så att du kan se den i Entity Framework Designer. En App.config fil har också lagts till i projektet med anslutningsinformationen för databasen.

Ytterligare steg i Visual Studio 2010

Om du arbetar i Visual Studio 2010 måste du uppdatera EF-designern för att använda EF6-kodgenerering.

  • Högerklicka på en tom plats för din modell i EF Designer och välj Lägg till kodgenereringsobjekt...
  • Välj Onlinemallar på den vänstra menyn och sök efter DbContext
  • Välj EF 6.x DbContext Generator för C#, ange ProductsModel som namn och klicka på Lägg till

Uppdatera kodgenerering för databindning

EF genererar kod från din modell med hjälp av T4-mallar. Mallarna som levereras med Visual Studio eller laddas ned från Visual Studio-galleriet är avsedda för generell användning. Det innebär att entiteterna som genereras från dessa mallar har enkla ICollection<T-egenskaper> . När du gör databindning är det dock önskvärt att ha samlingsegenskaper som implementerar IListSource. Därför skapade vi klassen ObservableListSource ovan och vi kommer nu att ändra mallarna för att använda den här klassen.

  • Öppna Solution Explorer och leta reda på filen ProductModel.edmx

  • Leta upp filen ProductModel.tt som ligger under filen ProductModel.edmx

    Produktmodellmall

  • Dubbelklicka på filen ProductModel.tt för att öppna den i Visual Studio-redigeraren

  • Hitta och ersätt de två förekomsterna av "ICollection" med "ObservableListSource". Dessa ligger ungefär på linjerna 296 och 484.

  • Hitta och ersätt den första förekomsten av "HashSet" med "ObservableListSource". Den här förekomsten finns på ungefär rad 50. Ersätt inte den andra förekomsten av HashSet som hittades senare i koden.

  • Spara filen ProductModel.tt. Detta bör leda till att koden för entiteter återskapas. Om koden inte återskapas automatiskt högerklickar du på ProductModel.tt och väljer Kör anpassat verktyg.

Om du nu öppnar filen Category.cs (som är kapslad under ProductModel.tt) bör du se att produktsamlingen har typen ObservableListSource<Product>.

Kompilera projektet.

Fördröjd laddning

Egenskapen Produkter i egenskapen Kategoriklass och Kategori i klassen Produkt är navigeringsegenskaper. I Entity Framework ger navigeringsegenskaper ett sätt att navigera i en relation mellan två entitetstyper.

EF ger dig möjlighet att läsa in relaterade entiteter från databasen automatiskt första gången du kommer åt navigeringsegenskapen. Med den här typen av inläsning (kallas för lat inläsning) bör du vara medveten om att första gången du kommer åt varje navigeringsegenskap körs en separat fråga mot databasen om innehållet inte redan finns i kontexten.

När du använder POCO-entitetstyper uppnår EF lat inläsning genom att skapa instanser av härledda proxytyper under körningen och sedan åsidosätta virtuella egenskaper i dina klasser för att lägga till inläsningskroken. För att få fördröjd inläsning av relaterade objekt måste du deklarera navigeringsegenskapsgetter som offentliga och virtuella (kan åsidosättas i Visual Basic), och din klass får inte vara förseglad (NotOverridable i Visual Basic). När du använder Database First-navigeringsegenskaper görs automatiskt virtuella för att aktivera lat inläsning. I avsnittet Kod först valde vi att göra navigeringsegenskaperna virtuella av samma anledning

Binda objekt till kontroller

Lägg till de klasser som definieras i modellen som datakällor för det här WinForms-programmet.

  • På huvudmenyn väljer du Projekt –> Lägg till ny datakälla ... (i Visual Studio 2010 måste du välja Data –> Lägg till ny datakälla...)

  • I fönstret Välj en datakälltyp väljer du Objekt och klickar på Nästa

  • I dialogrutan Välj dataobjekt öppnar man WinFormswithEFSample två gånger och väljer Kategori, Det finns inget behov av att välja datakällan Produkt, eftersom vi kommer åt den via produktens egenskap på datakällan Kategori.

    Datakälla

  • Klicka på Slutför. Om fönstret Datakällor inte visas väljer du Visa –> Andra Windows-datakällor>

  • Tryck på fästikonen så att fönstret Datakällor inte döljs automatiskt. Du kan behöva trycka på uppdateringsknappen om fönstret redan var synligt.

    Datakälla 2

  • Dubbelklicka på filen Form1.cs i Solution Explorer för att öppna huvudformuläret i designern.

  • Välj datakällan Kategori och dra den i formuläret. Som standard läggs en ny DataGridView-kontroll (categoryDataGridView) och navigeringsverktygsfältets kontroller till i designern. Dessa kontroller är bundna till komponenterna BindingSource (categoryBindingSource) och Binding Navigator (categoryBindingNavigator) som också skapas.

  • Redigera kolumnerna i kategorinDataGridView. Vi vill göra kolumnen CategoryId skrivskyddad. Värdet för egenskapen CategoryId genereras av databasen när vi har sparat data.

    • Högerklicka på kontrollen DataGridView och välj Redigera kolumner...
    • Välj kolumnen CategoryId och ange ReadOnly till True
    • Tryck på OK
  • Välj Produkter från under Datakällan Kategori och dra den i formuläret. ProductDataGridView och productBindingSource läggs till i formuläret.

  • Redigera kolumnerna i productDataGridView. Vi vill dölja kolumnerna CategoryId och Category och ställa in ProductId som skrivskyddat. Värdet för egenskapen ProductId genereras av databasen när vi har sparat data.

    • Högerklicka på kontrollen DataGridView och välj Redigera kolumner....
    • Välj kolumnen ProductId och ange ReadOnly till True.
    • Välj kolumnen CategoryId och tryck på knappen Ta bort . Gör samma sak med kolumnen Kategori .
    • Tryck på OK.

    Hittills har vi associerat våra DataGridView-kontroller med BindingSource-komponenter i designern. I nästa avsnitt lägger vi till kod i koden bakom för att ange categoryBindingSource.DataSource till samlingen av entiteter som för närvarande spåras av DbContext. När vi dra-och-släppte Produkter från Kategori tog WinForms hand om att konfigurera egenskapen productsBindingSource.DataSource till categoryBindingSource och productsBindingSource.DataMember till Produkter. På grund av den här bindningen visas endast de produkter som tillhör den valda kategorin i productDataGridView.

  • Aktivera knappen Spara i verktygsfältet Navigering genom att klicka på höger musknapp och välja Aktiverad.

    Formulär 1 Designer

  • Lägg till händelsehanteraren för knappen Spara genom att dubbelklicka på knappen. Detta lägger till händelsehanteraren och tar dig till koden bakom för formuläret. Koden för categoryBindingNavigatorSaveItem_Click händelsehanteraren läggs till i nästa avsnitt.

Lägg till koden som hanterar datainteraktion

Nu ska vi lägga till koden för att använda ProductContext för att utföra dataåtkomst. Uppdatera koden för huvudformulärfönstret enligt nedan.

Koden deklarerar en långvarig instans av ProductContext. ProductContext-objektet används för att fråga efter och spara data i databasen. Metoden Dispose() på ProductContext-instansen anropas sedan från den åsidosatta OnClosing-metoden. Kodkommentarna innehåller information om vad koden gör.

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

Testa Windows Forms-programmet

  • Kompilera och kör programmet så kan du testa funktionerna.

    Formulär 1 innan du sparar

  • När de butiksgenererade nycklarna har sparats visas de på skärmen.

    Formulär 1 efter sparande

  • Om du använde Code First ser du också att en WinFormswithEFSample.ProductContext-databas har skapats åt dig.

    Utforskaren för serverobjekt