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 窗体时,实现 IListSource
的 ObservableCollectionListSource
可用于集合导航。 这不是必需的,但确实改进了双向数据绑定体验。
定义 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 数据库。
将主窗体的名称从 Form1
更改为 MainForm
。
并将标题更改为“产品和类别”。
使用“工具箱”,添加两个彼此相邻排列的 DataGridView
控件。
在第一个 DataGridView
的“属性”中,将“名称”更改为 dataGridViewCategories
。
在第二个 DataGridView
的“属性”中,将“名称”更改为 dataGridViewProducts
。
此外,使用“工具箱”添加 Button
控件。
为按钮 buttonSave
命名,并为其指定文本“保存”。 窗体应如下所示:
下一步是将 Product
和 Category
类型从模型连接到 DataGridView
控件。 这会将 EF Core 加载的数据绑定到控件,以便 EF Core 跟踪的实体与控件中显示的实体保持同步。
在第一个 DataGridView
上单击“设计器操作字形”。 这是控件右上角的小按钮。
此时会打开“操作列表”,可从中访问“所选数据源”下拉列表。 我们尚未创建数据源,因此请转到底部并选择“添加新的对象数据源...”。
选择“类别”以创建类别的对象数据源,然后单击“确定”。
如果此处未显示任何数据源类型,请确保已将 Product.cs
、Category.cs
和 ProductsContext.cs
添加到项目中,并且已生成解决方案。
现在,“选择数据源”下拉列表包含我们刚刚创建的对象数据源。 展开“其他数据源”,然后展开“项目数据源”,选择“类别”。
第二个 DataGridView
将绑定到产品。 但是,它不会绑定到顶级 Product
类型,而是从第一个 DataGridView
的 Category
绑定绑定到 Products
导航。 这意味着,在第一个视图中选择某个类别时,该类别产品将自动在第二个视图中使用。
使用第二个 DataGridView
上的“设计器操作字形”,选择“选择数据源”,然后展开 categoryBindingSource
并选择 Products
。
配置显示的内容
默认情况下,在 DataGridView
中为绑定类型的每个属性创建一个列。 此外,用户可以编辑其中每个属性的值。 但是,某些值(如主键值)在概念上是只读的,因此不应进行编辑。 此外,某些属性(如 CategoryId
外键属性和 Category
导航)对用户没有用,因此应隐藏。
在实际应用程序中隐藏主键属性很常见。 它们在此处可见,以便轻松查看 EF Core 在后台执行的操作。
右键单击第一个 DataGridView
列,然后选择“编辑列...”。
将表示主键的 CategoryId
列设置为“只读”,然后单击“确定”。
右键单击第二个 DataGridView
列,然后选择“编辑列...”。将 ProductId
列设置为“只读”,删除 CategoryId
和 Category
列,然后单击“确定”。
连接到 EF Core
应用程序现在需要少量代码来将 EF Core 连接到数据绑定控件。
右键单击文件并选择“查看代码”,以打开 MainForm
代码。
添加一个专用字段来保存会话的 DbContext
,并为 OnLoad
和 OnClosing
方法添加替代方法。 代码应如下所示:
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
跟踪的类别。 这是通过在 Categories
DbSet
属性上调用 Local.ToBindingList()
来完成的。 Local
提供对跟踪类别的本地视图的访问权限,其中挂钩了事件,以确保本地数据与显示的数据保持同步,反之亦然。 ToBindingList()
将此数据公开为 Windows 窗体数据绑定所理解的 IBindingList
。
关闭窗体时调用 OnClosing
方法。 此时,将释放 DbContext
,这可确保释放任何数据库资源,并将 dbContext
字段设置为 null,使其不能再次使用。
填充“产品”视图
如果此时启用应用程序,则应如下所示:
请注意,已从数据库加载类别,但产品表仍为空。 此外,“保存”按钮不起作用。
若要填充产品表,EF Core 需要从数据库中加载所选类别的产品。 若要实现此目的:
在主窗体设计器中,为类别选择 DataGridView
。
在 DataGridView
的“属性”中,选择事件(闪电按钮),然后双击“SelectionChanged”事件。
这将在主窗体代码中创建存根,以便在类别选择发生更改时触发事件。
填充事件代码:
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 将仅访问数据库以加载产品(如果尚未加载)。
如果应用程序现在再次运行,那么只要选择了类别,它就应该加载相应的产品:
最后,“保存”按钮可以连接到 EF Core,以便对产品和类别所做的任何更改都保存到数据库。
在主窗体设计器中,选择“保存”按钮。
在 Button
的“属性”中,选择事件(闪电按钮),然后双击“Click”事件。
填充事件代码:
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 键将其删除。
请注意,单击“保存”时,将填充添加的类别和产品的主键值。
配置 DbContext
创建并配置模型
管理数据库架构
使用 OOP Windows 窗体设计器进行数据绑定