Introduzione a WPF

Questa procedura dettagliata illustra come associare tipi POCO ai controlli WPF in un modulo "main-detail". L'applicazione usa le API di 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\main) e Product (dependent\detail). Il framework di data binding WPF 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 2019 16.6.5.

Suggerimento

È possibile visualizzare l'esempio di questo articolo in GitHub.

Prerequisiti

Per completare questa procedura dettagliata, è necessario che Visual Studio 2019 16.3 o versione successiva sia installato con il carico di lavoro desktop .NET selezionato. Per altre informazioni sull'installazione della versione più recente di Visual Studio, vedere Installare Visual Studio.

Creare l'applicazione

  1. Aprire Visual Studio.
  2. Nella finestra iniziale scegliere Crea nuovo progetto.
  3. Cercare "WPF", scegliere App WPF (.NET Core) e quindi scegliere Avanti.
  4. Nella schermata successiva assegnare un nome al progetto, ad esempio GetStartedWPF e scegliere Crea.

Installare i pacchetti NuGet di Entity Framework

  1. Fare clic con il pulsante destro del mouse sulla soluzione e scegliere Gestisci pacchetti NuGet per la soluzione...

    Manage NuGet Packages

  2. Digitare entityframeworkcore.sqlite nella casella di ricerca.

  3. Selezionare il pacchetto Microsoft.EntityFrameworkCore.Sqlite .

  4. Controllare il progetto nel riquadro destro e fare clic su Installa

    Sqlite Package

  5. Ripetere i passaggi per cercare entityframeworkcore.proxies e installare Microsoft.EntityFrameworkCore.Proxies.

Nota

Quando è stato installato il pacchetto Sqlite, viene automaticamente eseguito il pull del pacchetto di base Microsoft.EntityFrameworkCore correlato. Il pacchetto Microsoft.EntityFrameworkCore.Proxies fornisce il supporto per i dati "lazy-loading". Ciò significa che quando si dispone di entità con entità figlio, solo gli elementi padre vengono recuperati sul carico iniziale. I proxy rilevano quando viene effettuato un tentativo di accesso alle entità figlio e li carica automaticamente su richiesta.

Definire un modello

In questa procedura dettagliata si implementerà un modello usando "code first". Questo significa che EF Core creerà le tabelle di database e lo schema in base alle classi C# definite.

Aggiungere una nuova classe. Assegnare il nome: Product.cs e popolarlo come segue:

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

Aggiungere quindi una classe denominata Category.cs e popolarla con il codice seguente:

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

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à.

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

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 ProductContext.cs classe al progetto con la definizione seguente:

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 Indica a EF Core quali entità C# devono essere mappate al database.
  • Esistono diversi modi per configurare EF Core DbContext. Per informazioni su di essi, vedere Configuring a DbContext (Configurazione di un oggetto DbContext).
  • In questo esempio viene utilizzata l'override OnConfiguring per specificare un file di dati Sqlite.
  • La UseLazyLoadingProxies chiamata indica a EF Core di implementare il caricamento differita, in modo che le entità figlio vengano caricate automaticamente quando si accede dall'elemento padre.

Premere CTRL+MAIUSC+B o passare a Compila > soluzione per compilare il progetto.

Suggerimento

Informazioni sui diversi modi per mantenere sincronizzati i modelli di database e EF Core: Gestione degli schemi di database.

Caricamento lazy

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

EF Core 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 i tipi di entità "Plain Old C# Object" (POCO), EF Core 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 di 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 usa Database First, le proprietà di navigazione vengono automaticamente rese virtuali per abilitare il caricamento differita.

Associare l'oggetto ai controlli

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

  1. Fare doppio clic su MainWindow.xaml in Esplora soluzioni per aprire il modulo principale

  2. Scegliere la scheda XAML per modificare il codice XAML.

  3. Subito dopo il tag di apertura Window , aggiungere le origini seguenti per connettersi alle entità di EF Core.

    <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. Viene impostata l'origine per le categorie "padre" e la seconda origine per i prodotti "dettagli".

  5. Aggiungere quindi il markup seguente al codice XAML dopo il tag di apertura Grid .

    <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. Si noti che CategoryId è impostato su ReadOnly perché è assegnato dal database e non può essere modificato.

