Databinding with WPF (Enlace de datos con WPF)

Importante

Este documento es válido solo para WPF en .NET Framework.

En este documento se describe el enlace de datos para WPF en .NET Framework. Para los nuevos proyectos de .NET Core, se recomienda usar EF Core en lugar de Entity Framework 6. La documentación de enlace de datos en EF Core se encuentra aquí: Introducción a WPF.

En este tutorial paso a paso se muestra cómo enlazar tipos POCO con controles de WPF en un formulario "master-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 una relación de uno a varios: Category (principal o maestro) y Product (dependiente o detallado). Después, las herramientas de Visual Studio se usan para enlazar los tipos definidos en el modelo con los controles de WPF. 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 2013, pero puede realizar este tutorial con Visual Studio 2012 o Visual Studio 2010.

Uso de la opción "Object" para crear orígenes de datos de WPF

Con la versión anterior de Entity Framework se solía recomendar el uso de la opción Base de datos al crear un nuevo origen de datos basado en un modelo creado con EF Designer. Esto se debía a que el diseñador generaba un contexto que se derivaba de ObjectContext y clases de entidad que se derivaban de EntityObject. El uso de la opción Base de datos le ayudará a escribir el mejor código para interactuar con esta superficie de API.

Los diseñadores de EF para Visual Studio 2012 y Visual Studio 2013 generan un contexto que se deriva de DbContext junto con clases simples de entidades POCO. Con Visual Studio 2010 se recomienda cambiar a una plantilla de generación de código que use DbContext como se indica más adelante en este tutorial.

Al usar la superficie de la API DbContext, debe usar la opción Objeto al crear un nuevo origen de datos como se muestra en este tutorial.

Si es necesario, puede revertir a la generación de código basada en ObjectContext para los modelos creados con EF Designer.

Requisitos previos

Es necesario tener instalado Visual Studio 2013, Visual Studio 2012 o Visual Studio 2010 para realizar este tutorial.

Si usa Visual Studio 2010, también tendrá que instalar NuGet. Para obtener más información, vea el tema Instalar NuGet.  

Crear la aplicación

  • Abra Visual Studio.
  • Archivo -> Nuevo -> Proyecto….
  • Seleccione Windows en el panel izquierdo y WPFApplication en el panel derecho.
  • Escriba WPFwithEFSample como nombre.
  • Seleccione Aceptar.

Instalación del paquete NuGet de Entity Framework

  • En el Explorador de soluciones, haga clic con el botón derecho en el proyecto WinFormswithEFSample.
  • Seleccione Administrar paquetes NuGet…
  • En el cuadro de diálogo Administrar paquetes NuGet, seleccione la pestaña En línea y elija el paquete EntityFramework.
  • Haz clic en Instalar

    Nota:

    Además del ensamblado EntityFramework, también se agrega una referencia a System.ComponentModel.DataAnnotations. Si el proyecto tiene una referencia a System.Data.Entity, se quitará cuando se instale el paquete EntityFramework. El ensamblado System.Data.Entity ya no se usa para las aplicaciones de Entity Framework 6.

Definición de un modelo

En este tutorial puede optar por implementar un modelo mediante Code First o EF Designer. Complete una de las dos secciones siguientes.

Opción 1: Definir un modelo mediante Code First

En esta sección se muestra cómo crear un modelo y su base de datos asociada mediante Code First. Vaya a la sección siguiente (Opción 2: Definir un modelo mediante Database First) si prefiere usar Database First para realizar ingeniería inversa del modelo desde una base de datos mediante el diseñador de EF.

