WPF 入门Getting Started with WPF

此分步演练演示如何将 POCO 类型绑定到“主要详细信息”窗体中的 WPF 控件。This step-by-step walkthrough shows how to bind POCO types to WPF controls in a "main-detail" form. 应用程序使用实体框架 API,用数据库中的数据填充对象、跟踪更改并将数据保存到数据库。The application uses the Entity Framework APIs to populate objects with data from the database, track changes, and persist data to the database.

该模型定义了两种参与一对多关系的类型:类别(主体\主要)和产品(依赖\详细信息)。The model defines two types that participate in one-to-many relationship: Category (principal\main) and Product (dependent\detail). WPF 数据绑定框架允许在相关对象之间导航:在主视图中选择行将导致详细信息视图使用相应的子数据进行更新。The WPF data-binding framework enables navigation between related objects: selecting rows in the master view causes the detail view to update with the corresponding child data.

本演练中的屏幕截图和代码清单取自 Visual Studio 2019 16.6.5。The screen shots and code listings in this walkthrough are taken from Visual Studio 2019 16.6.5.

提示

可在 GitHub 示例中查看此文章的示例。You can view this article's sample on GitHub.

先决条件Pre-Requisites

  • 需要将 Visual Studio 2019 16.3 或更高版本随 .NET 桌面工作负载一起安装才能完成此演练。You need to have Visual Studio 2019 16.3 or later installed with the .NET desktop workload selected to complete this walkthrough.

    有关安装最新版本的 Visual Studio 的详细信息,请参阅安装 Visual StudioFor more information about installing the latest version of Visual Studio, see Install Visual Studio.

创建应用程序Create the Application

  1. 打开 Visual StudioOpen Visual Studio
  2. 在“开始”窗口上,选择“创建新项目”。On the start window, choose Create new project.
  3. 搜索“WPF”,选择“WPF 应用(.NET Core)”,然后选择“下一步”。Search for "WPF," choose WPF App (.NET Core) and then choose Next.
  4. 在下一个屏幕中,为项目命名(例如“GetStartedWPF”),然后选择“创建”。At the next screen, give the project a name, for example, GetStartedWPF, and choose Create.

安装实体框架 NuGet 包Install the Entity Framework NuGet packages

  1. 右键单击解决方案,然后选择“管理解决方案的 NuGet 包…”Right-click on the solution and choose Manage NuGet Packages for Solution...

    管理 NuGet 包

  2. 在搜索框中键入“entityframeworkcore.sqlite”。Type entityframeworkcore.sqlite in the search box.

  3. 选择“Microsoft.EntityFrameworkCore.Sqlite”包。Select the Microsoft.EntityFrameworkCore.Sqlite package.

  4. 在右窗格中选中项目,然后单击“安装”Check the project in the right pane and click Install

    Sqlite 包

  5. 重复上述步骤,搜索 entityframeworkcore.proxies 并安装“Microsoft.EntityFrameworkCore.Proxies”。Repeat the steps to search for entityframeworkcore.proxies and install Microsoft.EntityFrameworkCore.Proxies.

备注

安装 Sqlite 包后,将自动下拉相关的 Microsoft.EntityFrameworkCore 基础包。When you installed the Sqlite package, it automatically pulled down the related Microsoft.EntityFrameworkCore base package. Microsoft.EntityFrameworkCore.Proxies 包提供对“延迟加载”数据的支持。The Microsoft.EntityFrameworkCore.Proxies package provides support for "lazy-loading" data. 这意味着,如果具有包含子实体的实体,则在初始加载时仅提取父项。This means when you have entities with child entities, only the parents are fetched on the initial load. 代理检测何时尝试访问子实体,并根据需要自动加载它们。The proxies detect when an attempt to access the child entities is made and automatically loads them on demand.

定义模型Define a Model

在本演练中,你将使用“Code First”实现模型。In this walkthrough you will implement a model using "code first." 这意味着,EF Core 将基于你定义的 C# 类创建数据库表和架构。This means that EF Core will create the database tables and schema based on the C# classes you define.

添加新类。Add a new class. 为其命名:Product.cs 并按如下所示填充:Give it the name: Product.cs and populate it like this:

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 的类,并使用以下代码进行填充:Next, add a class named Category.cs and populate it with the following code:

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

“类别”类上的“产品”属性和“产品”类上的“类别”属性是导航属性。The Products property on the Category class and Category property on the Product class are navigation properties. 在实体框架中,导航属性提供了一种在两个实体类型之间导航关系的方法。In Entity Framework, navigation properties provide a way to navigate a relationship between two entity types.

除了定义实体外,还需要定义派生自 DbContext 并公开 DbSet<TEntity> 属性的类。In addition to defining entities, you need to define a class that derives from DbContext and exposes DbSet<TEntity> properties. DbSet<TEntity> 属性让上下文知道要包括在模型中的类型。The DbSet<TEntity> properties let the context know which types you want to include in the model.