Aggiunta di una griglia dettagli

Ora che la griglia esiste per visualizzare le categorie, è possibile aggiungere la griglia dei dettagli per visualizzare i prodotti. Aggiungerlo all'interno dell'elemento Grid , dopo l'elemento categories DataGrid .

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>

Aggiungere infine un Save pulsante e collegare l'evento Click a Button_Click.

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

La visualizzazione progettazione dovrebbe essere simile alla seguente:

Screenshot of WPF Designer

Aggiungere codice che gestisce l'interazione dei dati

È il momento di aggiungere alcuni gestori eventi alla finestra principale.

  1. Nella finestra XAML fare clic sull'elemento <Window> per selezionare la finestra principale.

  2. Nella finestra Proprietà scegliere Eventi in alto a destra, quindi fare doppio clic sulla casella di testo a destra dell'etichetta Caricata.

    Main Window Properties

Verrà ora modificato il codice da usare ProductContext per eseguire l'accesso ai dati. Aggiornare il codice 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 Dispose() metodo nell'istanza ProductContext viene quindi chiamato dal metodo sottoposto OnClosing a override. I commenti del codice spiegano cosa accade in ogni passaggio.

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

Nota

Il codice usa una chiamata a EnsureCreated() per compilare il database alla prima esecuzione. Ciò è accettabile per le demo, ma nelle app di produzione è consigliabile esaminare le migrazioni per gestire lo schema. Il codice viene eseguito anche in modo sincrono perché usa un database SQLite locale. Per gli scenari di produzione che in genere coinvolgono un server remoto, è consigliabile usare le versioni asincrone dei Load metodi e SaveChanges .

Testare l'applicazione WPF

Compilare ed eseguire l'applicazione premendo F5 o scegliendo Debug > Avvia debug debug. Il database deve essere creato automaticamente con un file denominato products.db. Immettere un nome di categoria e premere INVIO, quindi aggiungere i prodotti alla griglia inferiore. Fare clic su Salva e controllare l'aggiornamento della griglia con gli ID forniti dal database. Evidenziare una riga e premere Elimina per rimuovere la riga. L'entità verrà eliminata quando si fa clic su Salva.

Running application

Notifica di modifiche alle proprietà

Questo esempio si basa su quattro passaggi per sincronizzare le entità con l'interfaccia utente.

  1. La chiamata _context.Categories.Load() iniziale carica i dati delle categorie.
  2. I proxy di caricamento differita caricano i dati dei prodotti dipendenti.
  3. Il rilevamento delle modifiche predefinito di EF Core apporta le modifiche necessarie alle entità, inclusi inserimenti ed eliminazioni, quando _context.SaveChanges() viene chiamato.
  4. Chiamate per forzare DataGridView.Items.Refresh() un ricaricamento con gli ID appena generati.

Questa operazione funziona per l'esempio introduttivo, ma potrebbe essere necessario codice aggiuntivo per altri scenari. I controlli WPF eseguono il rendering dell'interfaccia utente leggendo i campi e le proprietà nelle entità. Quando si modifica un valore nell'interfaccia utente, tale valore viene passato all'entità. Quando si modifica il valore di una proprietà direttamente nell'entità, ad esempio caricandolo dal database, WPF non rifletterà immediatamente le modifiche nell'interfaccia utente. Il motore di rendering deve ricevere una notifica delle modifiche. Il progetto ha eseguito questa operazione chiamando Refresh()manualmente . Un modo semplice per automatizzare questa notifica consiste nell'implementare l'interfaccia INotifyPropertyChanged . I componenti WPF rileveranno automaticamente l'interfaccia e eseguiranno la registrazione per gli eventi di modifica. L'entità è responsabile della generazione di questi eventi.

Suggerimento

Per altre informazioni su come gestire le modifiche, vedere Come implementare la notifica delle modifiche delle proprietà.

Passaggi successivi

Altre informazioni sulla configurazione di un oggetto DbContext.