Al usar el desarrollo de Code First, normalmente comienza escribiendo clases de .NET Framework que definen el modelo conceptual (dominio).

  • Agregue una nueva clase a WPFwithEFSample:
    • Haga clic con el botón derecho en el nombre del proyecto.
    • Seleccione Agregar y Nuevo elemento.
    • Seleccione Clase y escriba Producto como nombre de clase.
  • Reemplace la definición de la clase Product por el código siguiente:
    namespace WPFwithEFSample
    {
        public class Product
        {
            public int ProductId { get; set; }
            public string Name { get; set; }

            public int CategoryId { get; set; }
            public virtual Category Category { get; set; }
        }
    }
  • Agregue una clase Category con la siguiente definición:
    using System.Collections.ObjectModel;

    namespace WPFwithEFSample
    {
        public class Category
        {
            public Category()
            {
                this.Products = new ObservableCollection<Product>();
            }

            public int CategoryId { get; set; }
            public string Name { get; set; }

            public virtual ObservableCollection<Product> Products { get; private set; }
        }
    }

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 al proyecto con la siguiente definición:
    using System.Data.Entity;

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

Compile el proyecto.

Opción 2: Definir un modelo mediante Database First

En esta sección se muestra cómo usar Database First para invertir el modelo desde una base de datos mediante el diseñador de EF. Si completó la sección anterior (Opción 1: Definir un modelo mediante Code First), omita esta sección y vaya directamente a la sección Carga diferida.

Creación de una base de datos existente

Normalmente, cuando el destino es una base de datos existente, ya estará creada, pero para este tutorial necesitamos crear una base de datos a la que acceder.

El servidor de base de datos instalado con Visual Studio es diferente en función de la versión de Visual Studio que haya instalado:

  • Si usa Visual Studio 2010, va a crear una base de datos SQL Express.
  • Si usa Visual Studio 2012, creará una base de datos LocalDB.

Vamos a continuar y generar la base de datos.

  • Vista -> Explorador de servidores

  • Haga clic con el botón derecho en Conexiones de datos -> Agregar conexión...

  • Si no se ha conectado antes a una base de datos desde el Explorador de servidores, deberá seleccionar Microsoft SQL Server como origen de los datos.

    Change Data Source

  • Conéctese a LocalDB o SQL Express, en función de cuál haya instalado y escriba Products como nombre de base de datos.

    Add Connection LocalDB

    Add Connection Express

  • Seleccione Aceptar y se le preguntará si desea crear una nueva base de datos, seleccione .

    Create Database

  • La nueva base de datos aparecerá ahora en el Explorador de servidores, haga clic con el botón derecho en ella y seleccione Nueva consulta.

  • Copie el siguiente SQL en la nueva consulta y haga clic con el botón derecho en la consulta y seleccione Ejecutar.

    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

Modelo de ingeniería inversa

Vamos a usar Entity Framework Designer, que se incluye como parte de Visual Studio, para crear nuestro modelo.

  • Proyecto -> Agregar nuevo elemento...

  • Seleccione Datos en el menú de la izquierda y, a continuación, ADO.NET Entity Data Model.

  • Escriba ProductModel como nombre y haga clic en Aceptar.

  • Se inicia el Asistente para Entity Data Model.

  • Seleccione Generar desde base de datos y haga clic en Siguiente.

    Choose Model Contents

  • Seleccione la conexión a la base de datos que creó en la primera sección, escriba ProductContext como nombre de la cadena de conexión y haga clic en Siguiente.

    Choose Your Connection

  • Haga clic en la casilla situada junto a "Tablas" para importar todas las tablas y haga clic en "Finalizar".

    Choose Your Objects

Una vez que el proceso de ingeniería inversa completa el nuevo modelo se agrega al proyecto y se abre para que lo vea en Entity Framework Designer. También se ha agregado un archivo App.config al proyecto con los detalles de conexión de la base de datos.

Pasos adicionales en Visual Studio 2010

Si trabaja en Visual Studio 2010, deberá actualizar el diseñador de EF para usar la generación de código EF6.

  • Haga clic con el botón derecho en un lugar vacío del modelo en EF Designer y seleccione Agregar elemento de generación de código...
  • Seleccione Plantillas en línea en el menú de la izquierda y busque DbContext.
  • Seleccione el Generador de DbContext de EF 6.x para C#, escriba ProductsModel como nombre y haga clic en Agregar.

Actualización de la generación de código para el enlace de datos