DbContext 派生类型的实例在运行时管理实体对象,其中包括使用数据库中的数据填充对象、更改跟踪以及将数据保存到数据库。An instance of the DbContext derived type manages the entity objects during run time, which includes populating objects with data from a database, change tracking, and persisting data to the database.

使用以下定义向项目添加新的 ProductContext.cs 类:Add a new ProductContext.cs class to the project with the following definition:

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();
            base.OnConfiguring(optionsBuilder);
        }
    }
}
  • DbSet 通知 EF Core 哪些 C# 实体应映射到数据库。The DbSet informs EF Core what C# entities should be mapped to the database.
  • 可通过多种方式配置 EF Core DbContextThere are a variety of ways to configure the EF Core DbContext. 你可以从以下位置了解相关信息:配置 DbContextYou can read about them in: Configuring a DbContext.
  • 此示例使用 OnConfiguring 重写来指定 Sqlite 数据文件。This example uses the OnConfiguring override to specify a Sqlite data file.
  • UseLazyLoadingProxies 调用指示 EF Core 实现延迟加载,因此从父实体访问时,会自动加载子实体。The UseLazyLoadingProxies call tells EF Core to implement lazy-loading, so child entities are automatically loaded when accessed from the parent.

按 CTRL+SHIFT+B 或导航到“生成”>“生成解决方案”来编译项目。Press CTRL+SHIFT+B or navigate to Build > Build Solution to compile the project.

提示

了解使数据库和 EF Core 模型保持同步的不同方式:管理数据库架构Learn about the different was to keep your database and EF Core models in sync: Managing Database Schemas.

延迟加载Lazy Loading

“类别”类上的“产品”属性和“产品”类上的“类别”属性是导航属性。The Products property on the Category class and Category property on the Product class are navigation properties. 在 Entity Framework Core 中,导航属性提供了一种在两个实体类型之间导航关系的方法。In Entity Framework Core, navigation properties provide a way to navigate a relationship between two entity types.

EF Core 提供了在首次访问导航属性时自动从数据库加载相关实体的选项。EF Core gives you an option of loading related entities from the database automatically the first time you access the navigation property. 如果使用此类型的加载(称为“延迟加载”),请注意,首次访问每个导航属性时,将对数据库执行单独的查询(如果内容不在上下文中)。With this type of loading (called lazy loading), be aware that the first time you access each navigation property a separate query will be executed against the database if the contents are not already in the context.

使用“普通旧 C# 对象”(POCO) 实体类型时,EF Core 通过在运行时创建派生代理类型的实例,然后重写类中的虚拟属性以添加加载挂钩,来实现延迟加载。When using "Plain Old C# Object" (POCO) entity types, EF Core achieves lazy loading by creating instances of derived proxy types during runtime and then overriding virtual properties in your classes to add the loading hook. 若要获取相关对象的延迟加载,必须将导航属性 getter 声明为“公共”和“虚拟”(在 Visual Basic 中为 Overridable),并且不得密封类(在 Visual Basic 中为 NotOverridable)。To get lazy loading of related objects, you must declare navigation property getters as public and virtual (Overridable in Visual Basic), and your class must not be sealed (NotOverridable in Visual Basic). 使用 Database First 时,会自动将导航属性设为虚拟,以启用延迟加载。When using Database First, navigation properties are automatically made virtual to enable lazy loading.

将对象绑定到控件Bind Object to Controls

将在模型中定义的类作为此 WPF 应用程序的数据源添加。Add the classes that are defined in the model as data sources for this WPF application.

  1. 在“解决方案资源管理器”中双击 MainWindow.xaml 以打开主窗体Double-click MainWindow.xaml in Solution Explorer to open the main form

  2. 选择“XAML”选项卡以编辑 XAML。Choose the XAML tab to edit the XAML.

  3. 紧接开始 Window 标记之后添加以下源,以连接到 EF Core 实体。Immediately after the opening Window tag, add the following sources to connect to the EF Core entities.

    <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>
    
  4. 这样会为“父”类别设置源,并为“详细信息”产品设置第二个源。This sets up source for the "parent" categories, and second source for the "detail" products.

  5. 接下来,紧接结束 Window.Resources 标记之后,将以下标记添加到 XAML。Next, add the following markup to your XAML after the closing Window.Resources tag.

    <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>
    
  6. 请注意,CategoryId 设置为 ReadOnly,因为它是由数据库分配的,无法更改。Note that the CategoryId is set to ReadOnly because it is assigned by the database and cannot be changed.

添加“详细信息”网格Adding a Details Grid

现在,网格可以显示类别,可以添加“详细信息”网格来显示产品。Now that the grid exists to display categories, the details grid can be added to show products.

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>

最后,将 click 事件中的 Save 按钮和电线添加到 Button_ClickFinally, add a Save button and wire in the click event to Button_Click.

