Databinding with WinForms (Enlace de datos con WinForms)
En este tutorial paso a paso se muestra cómo enlazar tipos POCO a controles de Formularios de ventana (WinForms) en un formulario "master-detail". La aplicación utiliza 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). A continuación, las herramientas de Visual Studio se usan para enlazar los tipos definidos en el modelo a los controles de WinForms. El marco de enlace de datos de WinForms 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 descripciones de código de este tutorial se toman de Visual Studio 2013, pero puede completar este tutorial con Visual Studio 2012 o Visual Studio 2010.
Requisitos previos
Debe tener Instalado Visual Studio 2013, Visual Studio 2012 o Visual Studio 2010 para completar este tutorial.
Si usa Visual Studio 2010, también tiene 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 Windows FormsAplicación en el panel derecho.
- Escriba WinFormswithEFSample como nombre.
- Seleccione Aceptar.
Instalación de los paquetes NuGet de Entity Framework
- En el Explorador de soluciones, haga clic con el botón derecho del ratón 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.
Implementación de IListSource para colecciones
Las propiedades de colección deben implementar la interfaz IListSource para habilitar el enlace de datos bidireccional con la ordenación al usar Windows Forms. Para ello, vamos a extender ObservableCollection para agregar la funcionalidad IListSource.
- Agregue una clase ObservableListSource al proyecto:
- Haga clic con el botón derecho en el nombre del proyecto.
- Seleccione Agregar -> Nuevo elemento.
- Seleccione Clase y escriba ObservableListSource como nombre de clase.
- Reemplace el código generado de forma predeterminada por el código siguiente:
Esta clase habilita el enlace de datos bidireccional, así como la ordenación. La clase deriva de ObservableCollection<T> y agrega una implementación explícita de IListSource. El método GetList() de IListSource se implementa para devolver una implementación de IBindingList que permanece sincronizada con ObservableCollection. La implementación de IBindingList generada por ToBindingList admite la ordenación. El método de extensión ToBindingList se define en el ensamblado de EntityFramework.
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());
}
}
}
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).
- Agregar una nueva clase de Producto al proyecto
- Reemplace el código generado de forma predeterminada por el código siguiente:
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; }
}
}
- Agregue una clase de Categoría al proyecto.
- Reemplace el código generado de forma predeterminada por el código siguiente:
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; } }
}
}
Además de definir entidades, debe definir una clase que derive de DbContext y exponga las propiedades DbSet<TEntity>. Las propiedades DbSet permiten que el contexto sepa qué tipos desea incluir en el modelo. Los tipos DbContext y DbSet se definen en el ensamblado de EntityFramework.
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.
- Reemplace el código generado de forma predeterminada por el código siguiente:
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; }
}
}
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.
Conéctese a LocalDB o SQL Express, en función de cuál haya instalado y escriba Products como nombre de base de datos.
Seleccione Aceptar y se le preguntará si desea crear una nueva base de datos, seleccione Sí.
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.
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.
Haga clic en la casilla situada junto a "Tablas" para importar todas las tablas y haga clic en "Finalizar".
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, es conveniente tener propiedades de colección que implementen IListSource. Este es el motivo por el que creamos la clase ObservableListSource anterior y ahora vamos a modificar las plantillas para usar esta clase.
Abra el Explorador de soluciones y busque el archivo ProductModel.edmx.
Busque el archivo ProductModel.tt que se anidará en el archivo ProductModel.edmx.
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 "ObservableListSource". Se encuentran en aproximadamente las líneas 296 y 484.
Busque y reemplace la primera aparición de "hashSet" por "ObservableListSource". Esta aparición se encuentra en aproximadamente la línea 50. No reemplace la segunda aparición de HashSet que se encuentra más adelante en el código.
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 ObservableListSource<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 estar sellada (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 de WinForms.
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 WinFormswithEFSample dos veces y seleccione Categoría. No es necesario seleccionar el origen de datos Producto, porque llegaremos a él a través de la propiedad Producto del origen de datos Categoría.
Haga clic en Finalizar. Si la ventana Orígenes de datos no aparece, seleccione Vista -> 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.
En el Explorador de soluciones, haga doble clic en el archivo Form1.cs para abrir el formulario principal en el diseñador.
Seleccione el origen de datos Categoría y arrástrelo en el formulario. De forma predeterminada, se agregan un nuevo control de DataGridView (categoryDataGridView) y barra de herramientas de navegación al diseñador. Estos controles se enlazan a los componentes BindingSource (categoryBindingSource) y Binding Navigator (categoryBindingNavigator) que también se crean.
Edite las columnas de categoryDataGridView. Queremos establecer la columna CategoryId en solo lectura. La base de datos genera el valor de la propiedad CategoryId después de guardar los datos.
- Haga clic con el botón derecho en el control DataGridView y seleccione Editar columnas.…
- Seleccione la columna CategoryId y establezca ReadOnly en True.
- Presione Aceptar.
Seleccione Productos en el origen de datos Categoría y arrástrelo en el formulario. productDataGridView y productBindingSource se agregan al formulario.
Edite las columnas en productDataGridView. Queremos ocultar las columnas CategoryId y Category y establecer ProductId en solo lectura. La base de datos genera el valor de la propiedad ProductId después de guardar los datos.
- Haga clic con el botón derecho en el control de DataGridView y seleccione Editar columnas....
- Seleccione la columna ProductId y establezca ReadOnly en True.
- Seleccione la columna CategoryId y presione el botón Quitar. Haga lo mismo con la columna Categoría.
- Haga clic en Aceptar.
Hasta ahora, asociamos nuestros controles DataGridView con componentes BindingSource en el diseñador. En la sección siguiente agregaremos código al código subyacente para establecer categoryBindingSource.DataSource en la colección de entidades de las que DbContext realiza el seguimiento actualmente. Cuando arrastramos y quitamos Productos debajo de la categoría, WinForms se encargaba de configurar la propiedad productsBindingSource.DataSource en categoryBindingSource y productsBindingSource.DataMember en Products. Debido a este enlace, solo se mostrarán los productos que pertenecen a la categoría seleccionada actualmente en productDataGridView.
Habilite el botón Guardar de la barra de herramientas Navegación haciendo clic en el botón derecho del mouse y seleccionando Habilitado.
Agregue el controlador de eventos para el botón Guardar haciendo doble clic en el botón. Esto agregará el controlador de eventos y le llevará al código subyacente del formulario. El código del controlador de eventos categoryBindingNavigatorSaveItem_Click se agregará en la sección siguiente.
Adición de código que controla la interacción con los datos
Ahora agregaremos el código para usar ProductContext para realizar el acceso a datos. Actualice el código de la ventana del formulario principal, como se muestra a continuación.
El código declara una instancia de ejecución prolongada de ProductContext. El objeto ProductContext se usa 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;
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();
}
}
}
Prueba de la aplicación de Windows Forms
Compile y ejecute la aplicación y podrá probar la funcionalidad.
Después de guardar, las claves generadas por el almacén se muestran en la pantalla.
Si usó Code First, también verá que se crea una base de datos WinFormswithEFSample.ProductContext.