Datenbindung mit Windows Forms

In dieser schrittweisen exemplarischen Vorgehensweise wird gezeigt, wie POCO-Typen an Windows Forms (WinForms)-Steuerelemente in einem „main-detail“-Formular gebunden werden. Die Anwendung verwendet Entity Framework zum Auffüllen von Objekten mit Daten aus der Datenbank, zum Nachverfolgen von Änderungen und zum persistenten Speichern von Daten in der Datenbank.

Das Modell definiert zwei Typen, die an einer 1:n-Beziehung beteiligt sind: Category (principal\master) und Product (dependent\detail). Anschließend werden die Visual Studio-Tools verwendet, um die im Modell definierten Typen an die WinForms-Steuerelemente zu binden. Das WinForms-Datenbindungsframework ermöglicht die Navigation zwischen verbundenen Objekten: Durch Auswählen von Zeilen in der Masteransicht wird die Detailansicht mit den entsprechenden untergeordneten Daten aktualisiert.

Die Screenshots und Codeauflistungen in dieser exemplarischen Vorgehensweise stammen aus Visual Studio 2013, aber Sie können diese exemplarische Vorgehensweise auch mit Visual Studio 2012 oder Visual Studio 2010 ausführen.

Voraussetzungen

Sie müssen Visual Studio 2013, Visual Studio 2012 oder Visual Studio 2010 installiert haben, um diese exemplarische Vorgehensweise abzuschließen.

Wenn Sie Visual Studio 2010 verwenden, müssen Sie auch NuGet installieren. Weitere Informationen finden Sie unter Installation von NuGet.

Erstellen der Anwendung

  • Öffnen Sie Visual Studio.
  • Datei -> Neu -> Projekt….
  • Wählen Sie im linken Bereich Windows aus, und Windows FormsApplication- im rechten Bereich.
  • Geben Sie WinFormswithEFSample- als Namen ein.
  • Klicken Sie auf OK.

Installieren Sie das Entity Framework-NuGet-Paket.

  • Klicken Sie im Projektmappen-Explorer mit der rechten Maustaste auf das Projekt WinFormswithEFSample.
  • Wählen Sie NuGet-Pakete verwalten... aus.
  • Wählen Sie im Dialogfeld „NuGet-Pakete verwalten“ die Registerkarte Online- und dann das EntityFramework-Paket aus.
  • Klicken Sie auf Install (Installieren).

    Hinweis

    Zusätzlich zur EntityFramework-Assembly wird auch ein Verweis auf System.ComponentModel.DataAnnotations hinzugefügt. Wenn das Projekt über einen Verweis auf System.Data.Entity verfügt, wird es beim Installieren des EntityFramework-Pakets entfernt. Die System.Data.Entity-Assembly wird für Entity Framework 6-Anwendungen nicht mehr verwendet.

Implementieren von IListSource für Sammlungen

Sammlungseigenschaften müssen die IListSource-Schnittstelle implementieren, um die bidirektionale Datenbindung beim Sortieren bei Verwendung von Windows Forms zu ermöglichen. Dazu erweitern wir ObservableCollection, um IListSource-Funktionen hinzuzufügen.

  • Fügen Sie dem Projekt eine ObservableListSource-Klasse hinzu:
    • Klicken Sie mit der rechten Maustaste auf den Projektnamen.
    • Wählen Sie Hinzufügen -> Neues Element aus.
    • Wählen Sie Class aus, und geben Sie ObservableListSource für den Klassennamen ein.
  • Ersetzen Sie den standardmäßig generierten Code durch den folgenden Code:

Diese Klasse ermöglicht die bidirektionale Datenbindung sowie das Sortieren. Die Klasse wird von ObservableCollection<T> abgeleitet und fügt eine explizite Implementierung von IListSource hinzu. Die GetList()-Methode von IListSource wird implementiert, um eine IBindingList-Implementierung zurückzugeben, die mit observableCollection synchronisiert bleibt. Die von ToBindingList generierte IBindingList-Implementierung unterstützt die Sortierung. Die ToBindingList-Erweiterungsmethode wird in der EntityFramework-Assembly definiert.

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

