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

  1. Abra Visual Studio.

  2. En la ventana de inicio, elija Crear proyecto.

  3. Elija Aplicación de Windows Forms y, luego, seleccione Siguiente.

    Create a new Windows Forms project

  4. En la pantalla siguiente, asigne un nombre al proyecto, por ejemplo, GetStartedWinForms, y elija Siguiente.

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

  6. Elija Crear.

Instalación de los paquetes NuGet de EF Core

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

    Manage NuGet Packages for Solution

  2. Elija la pestaña Examinar y busque el paquete Microsoft.Data.SQLite.

  3. Instale el paquete Microsoft.EntityFrameworkCore.Sqlite.

  4. Compruebe el proyecto GetStartedWinForms en el panel derecho.

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

  6. Haz clic en Instalar

    Install the Microsoft.EntityFrameworkCore.Sqlite package

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.

  1. Haga clic con el botón derecho en el proyecto y elija Agregar y, a continuación, Clase... para agregar una nueva clase.

    Add new class

  2. 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!;
    }
    
  3. 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.

  1. Cambie el nombre del formulario principal de Form1 a MainForm.

    Rename Form1 to MainForm

  2. Y cambie el título a "Productos y categorías".

    Title MainForm as

  3. Con el Cuadro de herramientas, agregue dos controles DataGridView, dispuestos uno junto a otro.

    Add DataGridView

  4. En las propiedades del primer control DataGridView, cambie el nombre a dataGridViewCategories.

  5. En las propiedades del segundo control DataGridView, cambie el nombre a dataGridViewProducts.

  6. También con el Cuadro de herramientas, agregue un control Button.

  7. Dé un nombre al botón buttonSave y asígnele el texto "Guardar". El formulario debe tener un aspecto similar al siguiente:

    Form layout

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.

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

    The Designer Action Glyph

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

    Add new Object Data Source

  3. Elija Categoría para crear un origen de datos de objeto para categorías y haga clic en Aceptar.

    Choose Category data source type

    Sugerencia

    Si no aparecen aquí tipos de origen de datos, asegúrese de que Product.cs, Category.cs y ProductsContext.cs se hayan agregado al proyecto y se haya compilado la solución.

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

    Choose Category data source

    El segundo control DataGridView estará enlazado a los productos. Sin embargo, en lugar de enlazar al tipo Product de nivel superior, en su lugar se enlazará a la navegación Products desde el enlace Category del primer control DataGridView. 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.

  5. Con el glifo de acción del diseñador en el segundo control DataGridView, elija Elegir origen de datos y, a continuación, expanda categoryBindingSource y elija Products.

    Choose Products data source

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.

  1. Haga clic con el botón derecho en el primer control DataGridView y elija Editar columnas....

    Edit DataGridView columns

  2. Haga que la columna CategoryId, que representa la clave principal, sea de solo lectura, y haga clic en Aceptar.

    Make CategoryId column read-only

  3. Haga clic con el botón derecho en el segundo control DataGridView y elija Editar columnas.... Haga que la columna ProductId sea de solo lectura y quite las columnas CategoryId y Category y, a continuación, haga clic en Aceptar.

    Make ProductId column read-only and remove CategoryId and Category columns

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.

  1. Abra el código MainForm haciendo clic con el botón derecho en el archivo y seleccionando Ver código.

    View Code

  2. Agregue un campo privado que contenga DbContext para la sesión y agregue invalidaciones para los métodos OnLoad y OnClosing. 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 en DbContext 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ínea EnsureDeleted 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étodo ProductsContext.OnModelCreating.
  • El método de extensión Load se usa para cargar todas las categorías de la base de datos en DbContext. Ahora, el control DbContext 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 seguimiento DbContext. Esto se realiza mediante la llamada a Local.ToBindingList() en la propiedad CategoriesDbSet. 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 control IBindingList, 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:

Fist run of the application

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:

  1. En el diseñador del formulario principal, seleccione DataGridView para las categorías.

  2. En propiedades de DataGridView, elija los eventos (el botón del rayo) y haga doble clic en el evento SelectionChanged.

    Add the SelectionChanged event

    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.

  3. 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 de Category (dbContext.Entry(category)).
  • Informe a EF Core de que queremos operar en la navegación de colección Products de ese Category (.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:

Products are loaded

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.

  1. En el diseñador del formulario principal, seleccione el botón Guardar.

  2. En Propiedades de Button, elija los eventos (el botón de rayo) y haga doble clic en el evento Click.

    Add the Click event for Save

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

The running application before clicking Save

Después de guardar

The running application after clicking Save

Observe que los valores de clave principal de la categoría y los productos agregados se rellenan cuando se hace clic en Guardar.

Más información