本分步演练演示如何将 POCO 类型绑定到“主详细信息”窗体中的 WPF 控件。 应用程序使用 Entity Framework API 使用数据库中的数据填充对象、跟踪更改并将数据保存到数据库。
该模型定义了两种参与一对多关系的类型: Category (principal\main) 和 Product (dependent\detail)。 WPF 数据绑定框架支持在相关对象之间导航:在主视图中选择行会导致详细信息视图使用相应的子数据进行更新。
本演练中的屏幕截图和代码列表取自 Visual Studio 2019 16.6.5。
小窍门
可以在 GitHub 上查看本文的示例。
先决条件
需要安装 Visual Studio 2019 16.3 或更高版本,并选择 .NET 桌面工作负载 才能完成本演练。 有关安装最新版本的 Visual Studio 的详细信息,请参阅 “安装 Visual Studio”。
创建应用程序
- 打开 Visual Studio
- 在“开始”窗口中,选择“ 创建新项目”。
- 搜索“WPF”,选择 WPF 应用(.NET Core), 然后选择 “下一步”。
- 在下一个屏幕中,为项目命名,例如 GetStartedWPF,然后选择 “创建”。
安装 Entity Framework NuGet 包
右键单击解决方案,然后选择“ 管理解决方案的 NuGet 包...”
在搜索框中键入
entityframeworkcore.sqlite
。选择 Microsoft.EntityFrameworkCore.Sqlite 包。
在右窗格中检查项目,然后单击“安装”
重复这些步骤以搜索
entityframeworkcore.proxies
并安装 Microsoft.EntityFrameworkCore.Proxies。
注释
安装 Sqlite 包时,它会自动下载相关的 Microsoft.EntityFrameworkCore 基础包。 Microsoft.EntityFrameworkCore.Proxies 包支持“延迟加载”数据。 这意味着,当您拥有包含子实体的实体时,初次加载只会提取父实体。 代理检测何时尝试访问子实体,并按需自动加载它们。
定义模型
在本演练中,你将使用“代码优先”实现模型。这意味着 EF Core 将基于定义的 C# 类创建数据库表和架构。
添加新类。 为它指定名称: Product.cs
并填充它,如下所示:
Product.cs
namespace GetStartedWPF
{
public class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
}
}
接下来,添加一 Category.cs
个名为的类,并使用以下代码填充该类:
Category.cs
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace GetStartedWPF
{
public class Category
{
public int CategoryId { get; set; }
public string Name { get; set; }
public virtual ICollection<Product>
Products
{ get; private set; } =
new ObservableCollection<Product>();
}
}
Product 类上的 Products 属性和 Product 类上的 Category 属性是导航属性。 在 Entity Framework 中,导航属性提供了一种导航两种实体类型之间的关系的方法。
除了定义实体,还需要定义派生自 DbContext 并公开 DbSet<TEntity> 属性的类。 DbSet<TEntity> 属性可让上下文知道要在模型中包括的类型。
DbContext 派生类型的实例在运行时管理实体对象,其中包括使用数据库中的数据填充对象、更改跟踪并将数据保存到数据库。
ProductContext.cs
使用以下定义向项目添加新类:
ProductContext.cs
using Microsoft.EntityFrameworkCore;
namespace GetStartedWPF
{
public class ProductContext : 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");
optionsBuilder.UseLazyLoadingProxies();
}
}
}
-
DbSet
告知 EF Core 应该将哪些 C# 实体映射到数据库。 - 可通过多种方式配置 EF Core
DbContext
。 可以在以下内容中阅读有关它们的信息: 配置 DbContext。 - 此示例使用
OnConfiguring
重写来指定 Sqlite 数据文件。 - 该
UseLazyLoadingProxies
调用指示 EF Core 实现延迟加载,这样就可以在从父级访问时自动加载子实体。
按 Ctrl+Shift+B 或导航到 “生成 > 生成解决方案 ”以编译项目。
小窍门
了解不同的方法来确保数据库和 EF Core 模型保持同步:管理数据库架构。
延迟加载
Product 类上的 Products 属性和 Product 类上的 Category 属性是导航属性。 在 Entity Framework Core 中,导航属性提供了一种导航两种实体类型之间的关系的方法。
EF Core 提供在首次访问导航属性时自动从数据库加载相关实体的选项。 使用此类型的加载(称为延迟加载),请注意,首次访问每个导航属性时,如果上下文中尚不存在内容,则会对数据库执行单独的查询。
使用“普通旧 C# 对象”(POCO)实体类型时,EF Core 通过在运行时创建派生代理类型的实例,并重写类中的虚拟属性以插入加载挂钩,从而实现延迟加载。 若要获取相关对象的延迟加载,必须将导航属性的 getter 声明为public 和virtual(在 Visual Basic 中为Overridable),并且类不应为sealed(在 Visual Basic 中为NotOverridable)。 使用 Database First 时,导航属性会自动设置为虚拟,以启用延迟加载。
将对象绑定到控件
添加模型中定义的类作为此 WPF 应用程序的数据源。
在解决方案资源管理器中双击 MainWindow.xaml 以打开主窗体
选择 XAML 选项卡以编辑 XAML。
在开始标记
Window
之后立即添加以下源,以连接到 EF Core 实体。<Window x:Class="GetStartedWPF.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:GetStartedWPF" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800" Loaded="Window_Loaded"> <Window.Resources> <CollectionViewSource x:Key="categoryViewSource"/> <CollectionViewSource x:Key="categoryProductsViewSource" Source="{Binding Products, Source={StaticResource categoryViewSource}}"/> </Window.Resources>
这为“主”类别配置源,为“细节”产品配置第二个源。
接下来,在打开
Grid
标记后将以下标记添加到 XAML。<DataGrid x:Name="categoryDataGrid" AutoGenerateColumns="False" EnableRowVirtualization="True" ItemsSource="{Binding Source={StaticResource categoryViewSource}}" Margin="13,13,43,229" RowDetailsVisibilityMode="VisibleWhenSelected"> <DataGrid.Columns> <DataGridTextColumn Binding="{Binding CategoryId}" Header="Category Id" Width="SizeToHeader" IsReadOnly="True"/> <DataGridTextColumn Binding="{Binding Name}" Header="Name" Width="*"/> </DataGrid.Columns> </DataGrid>
请注意,
CategoryId
是由数据库分配的,因此设置为ReadOnly
,无法更改。
添加详细信息网格
现在网格已存在以显示类别,可以添加详细信息网格以显示产品。 在类别DataGrid
元素之后的Grid
元素中添加此值。
MainWindow.xaml
<DataGrid x:Name="productsDataGrid" AutoGenerateColumns="False"
EnableRowVirtualization="True"
ItemsSource="{Binding Source={StaticResource categoryProductsViewSource}}"
Margin="13,205,43,108" RowDetailsVisibilityMode="VisibleWhenSelected"
RenderTransformOrigin="0.488,0.251">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding CategoryId}"
Header="Category Id" Width="SizeToHeader"
IsReadOnly="True"/>
<DataGridTextColumn Binding="{Binding ProductId}" Header="Product Id"
Width="SizeToHeader" IsReadOnly="True"/>
<DataGridTextColumn Binding="{Binding Name}" Header="Name" Width="*"/>
</DataGrid.Columns>
</DataGrid>
最后,添加一个Save
按钮,并将单击事件连接到Button_Click
。
<Button Content="Save" HorizontalAlignment="Center" Margin="0,240,0,0"
Click="Button_Click" Height="20" Width="123"/>
设计视图应如下所示:
添加处理数据交互的代码
是时候向主窗口添加一些事件处理程序了。
在 XAML 窗口中,单击 <Window> 元素以选择主窗口。
在属性窗口中,选择右上角的事件,然后双击Loaded标签右侧的文本框。
这会将您引导至表单的后端代码,我们现在将编辑代码以使用 ProductContext
来执行数据访问操作。 更新代码,如下所示。
该代码声明了一个长期运行的实例 ProductContext
。 该 ProductContext
对象用于查询数据并将其保存到数据库。 然后,从重写的OnClosing
方法中调用实例上的Dispose()
方法。 代码注释说明了每个步骤的作用。
MainWindow.xaml.cs
using Microsoft.EntityFrameworkCore;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
namespace GetStartedWPF
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly ProductContext _context =
new ProductContext();
private CollectionViewSource categoryViewSource;
public MainWindow()
{
InitializeComponent();
categoryViewSource =
(CollectionViewSource)FindResource(nameof(categoryViewSource));
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// this is for demo purposes only, to make it easier
// to get up and running
_context.Database.EnsureCreated();
// load the entities into EF Core
_context.Categories.Load();
// bind to the source
categoryViewSource.Source =
_context.Categories.Local.ToObservableCollection();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
// all changes are automatically tracked, including
// deletes!
_context.SaveChanges();
// this forces the grid to refresh to latest values
categoryDataGrid.Items.Refresh();
productsDataGrid.Items.Refresh();
}
protected override void OnClosing(CancelEventArgs e)
{
// clean up database connections
_context.Dispose();
base.OnClosing(e);
}
}
}
注释
代码通过调用 EnsureCreated()
在首次运行时生成数据库。 在演示中这样做是可以接受的,但在生产环境中的应用程序中,应使用 迁移 来管理架构。 该代码还会同步执行,因为它使用本地 SQLite 数据库。 对于通常涉及远程服务器的生产方案,请考虑使用异步版本的 Load
和 SaveChanges
方法。
测试 WPF 应用程序
通过按 F5 或选择 “调试 > 开始调试”来编译并运行应用程序。 应使用名为 . products.db
. 的文件自动创建数据库。 输入类别名称并按 Enter,然后将产品添加到下网格。 单击“保存”,并使用数据库提供的 ID 监视网格刷新。 突出显示行并点击 “删除” 以删除该行。 单击“ 保存”时,将删除该实体。
属性更改通知
此示例依赖于四个步骤将实体与 UI 同步。
- 初始调用
_context.Categories.Load()
加载类别数据。 - 惰性加载代理负责加载依赖产品数据。
- EF Core 的内置更改跟踪在调用
_context.SaveChanges()
时对实体进行必要的修改,包括插入和删除。 - 调用
DataGridView.Items.Refresh()
强制使用新生成的 ID 重新加载。
这适用于我们的入门示例,但其他场景可能需要额外的代码。 WPF 控件通过读取实体上的字段和属性来呈现 UI。 在用户界面(UI)中编辑值时,该值将传递给实体。 直接在实体上更改属性的值(如从数据库加载它)时,WPF 不会立即反映 UI 中的更改。 必须通知呈现引擎更改。 该项目通过手动调用 Refresh()
来执行此作。 自动化此通知的一种简单方法是实现 INotifyPropertyChanged 接口。 WPF 组件将自动检测接口并注册更改事件。 该实体负责引发这些事件。
小窍门
若要详细了解如何处理更改,请阅读: 如何实现属性更改通知。
后续步骤
详细了解 如何配置 DbContext。