Definieren eines Modells

In dieser exemplarischen Vorgehensweise können Sie ein Modell mithilfe von Code First oder EF Designer implementieren. Schließen Sie einen der beiden folgenden Abschnitte ab.

Option 1: Definieren eines Modells mithilfe von Code First

In diesem Abschnitt wird gezeigt, wie Sie ein Modell und die zugehörige Datenbank mithilfe von Code First erstellen. Fahren Sie mit dem nächsten Abschnitt (Option 2: Definieren eines Modells mithilfe von Database First) fort, wenn Sie lieber Database First verwenden möchten, um Ihr Modell mit Hilfe des EF-Designers aus einer Datenbank zurückzuentwickeln.

Bei der Code-First-Entwicklung beginnen Sie in der Regel mit dem Schreiben von .NET Framework-Klassen, die Ihr konzeptionelles Modell (Domänenmodell) definieren.

  • Hinzufügen einer neuen Product-Klasse zum Projekt
  • Ersetzen Sie den standardmäßig generierten Code durch den folgenden Code:
    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; }
        }
    }
  • Fügen Sie dem Projekt eine Category-Klasse hinzu.
  • Ersetzen Sie den standardmäßig generierten Code durch den folgenden Code:
    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; } }
        }
    }

Zusätzlich zur Definition von Entitäten müssen Sie eine Klasse definieren, die von DbContext abgeleitet ist und DbSet<TEntity>-Eigenschaften bereitstellt. Die DbSet-Eigenschaften informieren den Kontext darüber, welche Typen Sie in das Modell einbeziehen möchten. Die Typen DbContext und DbSet werden in der EntityFramework-Assembly definiert.

Eine Instanz des von DbContext abgeleiteten Typs verwaltet die Entitätsobjekte während der Laufzeit, was das Auffüllen der Objekte mit Daten aus einer Datenbank, die Änderungsnachverfolgung und das persistente Speichern von Daten in der Datenbank umfasst.

  • Fügen Sie dem Projekt eine neue ProductContext-Klasse hinzu.
  • Ersetzen Sie den standardmäßig generierten Code durch den folgenden Code:
    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; }
        }
    }

Kompilieren Sie das Projekt.

Option 2: Definieren eines Modells mithilfe von Database First

In diesem Abschnitt wird gezeigt, wie Sie mit Database First Ihr Modell mithilfe des EF-Designers aus einer Datenbank zurückentwickeln können. Wenn Sie den vorherigen Abschnitt abgeschlossen haben (Option 1: Definieren eines Modells mithilfe von Code First), überspringen Sie diesen Abschnitt, und wechseln Sie direkt zum Abschnitt Lazy Loading.

Erstellen einer vorhandenen Datenbank

Wenn Sie auf eine bestehende Datenbank zugreifen, ist diese in der Regel bereits erstellt, aber für dieses Beispiel müssen wir eine Datenbank erstellen, auf die wir zugreifen können.

Der Datenbankserver, der mit Visual Studio installiert ist, unterscheidet sich je nach der installierten Version von Visual Studio:

  • Wenn Sie Visual Studio 2010 verwenden, erstellen Sie eine SQL Express-Datenbank.
  • Wenn Sie Visual Studio 2012 verwenden, erstellen Sie eine LocalDB-Datenbank.

Lassen Sie uns nun die Datenbank generieren.

  • Ansicht –>Server-Explorer

  • Klicken Sie mit der rechten Maustaste auf Datenverbindungen –>Verbindung hinzufügen….

  • Wenn Sie im Server-Explorer noch keine Verbindung mit einer Datenbank hergestellt haben, müssen Sie Microsoft SQL Server als Datenquelle auswählen.

    Change Data Source

  • Herstellen einer Verbindung mit LocalDB oder SQL Express, je nachdem, welches Sie installiert haben, und geben Sie Products als Datenbanknamen ein.

    Add Connection LocalDB

    Add Connection Express

  • Wählen Sie OK aus, und Sie werden gefragt, ob Sie eine neue Datenbank erstellen möchten, wählen Sie Ja aus.

    Create Database

  • Die neue Datenbank wird nun im Server-Explorer angezeigt. Klicken Sie mit der rechten Maustaste darauf, und wählen Sie Neue Abfrage aus.

  • Kopieren Sie die folgende SQL-Datei in die neue Abfrage, klicken Sie dann mit der rechten Maustaste auf die Abfrage, und wählen Sie Ausführen aus.

    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

