Erste Schritte mit WPF

In dieser schrittweisen exemplarischen Vorgehensweise wird gezeigt, wie POCO-Typen an WPF-Steuerelemente in einem „main-detail“-Formular gebunden werden. Die Anwendung verwendet die Entity Framework-APIs 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\main) und Product (dependent\detail). Das WPF-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 2019 16.6.5.

Tipp

Das in diesem Artikel verwendete Beispiel finden Sie auf GitHub.

Voraussetzungen

Um diese exemplarische Vorgehensweise nachvollziehen zu können, müssen Sie Visual Studio 2019 16.3 oder höher mit ausgewählter .NET-Desktopworkload installiert haben. Weitere Informationen zur Installation der neuesten Version von Visual Studio finden Sie unter Installieren von Visual Studio.

Erstellen der Anwendung

  1. Öffnen Sie Visual Studio.
  2. Wählen Sie im Startfenster Neues Projekt erstellen aus.
  3. Suchen Sie nach „WPF“, wählen Sie WPF-App (.NET Core) aus, und wählen Sie dann Weiter aus.
  4. Geben Sie dem Projekt auf dem nächsten Bildschirm einen Namen (z. B. GetStartedWPF), und wählen Sie Erstellen aus.

Installieren des Entity Framework-NuGet-Pakets

  1. Klicken Sie mit der rechten Maustaste auf die Projektmappe, und wählen Sie NuGet-Pakete für Projektmappe verwalten aus.

    Manage NuGet Packages

  2. Geben Sie im Suchfeld als Suchbegriff entityframeworkcore.sqlite ein.

  3. Wählen Sie das Paket Microsoft.EntityFrameworkCore.Sqlite aus.

  4. Wählen Sie das Projekt im rechten Bereich aus, und klicken Sie auf Installieren.

    Sqlite Package

  5. Wiederholen Sie die Schritte, um nach entityframeworkcore.proxies zu suchen und Microsoft.EntityFrameworkCore.Proxies zu installieren.

Hinweis

Als Sie das SQLite-Paket installiert haben, wurde das zugehörige Microsoft.EntityFrameworkCore-Basispaket automatisch heruntergeladen. Das Microsoft. EntityFrameworkCore.Proxies-Paket unterstützt „Lazy Loading“ von Daten. Dies bedeutet, dass bei Entitäten mit untergeordneten Entitäten nur die übergeordneten Elemente beim anfänglichen Laden abgerufen werden. Die Proxys erkennen, wenn versucht wird, auf die untergeordneten Entitäten zuzugreifen. Diese werden bei Bedarf automatisch geladen.

Definieren eines Modells

In dieser exemplarischen Vorgehensweise implementieren Sie ein Modell über „Code First“. Dies bedeutet, dass EF Core die Datenbanktabellen und das Schema basierend auf den von Ihnen definierten C#-Klassen erstellt.

Fügen Sie eine neue Klasse hinzu. Geben Sie ihr den Namen Product.cs, und füllen Sie sie wie folgt mit Daten auf:

Product.cs

namespace GetStartedWPF
{
    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 dann eine Klasse mit dem Namen Category.cs hinzu, und füllen Sie sie mit folgendem Code auf:

Category.cs

using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace GetStartedWPF
{
    public class Category
    {
        public int CategoryId { get; set; }
        public string Name { get; set; }

        public virtual ICollection<Product>
            Products
        { get; private set; } =
            new ObservableCollection<Product>();
    }
}

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.

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<TEntity>-Eigenschaften informieren den Kontext darüber, welche Typen Sie in das Modell einbeziehen möchten.

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.cs-Klasse mit der folgenden Definition hinzu:

ProductContext.cs

using Microsoft.EntityFrameworkCore;

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

        protected override void OnConfiguring(
            DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite(
                "Data Source=products.db");
            optionsBuilder.UseLazyLoadingProxies();
        }
    }
}
  • DbSet informiert EF Core, welche C#-Entitäten der Datenbank zugeordnet werden sollen.
  • Es gibt eine Vielzahl von Möglichkeiten, DbContext von EF Core zu konfigurieren. Informationen dazu finden Sie unter: Konfigurieren eines DbContexts.
  • In diesem Beispiel wird die OnConfiguring-Überschreibung verwendet, um eine SQLite-Datendatei anzugeben.
  • Der UseLazyLoadingProxies-Aufruf informiert EF Core, Lazy Loading zu implementieren, damit untergeordnete Entitäten automatisch geladen werden, wenn der Zugriff über das übergeordnete Element erfolgt.