<Button Content="Save" HorizontalAlignment="Center" Margin="0,240,0,0" 
        Click="Button_Click" Height="20" Width="123"/>

设计视图应如下所示:Your design view should look like this:

WPF 设计器的屏幕截图

添加处理数据交互的代码Add Code that Handles Data Interaction

现在可以向主窗口添加一些事件处理程序了。It's time to add some event handlers to the main window.

  1. 在“XAML”窗口中,单击“<窗口>”元素,以选择主窗口。In the XAML window, click on the <Window> element, to select the main window.

  2. 在“属性”窗口中,选择右上角的“事件”,然后双击“已加载”标签右侧的文本框。In the Properties window choose Events at the top right, then double-click the text box to right of the Loaded label.

    主窗口属性

这会使你转到窗体隐藏的代码,现在我们将编辑代码以使用 ProductContext 执行数据访问。This brings you to the code behind for the form, we'll now edit the code to use the ProductContext to perform data access. 更新代码,如下所示。Update the code as shown below.

代码声明 ProductContext 的长时间运行的实例。The code declares a long-running instance of ProductContext. ProductContext 对象用于查询数据并将数据保存到数据库。The ProductContext object is used to query and save data to the database. 然后,从重写的 OnClosing 方法中调用 ProductContext 实例上的 Dispose() 方法。The Dispose() method on the ProductContext instance is then called from the overridden OnClosing method.代码注释说明了每个步骤的作用。 The code comments explain what each step does.

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() 的调用在首次运行时生成数据库。The code uses a call to EnsureCreated() to build the database on the first run. 这对于演示是可接受的,但在生产应用中,应查看迁移来管理你的架构。This is acceptable for demos, but in production apps you should look at migrations to manage your schema. 代码还会同步执行,因为它使用本地 SQLite 数据库。The code also executes synchronously because it uses a local SQLite database. 对于通常涉及远程服务器的生产方案,请考虑使用 LoadSaveChanges 方法的异步版本。For production scenarios that typically involve a remote server, consider using the asynchronous versions of the Load and SaveChanges methods.

测试 WPF 应用程序Test the WPF Application

按 F5 或选择“调试”>“开始调试”以编译和运行应用程序。Compile and run the application by pressing F5 or choosing Debug > Start Debugging. 数据库应会自动使用名为 products.db 的文件创建。The database should be automatically created with a file named products.db. 输入类别名称并按 Enter,然后将产品添加到下部网格。Enter a category name and hit enter, then add products to the lower grid. 单击“保存”,并使用数据库提供的 ID 查看网格刷新。Click save and watch the grid refresh with the database provided ids. 突出显示某行,然后按 Delete 以删除该行。Highlight a row and hit Delete to remove the row. 单击“保存”后,将删除实体。The entity will be deleted when you click Save.

正在运行的应用程序

属性更改通知Property Change Notification

此示例依赖于四个步骤来将实体与 UI 同步。This example relies on four steps to synchronize the entities with the UI.

  1. 初始调用 _context.Categories.Load() 加载类别数据。The initial call _context.Categories.Load() loads the categories data.
  2. 延迟加载代理加载相关的产品数据。The lazy-loading proxies load the dependent products data.
  3. EF Core 的内置更改跟踪在调用 _context.SaveChanges() 时对实体进行必要的修改(包括插入和删除)。EF Core's built-in change tracking makes the necessary modifications to entities, including insertions and deletions, when _context.SaveChanges() is called.
  4. DataGridView.Items.Refresh() 的调用将使用新生成的 ID 强制重载。The calls to DataGridView.Items.Refresh() force a reload with the newly generated ids.

这适用于我们的入门示例,但对于其他方案,你可能需要其他代码。This works for our getting started sample, but you may require additional code for other scenarios. WPF 控件通过读取实体上的字段和属性来呈现 UI。WPF controls render the UI by reading the fields and properties on your entities. 当你在用户界面 (UI) 中编辑某个值时,该值将传递给你的实体。When you edit a value in the user interface (UI), that value is passed to your entity. 直接在实体上更改属性值(例如从数据库中加载它)时,WPF 不会立即反映 UI 中的更改。When you change the value of a property directly on your entity, such as loading it from the database, WPF will not immediately reflect the changes in the UI. 必须向呈现引擎通知更改。The rendering engine must be notified of the changes. 项目通过手动调用 Refresh() 来完成此操作。The project did this by manually calling Refresh(). 自动发出此通知的简单方法是实现 INotifyPropertyChanged 接口。An easy way to automate this notification is by implementing the INotifyPropertyChanged interface. WPF 组件会自动检测接口并注册更改事件。WPF components will automatically detect the interface and register for change events. 实体负责引发这些事件。The entity is responsible for raising these events.

提示

若要详细了解如何处理更改,请参阅:如何实现属性更改通知To learn more about how to handle changes, read: How to implement property change notification.

后续步骤Next Steps

了解有关配置 DbContext 的详细信息。Learn more about Configuring a DbContext.