Windows 窗体入门

本分步演练演示如何生成由 SQLite 数据库支持的简单 Windows 窗体 (WinForms) 应用程序。 应用程序使用 Entity Framework Core (EF Core) 从数据库加载数据,跟踪对这些数据所做的更改,并将这些更改保存回数据库。

本演练中的屏幕截图和代码清单取自 Visual Studio 2022 17.3.0。

提示

可在 GitHub 示例中查看此文章的示例。

先决条件

需要将 Visual Studio 2022 17.3 或更高版本随 .NET 桌面工作负载一起安装才能完成此演练。 有关安装最新版本的 Visual Studio 的详细信息,请参阅安装 Visual Studio

创建应用程序

  1. 打开 Visual Studio

  2. 在“开始”窗口上,选择“创建新项目”。

  3. 选择“Windows 窗体应用”,然后选择“下一步”。

    Create a new Windows Forms project

  4. 在下一个屏幕中,为项目命名(例如“GetStartedWinForms”),然后选择“下一步”。

  5. 在下一个屏幕中,选择要使用的 .NET 版本。 本演练是使用 .NET 7 创建的,但它也适用于更高版本。

  6. 选择“创建”。

安装 EF Core NuGet 包

  1. 右键单击解决方案,然后选择“管理解决方案的 NuGet 包…”

    Manage NuGet Packages for Solution

  2. 选择“浏览”选项卡并搜索“Microsoft.EntityFrameworkCore.Sqlite”。

  3. 选择“Microsoft.EntityFrameworkCore.Sqlite”包。

  4. 在右窗格中检查项目“GetStartedWinForms”。

  5. 选择最新版本。 若要使用预发行版本,请确保选中“包括预发行版”框。

  6. 单击“安装”

    Install the Microsoft.EntityFrameworkCore.Sqlite package

注意

Microsoft.EntityFrameworkCore.Sqlite 是用于将 EF Core 与 SQLite 数据库配合使用的“数据库提供程序”包。 类似的包可用于其他数据库系统。 安装数据库提供程序包会自动引入将 EF Core 与该数据库系统配合使用所需的所有依赖项。 这包括 Microsoft.EntityFrameworkCore 基础包。

定义模型

在本演练中,我们将使用“Code First”实现模型。 这意味着,EF Core 将基于你定义的 C# 类创建数据库表和架构。 请参阅管理数据库架构,了解如何改用现有数据库。

  1. 右键单击项目,然后选择“添加”,然后选择“类...”添加新类。

    Add new class

  2. 使用文件名 Product.cs,并将类代码替换为以下内容:

    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. 重复使用以下代码创建 Category.cs

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

Category 类上的 Products 属性和 Product 类上的 Category 属性称为“导航”。 在 EF Core 中,导航定义两个实体类型之间的关系。 在这种情况下,Product.Category 导航引用给定产品所属的类别。 同样,Category.Products 集合导航包含给定类别的所有产品。

提示

使用 Windows 窗体时,实现 IListSourceObservableCollectionListSource 可用于集合导航。 这不是必需的,但确实改进了双向数据绑定体验。

定义 DbContext

在 EF Core 中,派生自 DbContext 的类用于在模型中配置实体类型,并充当与数据库交互的会话。 在最简单的情况下,DbContext 类:

  • 包含模型中每个实体类型的 DbSet 属性。
  • 重写 OnConfiguring 方法,以配置要使用的数据库提供程序和连接字符串。 有关详细信息,请参阅配置 DbContext

在这种情况下,DbContext 类还会重写 OnModelCreating 方法,以便为应用程序提供一些示例数据。

使用以下代码向项目添加新的 ProductsContext.cs 类:

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

请确保此时生成解决方案。

将控件添加到窗体