Drücken Sie STRG+UMSCHALT+B, oder navigieren Sie zu Build > Projektmappe erstellen, um das Projekt zu kompilieren.

Tipp

Informieren Sie sich über die verschiedenen Möglichkeiten, die Datenbank und EF Core-Modelle synchron zu halten: Verwalten von Datenbankschemas.

Verzögertes Laden

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

EF Core 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 („Plain Old C# Object“) erreicht EF Core 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.

Binden von Objekten an Steuerelemente

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

  1. Doppelklicken Sie im Projektmappen-Explorer auf MainWindow.xaml, um das Hauptformular zu öffnen.

  2. Wählen Sie die Registerkarte XAML aus, um den XAML-Code zu bearbeiten.

  3. Fügen Sie direkt nach dem öffnenden Window-Tag die folgenden Quellen hinzu, um eine Verbindung mit den EF Core-Entitäten herzustellen.

    <Window x:Class="GetStartedWPF.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:GetStartedWPF"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800" Loaded="Window_Loaded">
        <Window.Resources>
            <CollectionViewSource x:Key="categoryViewSource"/>
            <CollectionViewSource x:Key="categoryProductsViewSource" 
                                  Source="{Binding Products, Source={StaticResource categoryViewSource}}"/>
        </Window.Resources>
    
  4. Dies legt die Quelle für die „übergeordneten“ Kategorien und die zweite Quelle für die „detail“-Produkte fest.

  5. Fügen Sie dem XAML dann das folgende Markup nach dem öffnenden Grid-Tag hinzu.

    <DataGrid x:Name="categoryDataGrid" AutoGenerateColumns="False" 
              EnableRowVirtualization="True" 
              ItemsSource="{Binding Source={StaticResource categoryViewSource}}" 
              Margin="13,13,43,229" RowDetailsVisibilityMode="VisibleWhenSelected">
        <DataGrid.Columns>
            <DataGridTextColumn Binding="{Binding CategoryId}"
                                Header="Category Id" Width="SizeToHeader"
                                IsReadOnly="True"/>
            <DataGridTextColumn Binding="{Binding Name}" Header="Name" 
                                Width="*"/>
        </DataGrid.Columns>
    </DataGrid>
    
  6. Beachten Sie, dass CategoryId auf ReadOnly festgelegt ist, da die ID von der Datenbank zugewiesen wird und nicht geändert werden kann.

Hinzufügen eines Detailrasters

Da das Raster nun zum Anzeigen von Kategorien vorhanden ist, kann das Detailraster zum Anzeigen von Produkten hinzugefügt werden. Fügen Sie dieses innerhalb des Grid-Elements nach dem DataGrid-Element der Kategorien hinzu.

MainWindow.xaml

<DataGrid x:Name="productsDataGrid" AutoGenerateColumns="False" 
          EnableRowVirtualization="True" 
          ItemsSource="{Binding Source={StaticResource categoryProductsViewSource}}" 
          Margin="13,205,43,108" RowDetailsVisibilityMode="VisibleWhenSelected" 
          RenderTransformOrigin="0.488,0.251">
    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding CategoryId}" 
                            Header="Category Id" Width="SizeToHeader"
                            IsReadOnly="True"/>
        <DataGridTextColumn Binding="{Binding ProductId}" Header="Product Id" 
                            Width="SizeToHeader" IsReadOnly="True"/>
        <DataGridTextColumn Binding="{Binding Name}" Header="Name" Width="*"/>
    </DataGrid.Columns>
</DataGrid>

Fügen Sie schließlich eine Schaltfläche Save hinzu, und verbinden Sie das click-Ereignis mit Button_Click.

<Button Content="Save" HorizontalAlignment="Center" Margin="0,240,0,0" 
        Click="Button_Click" Height="20" Width="123"/>

Die Entwurfsansicht sollte wie folgt aussehen:

Screenshot of WPF Designer

Hinzufügen von Code, der die Dateninteraktion verarbeitet

Es ist an der Zeit, dem Hauptfenster einige Ereignishandler hinzuzufügen.

  1. Klicken Sie im XAML-Fenster auf das Fenster <Window>-Element, um das Hauptfenster auszuwählen.

  2. Wählen Sie im Fenster Eigenschaften oben rechts Ereignisse aus, und doppelklicken Sie dann auf das Textfeld rechts neben der Bezeichnung Geladen.

    Main Window Properties

Damit gelangen Sie zum Code Behind für das Formular. Nun bearbeiten wir den Code so, dass ProductContext für den Datenzugriff verwendet wird. Aktualisieren Sie den Code wie unten gezeigt.

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. In den Codekommentaren wird erläutert, was jeder Schritt bewirkt.