Zurückentwickeln (Reverse Engineering) des Modells

Wir verwenden den Entity Framework-Designer, der als Teil von Visual Studio enthalten ist, um unser Modell zu erstellen.

  • Projekt -> Neues Element hinzufügen…

  • Wählen Sie Daten im linken Menü und dannADO.NET Entity Data Model aus.

  • Geben Sie als Name ProductModel ein, und klicken Sie auf OK.

  • Dadurch wird der Entity Data Model-Assistent gestartet.

  • Wählen Sie Aus Datenbank generieren aus, und klicken Sie auf Weiter.

    Choose Model Contents

  • Wählen Sie die Verbindung mit der Datenbank aus, die Sie im ersten Abschnitt erstellt haben, geben Sie ProductContext als Namen der Verbindungszeichenfolge ein, und klicken Sie auf Weiter.

    Choose Your Connection

  • Klicken Sie auf das Kontrollkästchen neben „Tabellen“, um alle Tabellen zu importieren, und klicken Sie auf „Fertig stellen“.

    Choose Your Objects

Sobald der Reverse Engineering-Prozess abgeschlossen ist, wird das neue Modell zu Ihrem Projekt hinzugefügt und geöffnet, damit Sie es im Entity Framework Designer anzeigen können. Außerdem wurde Ihrem Projekt eine App.config-Datei mit den Verbindungsdetails für die Datenbank hinzugefügt.

Zusätzliche Schritte in Visual Studio2010

Wenn Sie in Visual Studio 2010 arbeiten, müssen Sie den EF-Designer aktualisieren, um EF6-Codegenerierung zu verwenden.

  • Klicken Sie mit der rechten Maustaste auf einen leeren Bereich Ihres Modells im EF Designer, und wählen Sie Codegenerierungselement hinzufügen... aus.
  • Wählen Sie im linken Menü Onlinevorlagen aus, und suchen Sie nach DbContext.
  • Wählen Sie den EF 6.x DbContext-Generator für C# aus, geben Sie ProductsModel als Namen ein, und klicken Sie auf „Hinzufügen“.

Aktualisieren der Codegenerierung für die Datenbindung

EF generiert Code aus Ihrem Modell mithilfe von T4-Vorlagen. Die Vorlagen, die mit Visual Studio ausgeliefert oder aus dem Visual Studio-Katalog heruntergeladen wurden, sind für die allgemeine Verwendung vorgesehen. Dies bedeutet, dass die aus diesen Vorlagen generierten Entitäten über einfache ICollection<T>-Eigenschaften verfügen. Bei der Datenbindung ist es jedoch wünschenswert, Sammlungseigenschaften zu haben, die IListSource implementieren. Deshalb haben wir oben die ObservableListSource-Klasse erstellt, und wir werden jetzt die Vorlagen ändern, um diese Klasse zu verwenden.

  • Öffnen Sie den Projektmappen-Explorer, und suchen Sie die Datei ProductModel.edmx.

  • Suchen Sie die Datei ProductModel.tt, die unter der Datei „ProductModel.edmx“ geschachtelt ist.

    Product Model Template

  • Doppelklicken Sie auf die Datei „ProductModel.tt“, um sie im Visual Studio-Editor zu öffnen.

  • Suchen und ersetzen Sie die beiden Vorkommen von „ICollection“ durch „ObservableListSource“. Diese befinden sich in etwa den Zeilen 296 und 484.

  • Suchen und ersetzen Sie das erste Vorkommen von „HashSet-“ durch „ObservableListSource“. Dieses Vorkommen befindet sich etwa in Zeile 50. Ersetzen Sie nicht das zweite Vorkommen von „HashSet“, das später im Code zu finden ist.

  • Speichern Sie die Datei „ProductModel.tt“. Dies sollte dazu führen, dass der Code für Entitäten neu generiert wird. Wenn der Code nicht automatisch neu generiert wird, klicken Sie mit der rechten Maustaste auf ProductModel.tt, und wählen Sie „Benutzerdefiniertes Tool“ ausführen aus.