应用程序将显示类别列表和产品列表。 在第一个列表中选择类别时,第二个列表将更改为显示该类别的产品。 可以修改这些列表以添加、删除或编辑产品和类别,并且可以通过单击“保存”按钮将这些更改保存到 SQLite 数据库。

  1. 将主窗体的名称从 Form1 更改为 MainForm

    Rename Form1 to MainForm

  2. 并将标题更改为“产品和类别”。

    Title MainForm as

  3. 使用“工具箱”,添加两个彼此相邻排列的 DataGridView 控件。

    Add DataGridView

  4. 在第一个 DataGridView 的“属性”中,将“名称”更改为 dataGridViewCategories

  5. 在第二个 DataGridView 的“属性”中,将“名称”更改为 dataGridViewProducts

  6. 此外,使用“工具箱”添加 Button 控件。

  7. 为按钮 buttonSave 命名,并为其指定文本“保存”。 窗体应如下所示:

    Form layout

数据绑定

下一步是将 ProductCategory 类型从模型连接到 DataGridView 控件。 这会将 EF Core 加载的数据绑定到控件,以便 EF Core 跟踪的实体与控件中显示的实体保持同步。

  1. 在第一个 DataGridView 上单击“设计器操作字形”。 这是控件右上角的小按钮。

    The Designer Action Glyph

  2. 此时会打开“操作列表”,可从中访问“所选数据源”下拉列表。 我们尚未创建数据源,因此请转到底部并选择“添加新的对象数据源...”。

    Add new Object Data Source

  3. 选择“类别”以创建类别的对象数据源,然后单击“确定”。

    Choose Category data source type

    提示

    如果此处未显示任何数据源类型,请确保已将 Product.csCategory.csProductsContext.cs 添加到项目中,并且已生成解决方案

  4. 现在,“选择数据源”下拉列表包含我们刚刚创建的对象数据源。 展开“其他数据源”,然后展开“项目数据源”,选择“类别”。

    Choose Category data source

    第二个 DataGridView 将绑定到产品。 但是,它不会绑定到顶级 Product 类型,而是从第一个 DataGridViewCategory 绑定绑定到 Products 导航。 这意味着,在第一个视图中选择某个类别时,该类别产品将自动在第二个视图中使用。

  5. 使用第二个 DataGridView 上的“设计器操作字形”,选择“选择数据源”,然后展开 categoryBindingSource 并选择 Products

    Choose Products data source

配置显示的内容

默认情况下,在 DataGridView 中为绑定类型的每个属性创建一个列。 此外,用户可以编辑其中每个属性的值。 但是,某些值(如主键值)在概念上是只读的,因此不应进行编辑。 此外,某些属性(如 CategoryId 外键属性和 Category 导航)对用户没有用,因此应隐藏。

提示

在实际应用程序中隐藏主键属性很常见。 它们在此处可见,以便轻松查看 EF Core 在后台执行的操作。

  1. 右键单击第一个 DataGridView 列,然后选择“编辑列...”。

    Edit DataGridView columns

  2. 将表示主键的 CategoryId 列设置为“只读”,然后单击“确定”。

    Make CategoryId column read-only

  3. 右键单击第二个 DataGridView 列,然后选择“编辑列...”。将 ProductId 列设置为“只读”,删除 CategoryIdCategory 列,然后单击“确定”。

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

连接到 EF Core

应用程序现在需要少量代码来将 EF Core 连接到数据绑定控件。

  1. 右键单击文件并选择“查看代码”,以打开 MainForm 代码。

    View Code

  2. 添加一个专用字段来保存会话的 DbContext,并为 OnLoadOnClosing 方法添加替代方法。 代码应如下所示:

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

