Introducción a los formularios Windows Forms
En este tutorial paso a paso se muestra cómo compilar una sencilla aplicación de Windows Forms (WinForms) respaldada por una base de datos SQLite. La aplicación usa Entity Framework Core (EF Core) para cargar datos de la base de datos, realizar un seguimiento de los cambios realizados en esos datos y conservar esos cambios en la base de datos.
Las capturas de pantalla y las listas de código de este tutorial se han tomado de Visual Studio 2022 17.3.0.
Sugerencia
Puede ver un ejemplo de este artículo en GitHub.
Requisitos previos
Debe tener Visual Studio 2022 17.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
Abra Visual Studio.
En la ventana de inicio, elija Crear proyecto.
Elija Aplicación de Windows Forms y, luego, seleccione Siguiente.
En la pantalla siguiente, asigne un nombre al proyecto, por ejemplo, GetStartedWinForms, y elija Siguiente.
En la siguiente pantalla, elija la versión de .NET que se va a usar. Este tutorial se creó con .NET 7, pero también debería funcionar con versiones posteriores.
Elija Crear.
Instalación de los paquetes NuGet de EF Core
Haga clic con el botón derecho en la solución y elija Administrar paquetes NuGet para la solución...
Elija la pestaña Examinar y busque el paquete Microsoft.Data.SQLite.
Instale el paquete Microsoft.EntityFrameworkCore.Sqlite.
Compruebe el proyecto GetStartedWinForms en el panel derecho.
Elija la versión más reciente. Para usar una versión preliminar, asegúrese de que la casilla Incluir versión preliminar esté activada.
Haz clic en Instalar
Nota:
Microsoft.EntityFrameworkCore.Sqlite es el paquete "proveedor de bases de datos" para usar EF Core con una base de datos SQLite. Hay paquetes similares disponibles para otros sistemas de base de datos. La instalación de un paquete de proveedor de base de datos incorpora automáticamente todas las dependencias necesarias para usar EF Core con ese sistema de base de datos. Esto incluye el paquete base Microsoft.EntityFrameworkCore.
Definición de un modelo
En este tutorial, implementaremos un modelo con "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 defina. Consulte Administración de esquemas de base de datos para ver cómo usar una base de datos existente en su lugar.
Haga clic con el botón derecho en el proyecto y elija Agregar y, a continuación, Clase... para agregar una nueva clase.
Use el nombre de archivo
Product.cs
y reemplace el código de la clase por:using System.ComponentModel; namespace GetStartedWinForms; public class Product { public int ProductId { get; set; } public string? Name { get; set; } public int CategoryId { get; set; } public virtual Category Category { get; set; } = null!; }
Repita esta operación para crear
Category.cs
con el código siguiente:using Microsoft.EntityFrameworkCore.ChangeTracking; namespace GetStartedWinForms; public class Category { public int CategoryId { get; set; } public string? Name { get; set; } public virtual ObservableCollectionListSource<Product> Products { get; } = new(); }
La propiedad Products
de la clase Category
y la propiedad Category
de la clase Product
son propiedades llamadas ”de navegación”. En EF Core, las navegaciones definen una relación entre dos tipos de entidad. En este caso, la navegación Product.Category
hace referencia a la categoría a la que pertenece un producto determinado. Del mismo modo, la navegación de la colección Category.Products
contiene todos los productos de una categoría determinada.
Sugerencia
Cuando se usa Windows Forms, la clase ObservableCollectionListSource
, que implementa IListSource
, se puede usar para las navegaciones de colección. Esto no es necesario, pero mejora la experiencia de enlace de datos bidireccional.
Definición de DbContext
En EF Core, se usa una clase derivada de DbContext
para configurar tipos de entidad en un modelo y actuar como una sesión para interactuar con la base de datos. En el caso más sencillo, una clase DbContext
:
- Contiene las propiedades
DbSet
para cada tipo de entidad en el modelo. - Reemplaza el método
OnConfiguring
para configurar el proveedor de base de datos y la cadena de conexión que se va a usar. Consulte Configuración de dbContext para obtener más información.
En este caso, la clase DbContext también invalida el método OnModelCreating
para proporcionar algunos datos de ejemplo para la aplicación.
Agregue una nueva clase ProductsContext.cs
al proyecto con el código siguiente:
using Microsoft.EntityFrameworkCore;
namespace GetStartedWinForms;
public class ProductsContext : 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");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Category>().HasData(
new Category { CategoryId = 1, Name = "Cheese" },
new Category { CategoryId = 2, Name = "Meat" },
new Category { CategoryId = 3, Name = "Fish" },
new Category { CategoryId = 4, Name = "Bread" });
modelBuilder.Entity<Product>().HasData(
new Product { ProductId = 1, CategoryId = 1, Name = "Cheddar" },
new Product { ProductId = 2, CategoryId = 1, Name = "Brie" },
new Product { ProductId = 3, CategoryId = 1, Name = "Stilton" },
new Product { ProductId = 4, CategoryId = 1, Name = "Cheshire" },
new Product { ProductId = 5, CategoryId = 1, Name = "Swiss" },
new Product { ProductId = 6, CategoryId = 1, Name = "Gruyere" },
new Product { ProductId = 7, CategoryId = 1, Name = "Colby" },
new Product { ProductId = 8, CategoryId = 1, Name = "Mozzela" },
new Product { ProductId = 9, CategoryId = 1, Name = "Ricotta" },
new Product { ProductId = 10, CategoryId = 1, Name = "Parmesan" },
new Product { ProductId = 11, CategoryId = 2, Name = "Ham" },
new Product { ProductId = 12, CategoryId = 2, Name = "Beef" },
new Product { ProductId = 13, CategoryId = 2, Name = "Chicken" },
new Product { ProductId = 14, CategoryId = 2, Name = "Turkey" },
new Product { ProductId = 15, CategoryId = 2, Name = "Prosciutto" },
new Product { ProductId = 16, CategoryId = 2, Name = "Bacon" },
new Product { ProductId = 17, CategoryId = 2, Name = "Mutton" },
new Product { ProductId = 18, CategoryId = 2, Name = "Pastrami" },
new Product { ProductId = 19, CategoryId = 2, Name = "Hazlet" },
new Product { ProductId = 20, CategoryId = 2, Name = "Salami" },
new Product { ProductId = 21, CategoryId = 3, Name = "Salmon" },
new Product { ProductId = 22, CategoryId = 3, Name = "Tuna" },
new Product { ProductId = 23, CategoryId = 3, Name = "Mackerel" },
new Product { ProductId = 24, CategoryId = 4, Name = "Rye" },
new Product { ProductId = 25, CategoryId = 4, Name = "Wheat" },
new Product { ProductId = 26, CategoryId = 4, Name = "Brioche" },
new Product { ProductId = 27, CategoryId = 4, Name = "Naan" },
new Product { ProductId = 28, CategoryId = 4, Name = "Focaccia" },
new Product { ProductId = 29, CategoryId = 4, Name = "Malted" },
new Product { ProductId = 30, CategoryId = 4, Name = "Sourdough" },
new Product { ProductId = 31, CategoryId = 4, Name = "Corn" },
new Product { ProductId = 32, CategoryId = 4, Name = "White" },
new Product { ProductId = 33, CategoryId = 4, Name = "Soda" });
}
}
Asegúrese de compilar la solución en este momento.
Agregar controles al formulario
La aplicación mostrará una lista de categorías y una lista de productos. Cuando se selecciona una categoría en la primera lista, la segunda se cambiará para mostrar los productos de esa categoría. Estas listas se pueden modificar para agregar, quitar o editar productos y categorías, y estos cambios se pueden guardar en la base de datos de SQLite haciendo clic en el botón Guardar.
Cambie el nombre del formulario principal de
Form1
aMainForm
.Y cambie el título a "Productos y categorías".
Con el Cuadro de herramientas, agregue dos controles
DataGridView
, dispuestos uno junto a otro.En las propiedades del primer control
DataGridView
, cambie el nombre adataGridViewCategories
.En las propiedades del segundo control
DataGridView
, cambie el nombre adataGridViewProducts
.También con el Cuadro de herramientas, agregue un control
Button
.Dé un nombre al botón
buttonSave
y asígnele el texto "Guardar". El formulario debe tener un aspecto similar al siguiente:
Enlace de datos
El siguiente paso consiste en conectar los tipos Product
y Category
del modelo a los controles DataGridView
. Esto enlazará los datos cargados por EF Core a los controles, de modo que las entidades de seguimiento de EF Core se mantengan sincronizadas con las que se muestran en los controles.
Haga clic en el glifo de acción del diseñador en el primer control
DataGridView
. Este es el pequeño botón situado en la esquina superior derecha del control.Se abre la lista de acciones, desde la que se puede acceder a la lista desplegable para elegir el origen de datos. Todavía no hemos creado un origen de datos, así que vaya a la parte inferior y elija Agregar nuevo origen de datos de objeto....
Elija Categoría para crear un origen de datos de objeto para categorías y haga clic en Aceptar.
Sugerencia
Si no aparecen aquí tipos de origen de datos, asegúrese de que
Product.cs
,Category.cs
yProductsContext.cs
se hayan agregado al proyecto y se haya compilado la solución.Ahora la lista desplegable Elegir origen de datos contiene el origen de datos de objeto que acabamos de crear. Expanda Otros orígenes de datos, después Orígenes de datos del proyecto y elija Categoría.
El segundo control
DataGridView
estará enlazado a los productos. Sin embargo, en lugar de enlazar al tipoProduct
de nivel superior, en su lugar se enlazará a la navegaciónProducts
desde el enlaceCategory
del primer controlDataGridView
. Esto significa que, cuando se selecciona una categoría en la primera vista, los productos de esa categoría se usarán automáticamente en la segunda vista.Con el glifo de acción del diseñador en el segundo control
DataGridView
, elija Elegir origen de datos y, a continuación, expandacategoryBindingSource
y elijaProducts
.
Configuración de lo que se muestra
De forma predeterminada, se crea una columna en DataGridView
para cada propiedad de los tipos enlazados. Además, el usuario puede editar los valores de cada una de estas propiedades. Sin embargo, algunos valores, como los valores de clave principal, son conceptualmente de solo lectura y, por tanto, no se deben editar. Además, algunas propiedades, como la propiedad de clave externa CategoryId
y la navegación Category
no son útiles para el usuario, por lo que deben ocultarse.
Sugerencia
Es habitual ocultar las propiedades de clave principal en una aplicación real. Se dejan visibles aquí para facilitar la visualización de lo que EF Core está haciendo en segundo plano.
Haga clic con el botón derecho en el primer control
DataGridView
y elija Editar columnas....Haga que la columna
CategoryId
, que representa la clave principal, sea de solo lectura, y haga clic en Aceptar.Haga clic con el botón derecho en el segundo control
DataGridView
y elija Editar columnas.... Haga que la columnaProductId
sea de solo lectura y quite las columnasCategoryId
yCategory
y, a continuación, haga clic en Aceptar.
Conexión a EF Core
La aplicación ahora necesita una pequeña cantidad de código para conectar EF Core a los controles enlazados a datos.
Abra el código
MainForm
haciendo clic con el botón derecho en el archivo y seleccionando Ver código.Agregue un campo privado que contenga
DbContext
para la sesión y agregue invalidaciones para los métodosOnLoad
yOnClosing
. El código debe ser similar al siguiente:
using Microsoft.EntityFrameworkCore;
using System.ComponentModel;
namespace GetStartedWinForms
{
public partial class MainForm : Form
{
private ProductsContext? dbContext;
public MainForm()
{
InitializeComponent();
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
this.dbContext = new ProductsContext();
// Uncomment the line below to start fresh with a new database.
// this.dbContext.Database.EnsureDeleted();
this.dbContext.Database.EnsureCreated();
this.dbContext.Categories.Load();
this.categoryBindingSource.DataSource = dbContext.Categories.Local.ToBindingList();
}
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
this.dbContext?.Dispose();
this.dbContext = null;
}
}
}
Se llama al método OnLoad
cuando se carga el formulario. En este momento
- Se crea una instancia de
ProductsContext
que se usará para cargar y realizar un seguimiento de los cambios en los productos y categorías mostrados por la aplicación. - Se llama a
EnsureCreated
enDbContext
para crear la base de datos de SQLite si aún no existe. Se trata de una manera rápida de crear una base de datos al crear prototipos o probar aplicaciones. Sin embargo, si el modelo cambia, la base de datos deberá eliminarse para que se pueda volver a crear. (Se pueden quitar los comentarios de la líneaEnsureDeleted
para eliminar y volver a crear fácilmente la base de datos cuando se ejecuta la aplicación). En su lugar, puede usar migraciones de EF Core para modificar y actualizar el esquema de la base de datos sin perder datos. EnsureCreated
también rellenará la nueva base de datos con los datos definidos en el métodoProductsContext.OnModelCreating
.- El método de extensión
Load
se usa para cargar todas las categorías de la base de datos enDbContext
. Ahora, el controlDbContext
realizará el seguimiento de estas entidades, lo que detectará los cambios realizados cuando el usuario edite las categorías. - La propiedad
categoryBindingSource.DataSource
se inicializa en las categorías de las que realiza el seguimientoDbContext
. Para ello, llame aLocal.ToBindingList()
en la propiedadCategories
DbSet
.Local
proporciona acceso a una vista local de las categorías con seguimiento, con eventos conectados para asegurarse de que los datos locales permanecen sincronizados con los datos mostrados y viceversa.ToBindingList()
expone estos datos como un controlIBindingList
, que entiende el enlace de datos de Windows Forms.
El método OnClosing
se llama cuando se cierra el formulario. En este momento, DbContext
se elimina, lo que garantiza que se liberarán los recursos de la base de datos y el campo dbContext
se establece en null para que no se pueda volver a usar.
Rellenado de la vista Productos
Si la aplicación se inicia en este momento, debería tener un aspecto similar al siguiente:
Observe que las categorías se han cargado desde la base de datos, pero la tabla de productos permanece vacía. Además, el botón Guardar no funciona.
Para rellenar la tabla de productos, EF Core debe cargar productos de la base de datos para la categoría seleccionada. Para hacer esto:
En el diseñador del formulario principal, seleccione
DataGridView
para las categorías.En propiedades de
DataGridView
, elija los eventos (el botón del rayo) y haga doble clic en el evento SelectionChanged.Esto creará código auxiliar en el código de formulario principal para que se desencadene un evento cada vez que cambie la selección de categorías.
Rellene el código del evento:
private void dataGridViewCategories_SelectionChanged(object sender, EventArgs e)
{
if (this.dbContext != null)
{
var category = (Category)this.dataGridViewCategories.CurrentRow.DataBoundItem;
if (category != null)
{
this.dbContext.Entry(category).Collection(e => e.Products).Load();
}
}
}
En este código, si hay una sesión activa (no null) de DbContext
, se obtiene la instancia de Category
enlazada a la fila seleccionada actualmente de DataViewGrid
. (Esto puede ser null
si se selecciona la fila final de la vista, que se usa para crear nuevas categorías). Si hay una categoría seleccionada, se indica a DbContext
que cargue los productos asociados a esa categoría. Para hacer esto:
- Obtenga un
EntityEntry
para la instancia deCategory
(dbContext.Entry(category)
). - Informe a EF Core de que queremos operar en la navegación de colección
Products
de eseCategory
(.Collection(e => e.Products)
). - Y, por último, indique a EF Core que queremos cargar esa colección de productos de la base de datos (
.Load();
).
Sugerencia
Cuando se llama a Load
, EF Core solo tendrá acceso a la base de datos para cargar los productos si aún no se han cargado.
Si la aplicación se vuelve a ejecutar, debe cargar los productos adecuados siempre que se seleccione una categoría:
Guardando cambios
Por último, el botón Guardar se puede conectar a EF Core para que los cambios realizados en los productos y categorías se guarden en la base de datos.
En el diseñador del formulario principal, seleccione el botón Guardar.
En Propiedades de
Button
, elija los eventos (el botón de rayo) y haga doble clic en el evento Click.Rellene el código del evento:
private void buttonSave_Click(object sender, EventArgs e)
{
this.dbContext!.SaveChanges();
this.dataGridViewCategories.Refresh();
this.dataGridViewProducts.Refresh();
}
Este código llama a SaveChanges
en DbContext
, que guarda los cambios realizados en la base de datos de SQLite. Si no se realizaron cambios, se trata de una operación no operativa y no se realiza ninguna llamada a la base de datos. Después de guardar, se actualizan los controles DataGridView
. Esto se debe a que EF Core lee los valores de clave principal generados para los nuevos productos y categorías de la base de datos. La llamada a Refresh
actualiza la presentación con estos valores generados.
Aplicación final
Este es el código completo del formulario principal:
using Microsoft.EntityFrameworkCore;
using System.ComponentModel;
namespace GetStartedWinForms
{
public partial class MainForm : Form
{
private ProductsContext? dbContext;
public MainForm()
{
InitializeComponent();
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
this.dbContext = new ProductsContext();
// Uncomment the line below to start fresh with a new database.
// this.dbContext.Database.EnsureDeleted();
this.dbContext.Database.EnsureCreated();
this.dbContext.Categories.Load();
this.categoryBindingSource.DataSource = dbContext.Categories.Local.ToBindingList();
}
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
this.dbContext?.Dispose();
this.dbContext = null;
}
private void dataGridViewCategories_SelectionChanged(object sender, EventArgs e)
{
if (this.dbContext != null)
{
var category = (Category)this.dataGridViewCategories.CurrentRow.DataBoundItem;
if (category != null)
{
this.dbContext.Entry(category).Collection(e => e.Products).Load();
}
}
}
private void buttonSave_Click(object sender, EventArgs e)
{
this.dbContext!.SaveChanges();
this.dataGridViewCategories.Refresh();
this.dataGridViewProducts.Refresh();
}
}
}
Ahora se puede ejecutar la aplicación, y se pueden agregar, eliminar y editar productos y categorías. Tenga en cuenta que si se hace clic en el botón Guardar antes de cerrar la aplicación, los cambios realizados se almacenarán en la base de datos y se volverán a cargar cuando se vuelva a iniciar la aplicación. Si no se hace clic en Guardar, los cambios se perderán cuando se vuelva a iniciar la aplicación.
Sugerencia
Se puede agregar una nueva categoría o producto a DataViewControl
mediante la fila vacía en la parte inferior del control. Para eliminar una fila, selecciónela y presione la tecla Supr.
Antes de guardar
Después de guardar
Observe que los valores de clave principal de la categoría y los productos agregados se rellenan cuando se hace clic en Guardar.