Wenn Sie nun die Datei „Category.cs“ (die unter ProductModel.tt geschachtelt ist) öffnen, sollten Sie sehen, dass die Products-Auflistung den Typ ObservableListSource<Product> hat.

Kompilieren Sie das Projekt.

Verzögertes Laden

Die Products-Eigenschaft für die Category-Klasse und die Category-Eigenschaft für die Product-Klasse sind Navigationseigenschaften. Im Entity Framework bieten Navigationseigenschaften eine Möglichkeit, in einer Beziehung zwischen zwei Entitätstypen zu navigieren.

EF bietet Ihnen die Möglichkeit, verwandte Entitäten aus der Datenbank automatisch zu laden, wenn Sie zum ersten Mal auf die Navigationseigenschaft zugreifen. Beachten Sie bei dieser Art von Laden (das als „Lazy Loading“ bezeichnet wird), dass beim ersten Zugriff auf jede Navigationseigenschaft eine separate Abfrage für die Datenbank ausgeführt wird, wenn sich die Inhalte nicht bereits im Kontext befinden.

Bei der Verwendung von POCO-Entitätstypen erreicht EF Lazy Loading, indem während der Laufzeit Instanzen abgeleiteter Proxytypen erstellt und dann virtuelle Eigenschaften in Ihren Klassen überschrieben werden, um den Ladehook hinzuzufügen. Um Lazy Loading von verwandten Objekten zu erzielen, müssen Sie Navigationseigenschaftsgetter als public und virtual deklarieren (Overridable in Visual Basic), und Ihre Klasse darf nicht sealed (NotOverridable in Visual Basic) sein. Wenn Database First verwendet wird, werden Navigationseigenschaften automatisch als virtuell festgelegt, um Lazy Loading zu ermöglichen. Im Abschnitt zu Code First haben wir uns entschieden, die Navigationseigenschaften aus demselben Grund virtuell zu machen.

Binden von Objekten an Steuerelemente

