Начало работы с WPF
В этом пошаговом руководстве показано, как привязать типы POCO к элементам управления WPF в форме "Основное — сведения". Приложение использует API-интерфейсы Entity Framework для заполнения объектов данными из базы данных, трассировки изменений и сохранения данных в базе данных.
Модель определяет два типа, которые участвуют в связи "один ко многим": Категория (субъект\главный) и Продукт (зависимый\детали). Платформа привязки данных 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, и щелкните Создать.
Установка пакета NuGet для Entity Framework
Щелкните правой кнопкой мыши решение и выберите Управление пакетами 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>();
}
}
Свойство Products класса Category и свойство Category класса Product — это свойства навигации. В 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# следует сопоставить с базой данных.DbContext
для EF Core можно настроить множеством способов. Их можно прочитать в: настройка DbContext.- Чтобы задать файл данных SQLite, в этом примере используется переопределение
OnConfiguring
. - Вызов
UseLazyLoadingProxies
указывает EF Core, что необходимо реализовать отложенную загрузку, благодаря которой дочерние сущности автоматически загружаются при доступе из родительской сущности.
Нажмите клавиши CTRL+SHIFT+B или последовательно выберите Сборка> Собрать решение для компиляции проекта.
Совет
Сведения о том, как сохранить базу данных и модели EF Core в синхронизации: управление схемами баз данных.
Отложенная загрузка
Свойство Products класса Category и свойство Category класса Product — это свойства навигации. В Entity Framework Core свойства навигации обеспечивают возможность навигации по связям между двумя типами сущностей.
EF Core обеспечивает возможность автоматической загрузки связанных сущностей из базы данных при первом доступе к свойству навигации. В случае использования такого типа загрузки (называемой отложенной) следует помнить о следующем: если содержимое еще отсутствует в контексте, при первом обращении к каждому свойству навигации будет выполняться отдельный запрос базы данных.
При использовании типов сущностей POCO решение EF Core обеспечивает отложенную загрузку, создавая во время выполнения экземпляры производных прокси-типов, а затем переопределяя виртуальные свойства в классах для добавления обработчика загрузки. Чтобы получить отложенную загрузку связанных объектов, необходимо объявить методы получения свойств навигации как общедоступные и виртуальные (переопределяемые в Visual Basic). При этом класс не должен быть запечатанным (непереопределяемым в Visual Basic). При использовании 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>
Таким образом будет задан один источник для "родительских" категорий, а второй — для продуктов "сведения".
Затем добавьте в код XAML следующую разметку после открывающего тега
Grid
.<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
, поскольку оно назначается базой данных и не может быть изменено.
Добавление таблицы сведений
Теперь, когда существует таблица для отображения категорий, можно добавить таблицу сведений для отображения продуктов. Добавьте его внутри элемента Grid
после элемента категорий DataGrid
.
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
и привяжите событие Click к Button_Click
.
<Button Content="Save" HorizontalAlignment="Center" Margin="0,240,0,0"
Click="Button_Click" Height="20" Width="123"/>
Представление конструктора должно выглядеть следующим образом:
Добавление кода, обрабатывающего взаимодействие с данными
Теперь следует добавить в главное окно несколько обработчиков событий.
В окне XAML щелкните элемент <Window>, чтобы выбрать главное окно.
В окне Свойства в правом верхнем углу выберите События, а затем дважды щелкните текстовое поле справа от метки Загружен.
В результате вы перейдете к коду программной части формы, который теперь можно изменить, чтобы использовать ProductContext
для доступа к данным. Обновите код, как показано ниже.
Код объявляет длительный экземпляр ProductContext
. Объект ProductContext
используется для запроса и сохранения данных в базе данных. Затем из переопределенного метода OnClosing
вызывается метод Dispose()
для экземпляра ProductContext
. Комментарии к коду объясняют, что делает каждый шаг.
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
. Введите имя категории и нажмите клавишу ВВОД, а затем добавьте продукты в нижнюю таблицу. Нажмите кнопку "Сохранить" и посмотрите, обновилась ли таблица с учетом указанных идентификаторов базы данных. Выделите строку и нажмите Удалить, чтобы ее удалить. Сущность будет удалена при нажатии кнопки Сохранить.
Уведомление об изменении свойства
Этот пример предполагает выполнение четырех шагов для синхронизации сущностей с пользовательским интерфейсом.
- При начальном вызове
_context.Categories.Load()
загружаются данные категорий. - Прокси отложенной загрузки загружают данные зависимых продуктов.
- Встроенная функция отслеживания изменений EF Core вносит необходимые изменения в сущности, в том числе вставки и удаления, при вызове метода
_context.SaveChanges()
. - Вызовы
DataGridView.Items.Refresh()
приводят к принудительной перезагрузке с вновь созданными идентификаторами.
Это подходит для нашего примера для начала работы, в других сценариях может потребоваться дополнительный код. Элементы управления WPF преобразовывают для просмотра пользовательский интерфейс, считывая поля и свойства сущностей. При изменении значения в пользовательском интерфейсе это значение передается в сущность. При изменении значения свойства непосредственно в сущности, например при его загрузке из базы данных, WPF не отражает эти изменения сразу же в пользовательском интерфейсе. Подсистема преобразования для просмотра должна быть уведомлена об изменениях. В данном проекте это выполняется путем вызова Refresh()
вручную. Такое уведомление можно легко автоматизировать, реализовав интерфейс INotifyPropertyChanged. Компоненты WPF будут автоматически обнаруживать интерфейс и регистрироваться для получения событий изменения. За создание таких событий отвечает сущность.
Совет
Дополнительные сведения об обработке изменений см. в статье о реализации уведомления об изменении свойств.
Дальнейшие действия
Узнайте больше о настройке DbContext.