MainWindow.xaml.cs

using Microsoft.EntityFrameworkCore;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;

namespace GetStartedWPF
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private readonly ProductContext _context =
            new ProductContext();

        private CollectionViewSource categoryViewSource;

        public MainWindow()
        {
            InitializeComponent();
            categoryViewSource =
                (CollectionViewSource)FindResource(nameof(categoryViewSource));
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // this is for demo purposes only, to make it easier
            // to get up and running
            _context.Database.EnsureCreated();

            // load the entities into EF Core
            _context.Categories.Load();

            // bind to the source
            categoryViewSource.Source =
                _context.Categories.Local.ToObservableCollection();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            // all changes are automatically tracked, including
            // deletes!
            _context.SaveChanges();

            // this forces the grid to refresh to latest values
            categoryDataGrid.Items.Refresh();
            productsDataGrid.Items.Refresh();
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            // clean up database connections
            _context.Dispose();
            base.OnClosing(e);
        }
    }
}

Hinweis

Der Code verwendet einen Aufruf von EnsureCreated() zum Erstellen der Datenbank bei der ersten Ausführung. Dies ist für Demos akzeptabel, aber in Produktions-Apps sollten Sie Migrationen verwenden, um Ihr Schema zu verwalten. Der Code wird auch synchron ausgeführt, da er eine lokale SQLite-Datenbank verwendet. In Produktionsszenarien, in denen normalerweise ein Remoteserver verwendet wird, sollten Sie die Verwendung der asynchronen Versionen der Methoden Load und SaveChanges in Erwägung ziehen.

Testen der WPF-Anwendung

Kompilieren Sie die Anwendung, und führen Sie sie aus, indem Sie F5 drücken oder Debuggen > Debuggen starten auswählen. Die Datenbank sollte automatisch mit einer Datei namens products.db erstellt werden. Geben Sie einen Kategorienamen ein, und drücken Sie die EINGABETASTE. Fügen Sie dann dem unteren Raster Produkte hinzu. Klicken Sie auf „Speichern“, und beobachten Sie, wie das Raster mit den von der Datenbank bereitgestellten IDs aktualisiert wird. Markieren Sie eine Zeile, und klicken Sie auf Löschen, um die Zeile zu entfernen. Die Entität wird gelöscht, wenn Sie auf Speichern klicken.

Running application

Benachrichtigung der Eigenschaftenänderung

Dieses Beispiel basiert auf vier Schritten, um die Entitäten mit der Benutzeroberfläche zu synchronisieren.

  1. Der anfängliche Aufruf von _context.Categories.Load() lädt die Kategoriedaten.
  2. Die Lazy Loading-Proxys laden die Daten der abhängigen Produkte.
  3. Die integrierte Änderungsnachverfolgung von EF Core nimmt die erforderlichen Änderungen an Entitäten vor, einschließlich Einfügungen und Löschungen, wenn _context.SaveChanges() aufgerufen wird.
  4. Die Aufrufe von DataGridView.Items.Refresh() erzwingen einen erneuten Ladevorgang mit den neu generierten IDs.

Dies funktioniert für unser Beispiel mit ersten Schritten, aber Sie benötigen möglicherweise zusätzlichen Code für andere Szenarien. WPF-Steuerelemente rendern die Benutzeroberfläche, indem sie die Felder und Eigenschaften für Ihre Entitäten lesen. Wenn Sie einen Wert in der Benutzeroberfläche (User Interface, UI) bearbeiten, wird dieser Wert an Ihre Entität übergeben. Wenn Sie den Wert einer Eigenschaft direkt für Ihre Entität ändern, z. B. das Laden aus der Datenbank, spiegelt WPF die Änderungen nicht sofort in der Benutzeroberfläche wider. Die Rendering-Engine muss über die Änderungen benachrichtigt werden. Das Projekt hat dies durch manuelles Aufrufen von Refresh() erreicht. Eine einfache Möglichkeit, diese Benachrichtigung zu automatisieren, besteht darin, die INotifyPropertyChanged-Schnittstelle zu implementieren. WPF-Komponenten erkennen die Schnittstelle automatisch und registrieren sich für Änderungsereignisse. Die Entität ist für das Auslösen dieser Ereignisse verantwortlich.

Tipp

Weitere Informationen zum Behandeln von Änderungen finden Sie unter: Implementieren von Benachrichtigungen zur Änderung von Eigenschaften.

Nächste Schritte

Weitere Informationen zum Konfigurieren eines DbContext.