Fügen Sie die Klassen, die im Modell definiert sind, als Datenquellen für diese WinForms-Anwendung hinzu.

  • Wählen Sie im Hauptmenü Projekt –> Neue Datenquelle hinzufügen… aus (in Visual Studio 2010 müssen Sie Daten –> Neue Datenquelle hinzufügen… auswählen).

  • Wählen Sie im Fenster „Datenquellentyp auswählen“ die Option Objekt aus, und klicken Sie dann auf Weiter.

  • Entfalten Sie im Dialogfeld „Datenobjekte auswählen“ WinFormswithEFSample zweimal, und wählen Sie Category aus. Es ist nicht erforderlich, die Datenquelle „Product“ auszuwählen, da wir die Eigenschaft „Product“ in der Datenquelle „Category“ durchlaufen.

    Data Source

  • Klicken Sie auf Fertig stellen. Wenn das Fenster „Datenquellen“ nicht angezeigt wird, wählen Sie Ansicht –> Andere Windows-> Datenquellen aus.

  • Klicken Sie auf das Anheftensymbol, sodass das Fenster „Datenquellen“ nicht automatisch ausgeblendet wird. Möglicherweise müssen Sie auf die Aktualisierungsschaltfläche klicken, wenn das Fenster bereits sichtbar war.

    Data Source 2

  • Doppelklicken Sie im Projektmappen-Explorer auf die Datei Form1.cs, um das Hauptformular im Designer zu öffnen.

  • Wählen Sie die Datenquelle Category aus, und ziehen Sie sie auf das Formular. Standardmäßig werden dem Designer eine neue DataGridView (categoryDataGridView) und Steuerelemente in der Navigationssymbolleiste hinzugefügt. Diese Steuerelemente sind an die Komponenten BindingSource (categoryBindingSource) und Binding Navigator (categoryBindingNavigator) gebunden, die ebenfalls erstellt werden.

  • Bearbeiten Sie die Spalten in der categoryDataGridView. Wir legen die Spalte CategoryId als schreibgeschützt fest. Der Wert für die CategoryId-Eigenschaft wird von der Datenbank generiert, nachdem die Daten gespeichert wurden.

    • Klicken Sie mit der rechten Maustaste auf das Steuerelement DataGridView, und wählen Sie „Spalten bearbeiten…“ aus.
    • Wählen Sie die Spalte „CategoryId“ aus, und legen Sie „ReadOnly“ auf „True“ fest.
    • Klicken Sie auf „OK“.
  • Wählen Sie „Products“ unter der Datenquelle „Category“ aus, und ziehen Sie sie auf das Formular. productDataGridView und productBindingSource werden dem Formular hinzugefügt.

  • Bearbeiten Sie die Spalten in der productDataGridView. Wir blenden die Spalten „CategoryId“ und „Category“ aus und legen „ProductId“ als schreibgeschützt fest. Der Wert für die ProductId-Eigenschaft wird von der Datenbank generiert, nachdem die Daten gespeichert wurden.

    • Klicken Sie mit der rechten Maustaste auf das Steuerelement DataGridView, und wählen Sie Spalten bearbeiten… aus.
    • Wählen Sie die Spalte ProductId aus, und legen Sie ReadOnly auf True fest.
    • Wählen Sie die Spalte CategoryId aus, und drücken Sie die Schaltfläche Entfernen. Führen Sie dieselbe Vorgehensweise mit der Spalte Category aus.
    • Klicken Sie auf OK.

    Bisher haben wir unsere DataGridView-Steuerelemente mit BindingSource-Komponenten im Designer verknüpft. Im nächsten Abschnitt fügen wir dem Code dahinter Code hinzu, um categoryBindingSource.DataSource auf die Sammlung von Entitäten zu setzen, die derzeit von DbContext verfolgt werden. Als wir Products aus der Kategorie gezogen und abgelegt haben, hat WinForms die Eigenschaft productsBindingSource.DataSource auf categoryBindingSource und die Eigenschaft productsBindingSource.DataMember auf Products gesetzt. Aufgrund dieser Bindung werden nur die Produkte, die zur aktuell ausgewählten Kategorie gehören, in „productDataGridView“ angezeigt.

  • Aktivieren Sie die Schaltfläche Speichern auf der Navigationssymbolleiste, indem Sie auf die rechte Maustaste klicken und Aktiviert auswählen.

    Form 1 Designer

  • Fügen Sie den Ereignishandler für die Schaltfläche „Speichern“ hinzu, indem Sie auf die Schaltfläche doppelklicken. Dies fügt den Ereignishandler hinzu und bringt Sie zum Code hinter dem Formular. Der Code für den Ereignishandler categoryBindingNavigatorSaveItem_Click wird im nächsten Abschnitt hinzugefügt.

Hinzufügen des Codes, der die Dateninteraktion verarbeitet

Wir fügen nun den Code hinzu, um den ProductContext zum Ausführen des Datenzugriffs zu verwenden. Aktualisieren Sie den Code für das Hauptformularfenster wie unten dargestellt.

Der Code deklariert eine Instanz von ProductContext mit langer Ausführungszeit. Das ProductContext-Objekt wird verwendet, um Daten abzufragen und in der Datenbank zu speichern. Die Dispose()-Methode für die ProductContext-Instanz wird dann von der überschriebenen OnClosing-Methode aufgerufen. Die Codekommentare enthalten Details zur Funktionsweise des Codes.

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

Testen der Windows Forms-Anwendung

  • Kompilieren Sie die Anwendung, und führen Sie sie aus, um die Funktionalität zu testen.

    Form 1 Before Save

  • Nach dem Speichern werden die erzeugten Speicherschlüssel auf dem Bildschirm angezeigt.

    Form 1 After Save

  • Wenn Sie Code First verwendet haben, sehen Sie auch, dass eine WinFormswithEFSample.ProductContext-Datenbank für Sie erstellt wurde.

    Server Object Explorer