EF genera código a partir del modelo mediante plantillas T4. Las plantillas enviadas con Visual Studio o descargadas desde la galería de Visual Studio están diseñadas para uso general. Esto significa que las entidades generadas a partir de estas plantillas tienen propiedades ICollection<T> simples. Sin embargo, al realizar el enlace de datos mediante WPF, es conveniente usar ObservableCollection para las propiedades de colección para que WPF pueda realizar un seguimiento de los cambios realizados en las colecciones. Para ello, modificaremos las plantillas para usar ObservableCollection.

  • Abra el Explorador de soluciones y busque el archivo ProductModel.edmx.

  • Busque el archivo ProductModel.tt que se anidará en el archivo ProductModel.edmx.

    WPF Product Model Template

  • Haga doble clic en el archivo ProductModel.tt para abrirlo en el editor de Visual Studio.

  • Busque y reemplace las dos apariciones de "ICollection" por "ObservableCollection". Se encuentran en aproximadamente las líneas 296 y 484.

  • Busque y reemplace la primera aparición de "HashSet" por "ObservableCollection". Esta aparición se encuentra aproximadamente en la línea 50. No reemplace la segunda aparición de HashSet que se encuentra más adelante en el código.

  • Busque y reemplace la única aparición de "System.Collections.Generic" por "System.Collections.ObjectModel". Esta aparición se encuentra aproximadamente en la línea 424.

  • Guarde el archivo ProductModel.tt. Esto debería hacer que se vuelva a generar el código de las entidades. Si el código no se regenera automáticamente, haga clic con el botón derecho en ProductModel.tt y elija "Ejecutar herramienta personalizada".

Si ahora abre el archivo Category.cs (que está anidado en ProductModel.tt), debería ver que la colección Products tiene el tipo ObservableCollection<Product>.

Compile el proyecto.

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, las propiedades de navegación proporcionan una manera de navegar por una relación entre dos tipos de entidad.

EF 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 POCO, EF 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. En la sección de Code First elegimos hacer que las propiedades de navegación sean virtuales por el mismo motivo.

Enlace de objetos a controles

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

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

  • En el menú principal, seleccione Proyecto -> Agregar nuevo origen de datos... (en Visual Studio 2010, debe seleccionar Datos -> Agregar nuevo origen de datos...).

  • En la ventana Elegir un tipo de origen de datos, seleccione Objeto y haga clic en Siguiente.

  • En el cuadro de diálogo Seleccionar los objetos de datos, despliegue WPFwithEFSample dos veces y seleccione Categoría.
    No es necesario seleccionar el origen de datos Product, ya que llegaremos a él a través de la propiedad Product en el origen de datos Categoría

    Select Data Objects

  • Haga clic en Finalizar

  • La ventana Orígenes de datos se abre junto a la ventana MainWindow.xaml. Si la ventana Orígenes de datos no aparece, seleccione Ver -> Otras ventanas -> Orígenes de datos.

  • Presione el icono de anclaje, para que la ventana Orígenes de datos no se oculte automáticamente. Es posible que tenga que presionar el botón Actualizar si la ventana ya estaba visible.

    Data Sources

  • Seleccione el origen de datos Categoría y arrástrelo en el formulario.

Lo siguiente ocurrió cuando arrastramos este origen:

  • El recurso categoryViewSource y el control categoryDataGrid se agregaron a XAML.
  • La propiedad DataContext del elemento Grid primario se estableció en "{StaticResource categoryViewSource }". El recurso categoryViewSource actúa como origen de enlace para el elemento Grid externo o primario. A continuación, los elementos Grid internos heredan el valor DataContext del elemento Grid primario (la propiedad ItemsSource de categoryDataGrid se establece en "{Binding}")
    <Window.Resources>
        <CollectionViewSource x:Key="categoryViewSource"
                                d:DesignSource="{d:DesignInstance {x:Type local:Category}, CreateList=True}"/>
    </Window.Resources>
    <Grid DataContext="{StaticResource categoryViewSource}">
        <DataGrid x:Name="categoryDataGrid" AutoGenerateColumns="False" EnableRowVirtualization="True"
                    ItemsSource="{Binding}" Margin="13,13,43,191"
                    RowDetailsVisibilityMode="VisibleWhenSelected">
            <DataGrid.Columns>
                <DataGridTextColumn x:Name="categoryIdColumn" Binding="{Binding CategoryId}"
                                    Header="Category Id" Width="SizeToHeader"/>
                <DataGridTextColumn x:Name="nameColumn" Binding="{Binding Name}"
                                    Header="Name" Width="SizeToHeader"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>

