Introducción a WPF

En este tutorial paso a paso se muestra cómo enlazar tipos POCO a controles de WPF en un formulario "main-detail". La aplicación utiliza las API de Entity Framework para rellenar los objetos con datos de la base de datos, realizar un seguimiento de los cambios y conservar los datos en la base de datos.

El modelo define dos tipos que participan en la relación de uno a varios: Category (principal) y Product (dependiente o detallado). El marco de enlace de datos de WPF permite la navegación entre objetos relacionados: la selección de filas en la vista maestra hace que la vista de detalles se actualice con los datos secundarios correspondientes.

Las capturas de pantalla y las listas de código de este tutorial se han tomado de Visual Studio 2019 16.6.5.

Sugerencia

Puede ver un ejemplo de este artículo en GitHub.

Requisitos previos

Debe tener Visual Studio 2019 16.3 o posterior instalado con la carga de trabajo de escritorio de .NET seleccionada para completar este tutorial. Para obtener más información sobre cómo instalar la última versión de Visual Studio, vea Instalación de Visual Studio.

Crear la aplicación

  1. Abra Visual Studio.
  2. En la ventana de inicio, elija Crear proyecto.
  3. Busque "WPF", elija Aplicación WPF (.NET Core) y después seleccione Siguiente.
  4. En la pantalla siguiente, asigne un nombre al proyecto, por ejemplo, GetStartedWPF, y elija Crear.

Instalación de los paquetes de NuGet Entity Framework

  1. Haga clic con el botón derecho en la solución y elija Administrar paquetes NuGet para la solución...

    Manage NuGet Packages

  2. Escriba entityframeworkcore.sqlite en el cuadro de búsqueda.

  3. Instale el paquete Microsoft.EntityFrameworkCore.Sqlite.

  4. Compruebe el proyecto en el panel derecho y haga clic en Instalar.

    Sqlite Package

  5. Repita los pasos para buscar entityframeworkcore.proxies e instalar Microsoft.EntityFrameworkCore.Proxies.

Nota:

Al instalar el paquete de Sqlite, se extrae automáticamente el paquete de base Microsoft.EntityFrameworkCore relacionado. El paquete Microsoft.EntityFrameworkCore.Proxies proporciona compatibilidad con los datos de "carga diferida". Esto significa que, si tiene entidades con entidades secundarias, solo se capturan los elementos primarios en la carga inicial. Los proxies detectan cuándo se produce un intento de acceso a las entidades secundarias, y las cargan automáticamente a petición.

Definición de un modelo

En este tutorial, implementará un modelo mediante "code first". Esto significa que EF Core creará las tablas de base de datos y el esquema en función de las clases de C# que usted defina.

Agregue una nueva clase. Asígnele el nombre Product.cs y rellénelo como se indica a continuación:

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

A continuación, agregue una clase denominada Category.cs y rellénela con el código siguiente:

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 propiedad Products de la clase Category y la propiedad Category de la clase Product son propiedades de navegación. En Entity Framework, las propiedades de navegación proporcionan una manera de navegar por una relación entre dos tipos de entidad.

Además de definir entidades, debe definir una clase que derive de DbContext y exponga las propiedades DbSet<TEntity>. Las propiedades DbSet<TEntity> permiten que el contexto sepa qué tipos desea incluir en el modelo.

Una instancia del tipo derivado de DbContext administra los objetos de entidad durante el tiempo de ejecución, lo que incluye rellenar los objetos con datos de una base de datos, el seguimiento de cambios y la persistencia de datos en la base de datos.

Agregue una nueva clase ProductContext.cs al proyecto con la siguiente definición:

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 informa a EF Core de qué entidades de C# se deben asignar a la base de datos.
  • Hay varias maneras de configurar DbContext de EF Core. Puede leer sobre ello en: Configurar un DbContext.
  • En este ejemplo se usa la invalidación OnConfiguring para especificar un archivo de datos de Sqlite.
  • La llamada a UseLazyLoadingProxies indica a EF Core que implemente la carga diferida, por lo que las entidades secundarias se cargan automáticamente cuando se obtiene acceso a ellas desde el elemento primario.

Presione CTRL + MAYÚS + B o vaya a Compilar > Compilar solución para compilar el proyecto.

Sugerencia

Obtenga información sobre los distintos pasos para mantener sincronizados la base de datos y los modelos de EF Core: Administrar esquemas de base de datos.

Carga diferida

La propiedad Products de la clase Category y la propiedad Category de la clase Product son propiedades de navegación. En Entity Framework Core, las propiedades de navegación proporcionan una manera de navegar por una relación entre dos tipos de entidad.

EF Core ofrece una opción para cargar automáticamente las entidades relacionadas desde la base de datos la primera vez que se accede a la propiedad de navegación. Con este tipo de carga (denominado carga diferida), tenga en cuenta que la primera vez que se accede a cada propiedad de navegación se ejecutará una consulta independiente en la base de datos si el contenido no está ya en el contexto.