加载窗体时调用 OnLoad 方法。 此时

  • 将创建一个 ProductsContext 实例,用于加载和跟踪应用程序显示的产品和类别更改。
  • DbContext 上调用 EnsureCreated 以创建 SQLite 数据库(如果尚不存在)。 这是在原型制作或测试应用程序时创建数据库的一种快速方法。 但是,如果模型发生更改,则需要删除数据库,以便可以再次创建它。 (运行应用程序时,可以取消注释 EnsureDeleted 行以轻松删除和重新创建数据库。)你可能希望使用 EF Core 迁移来修改和更新数据库架构,而不会丢失任何数据。
  • EnsureCreated 还将使用 ProductsContext.OnModelCreating 方法中定义的数据填充新数据库。
  • Load 扩展方法用于将所有类别从数据库加载到 DbContext 中。 目前由 DbContext 跟踪这些实体,它将检测用户编辑类别时所做的任何更改。
  • categoryBindingSource.DataSource 属性初始化为由 DbContext 跟踪的类别。 这是通过在 CategoriesDbSet 属性上调用 Local.ToBindingList() 来完成的。 Local 提供对跟踪类别的本地视图的访问权限,其中挂钩了事件,以确保本地数据与显示的数据保持同步,反之亦然。 ToBindingList() 将此数据公开为 Windows 窗体数据绑定所理解的 IBindingList

关闭窗体时调用 OnClosing 方法。 此时,将释放 DbContext,这可确保释放任何数据库资源,并将 dbContext 字段设置为 null,使其不能再次使用。

填充“产品”视图

如果此时启用应用程序,则应如下所示:

Fist run of the application

请注意,已从数据库加载类别,但产品表仍为空。 此外,“保存”按钮不起作用。

若要填充产品表,EF Core 需要从数据库中加载所选类别的产品。 若要实现此目的:

  1. 在主窗体设计器中,为类别选择 DataGridView

  2. DataGridView 的“属性”中,选择事件(闪电按钮),然后双击“SelectionChanged”事件。

    Add the SelectionChanged event

    这将在主窗体代码中创建存根,以便在类别选择发生更改时触发事件。

  3. 填充事件代码:

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

在此代码中,如果有一个活动(非 null)DbContext 会话,则我们将获得绑定到 DataViewGrid 的当前选定行的 Category 实例。 (如果选择了视图中的最后一行,则该值可能为 null,用于创建新类别。)如果存在所选类别,则会指示 DbContext 加载与该类别关联的产品。 这通过以下方式实现:

  • 获取 Category 实例的 (dbContext.Entry(category)) 的 EntityEntry
  • 让 EF Core 知道我们想要对该 Category (.Collection(e => e.Products)) 的 Products 集合导航进行操作
  • 最后,告知 EF Core 我们想要从数据库 (.Load();) 加载该产品集合

提示

调用 Load 时,EF Core 将仅访问数据库以加载产品(如果尚未加载)。

如果应用程序现在再次运行,那么只要选择了类别,它就应该加载相应的产品:

Products are loaded

保存更改

最后,“保存”按钮可以连接到 EF Core,以便对产品和类别所做的任何更改都保存到数据库。

  1. 在主窗体设计器中,选择“保存”按钮。

  2. Button 的“属性”中,选择事件(闪电按钮),然后双击“Click”事件。

    Add the Click event for Save

  3. 填充事件代码:

private void buttonSave_Click(object sender, EventArgs e)
{
    this.dbContext!.SaveChanges();

    this.dataGridViewCategories.Refresh();
    this.dataGridViewProducts.Refresh();
}

此代码对 DbContext 调用 SaveChanges,这将保存对 SQLite 数据库所做的任何更改。 如果未进行任何更改,则这是无操作,并且不会进行数据库调用。 保存后,将刷新 DataGridView 控件。 这是因为 EF Core 从数据库中读取为任何新产品和类别生成的主键值。 调用 Refresh 将使用这些生成的值更新显示。

最终应用程序

下面是主窗体的完整代码:

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

现在可以运行应用程序,并且可以添加、删除和编辑产品和类别。 请注意,如果在关闭应用程序之前单击了“保存”按钮,则所做的任何更改都将存储在数据库中,并在应用程序重新启动时重新加载。 如果未单击“保存”,则重新启动应用程序时,任何更改都将丢失。

提示

可以使用控件底部的空行将新类别或产品添加到 DataViewControl。 可以选择行并按 Del 键将其删除。

保存之前

The running application before clicking Save

保存后

The running application after clicking Save

请注意,单击“保存”时,将填充添加的类别和产品的主键值。

了解更多