Adición de una cuadrícula de detalles

Ahora que tenemos una cuadrícula para mostrar Categorías, vamos a agregar una cuadrícula de detalles para mostrar los productos asociados.

  • Seleccione la propiedad Products en el origen de datos Categoría y arrástrela en el formulario.
    • El recurso categoryProductsViewSource y la cuadrícula productDataGrid se agregan a XAML.
    • La ruta de acceso de enlace de este recurso se establece en Products
    • El marco de enlace de datos de WPF garantiza que solo los productos relacionados con la categoría seleccionada aparecen en productDataGrid
  • En el cuadro de herramientas, arrastre Botón al formulario. Establezca la propiedad Name en buttonSave y la propiedad Content en Save.

El formulario debe tener un aspecto similar al siguiente:

Designer Form

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.

  • En la ventana de XAML, haga clic en el elemento <Window, para seleccionar la ventana principal.

  • 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

  • Agregue también el evento Click para el botón Guardar haciendo doble clic en el botón Guardar del diseñador.

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 del elemento MainWindow, como se muestra a continuación.

El código declara una instancia de ejecución prolongada 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 de código proporcionan detalles sobre lo que hace el código.

    using System.Data.Entity;
    using System.Linq;
    using System.Windows;

    namespace WPFwithEFSample
    {
        public partial class MainWindow : Window
        {
            private ProductContext _context = new ProductContext();
            public MainWindow()
            {
                InitializeComponent();
            }

            private void Window_Loaded(object sender, RoutedEventArgs e)
            {
                System.Windows.Data.CollectionViewSource categoryViewSource =
                    ((System.Windows.Data.CollectionViewSource)(this.FindResource("categoryViewSource")));

                // Load is an extension method on IQueryable,
                // defined in the System.Data.Entity namespace.
                // This method enumerates the results of the query,
                // similar to ToList but without creating a list.
                // When used with Linq to Entities this method
                // creates entity objects and adds them to the context.
                _context.Categories.Load();

                // After the data is loaded call the DbSet<T>.Local property
                // to use the DbSet<T> as a binding source.
                categoryViewSource.Source = _context.Categories.Local;
            }

            private void buttonSave_Click(object sender, RoutedEventArgs e)
            {
                // When you delete an object from the related entities collection
                // (in this case Products), the Entity Framework doesn’t mark
                // these child entities as deleted.
                // Instead, it removes the relationship between the parent and the child
                // by setting the parent reference to null.
                // So we manually have to delete the products
                // that have a Category reference set to null.

                // The following code uses LINQ to Objects
                // against the Local collection of Products.
                // 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 use 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);
                    }
                }

                _context.SaveChanges();
                // Refresh the grids so the database generated values show up.
                this.categoryDataGrid.Items.Refresh();
                this.productsDataGrid.Items.Refresh();
            }

            protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
            {
                base.OnClosing(e);
                this._context.Dispose();
            }
        }

    }

Prueba de una aplicación WPF

  • Compile y ejecute la aplicación. Si usó Code First, verá que se crea una base de datos WPFwithEFSample.ProductContext.

  • Escriba un nombre de categoría en la cuadrícula superior y los nombres de producto de la cuadrícula inferior. No escriba nada en las columnas de id. porque la base de datos genera la clave principal.

    Main Window with new categories and products

  • Presione el botón Guardar para guardar los datos en la base de datos.

Después de la llamada a SaveChanges() de DbContext, los identificadores se rellenan con los valores generados por la base de datos. Dado que hemos llamado a Refresh() después de SaveChanges() los controles DataGrid también se actualizan con los nuevos valores.

Main Window with IDs populated

Recursos adicionales

Para más información sobre el enlace de datos a colecciones mediante WPF, consulte este tema en la documentación de WPF.