Al usar tipos de entidad "Plain Old C# Object" (POCO), EF Core logra la carga diferida mediante la creación de instancias de tipos de proxy derivados durante el tiempo de ejecución y, después, la invalidación de las propiedades virtuales de las clases para agregar el enlace de carga. Para obtener la carga diferida de los objetos relacionados, debe declarar captadores de propiedades de navegación como público y virtual (Overridable en Visual Basic), y la clase no debe sellarse (NotOverridable en Visual Basic). Al usar Database First, las propiedades de navegación se convierten automáticamente en virtuales para habilitar la carga diferida.

Enlace de objetos a controles

Agregue las clases que se definen en el modelo como orígenes de datos para esta aplicación WPF.

  1. Haga doble clic en MainWindow.xaml en el Explorador de soluciones para abrir el formulario principal.

  2. Elija la pestaña XAML para editar el código XAML.

  3. Inmediatamente después de la etiqueta de apertura Window, agregue los orígenes siguientes para conectarse a las entidades de 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. De esta forma, se configura el origen para las categorías "principales" y el segundo origen para los productos "detallados".

  5. A continuación, agregue el marcado siguiente al código XAML después de la etiqueta de 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. Tenga en cuenta que el elemento CategoryId se establece en ReadOnly porque está asignado por la base de datos y no se puede cambiar.

Adición de una cuadrícula de detalles

Ahora que la cuadrícula existe para mostrar las categorías, se puede agregar la cuadrícula de detalles para mostrar los productos. Añádala dentro del elemento Grid, después del elemento de categorías 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>

Por último, agregue un botón Save y una conexión en el evento de clic a Button_Click.

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

La vista de diseño debería tener un aspecto similar a este:

Screenshot of WPF Designer

Adición de código que controla la interacción con los datos

Es el momento de agregar algunos controladores de eventos a la ventana principal.

  1. En la ventana de XAML, haga clic en el elemento <Ventana>, para seleccionar la ventana principal.

  2. En la ventana Propiedades, elija Eventos en la parte superior derecha y, a continuación, haga doble clic en el cuadro de texto que se encuentra a la derecha de la etiqueta Cargado.

    Main Window Properties

Esto lo lleva al código subyacente del formulario; ahora, se editará el código para usar el elemento ProductContext para ejecutar el acceso a los datos. Actualice el código como se muestra a continuación.

El código declara una instancia de larga duración de ProductContext. El objeto ProductContext se utiliza para consultar y guardar datos en la base de datos. A continuación, se llama al método Dispose() en la instancia de ProductContext desde el método OnClosing invalidado. Los comentarios del código explican qué hace cada paso.

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:

El código usa una llamada a EnsureCreated() para compilar la base de datos en la primera ejecución. Esto es aceptable para demostraciones, pero en las aplicaciones de producción debe examinar migraciones para administrar el esquema. El código también se ejecuta de forma sincrónica porque usa una base de datos SQLite local. En escenarios de producción que normalmente implican un servidor remoto, considere la posibilidad de utilizar las versiones asincrónicas de los métodos Load y SaveChanges.

Prueba de una aplicación WPF

Para compilar y ejecutar la aplicación, presione F5 o seleccione Depurar > Iniciar depuración. La base de datos debe crearse automáticamente con un archivo denominado products.db. Escriba un nombre de categoría y presione ENTRAR y, a continuación, agregue productos a la cuadrícula inferior. Haga clic en Guardar y observe la actualización de la cuadrícula con los identificadores proporcionados por la base de datos. Resalte una fila y presione Eliminar para quitarla. La entidad se eliminará al hacer clic en Guardar.

Running application

Notificación de cambio de propiedad

Este ejemplo se basa en cuatro pasos para sincronizar las entidades con la interfaz de usuario.

  1. La llamada inicial a _context.Categories.Load() carga los datos de categorías.
  2. Los proxies de carga diferida cargan los datos de los productos dependientes.
  3. El seguimiento de cambios integrado de EF Core realiza las modificaciones necesarias en las entidades, incluidas las inserciones y eliminaciones, cuando se llama a _context.SaveChanges().
  4. Las llamadas a DataGridView.Items.Refresh() fuerzan una recarga con los identificadores recién generados.

Esto sirve para el ejemplo de introducción, pero puede que necesite código adicional para otros escenarios. Los controles de WPF representan la interfaz de usuario mediante la lectura de los campos y las propiedades de las entidades. Cuando se edita un valor en la interfaz de usuario (UI), ese valor se pasa a la entidad. Al cambiar el valor de una propiedad directamente en la entidad, como cargarla desde la base de datos, WPF no reflejará inmediatamente los cambios en la interfaz de usuario. El motor de representación debe recibir notificaciones de los cambios. El proyecto lo hizo llamando manualmente a Refresh(). Una manera sencilla de automatizar esta notificación es mediante la implementación de la interfaz INotifyPropertyChanged. Los componentes de WPF detectarán automáticamente la interfaz y se registrarán para los eventos de cambio. La entidad es responsable de generar estos eventos.

Sugerencia

Para obtener más información sobre cómo controlar los cambios, lea: Cómo implementar la notificación de cambio de propiedad.

Pasos siguientes

Más información sobre la Configuración de un DbContext.