Начало работы с WPF

В этом пошаговом руководстве показано, как привязать типы POCO к элементам управления WPF в форме "Основное — сведения". Приложение использует API-интерфейсы Entity Framework для заполнения объектов данными из базы данных, трассировки изменений и сохранения данных в базе данных.

Модель определяет два типа, которые участвуют в связи "один ко многим": Категория (субъект\главный) и Продукт (зависимый\детали). Платформа привязки данных WPF обеспечивает навигацию между связанными объектами: выбор строк в главном представлении приводит к обновлению представления сведений с соответствующими дочерними данными.

Снимки экрана и листинги кода в этом пошаговом руководстве взяты из Visual Studio 2019 16.6.5.

Совет

Вы можете скачать используемый в этой статье пример из репозитория GitHub.

Предварительные требования

Для выполнения этого пошагового руководства необходимо установить Visual Studio 2019 16.3 или более поздней версии и выбрать при этом рабочую нагрузку для классических приложений .NET. Дополнительные сведения об установке новейшей версии Visual Studio см. в статье Установка Visual Studio.

Создание приложения

  1. Запустите Visual Studio
  2. На начальном экране выберите Создать проект.
  3. Выполните поиск по запросу "WPF", выберите в результатах Приложение WPF (.NET Core) и нажмите кнопку Далее.
  4. На следующем экране присвойте проекту имя, например GetStartedWPF, и щелкните Создать.

Установка пакета NuGet для Entity Framework

  1. Щелкните правой кнопкой мыши решение и выберите Управление пакетами NuGet для решения...

    Manage NuGet Packages

  2. Введите entityframeworkcore.sqlite в поле поиска.

  3. Выберите пакет Microsoft.EntityFrameworkCore.Sqlite.

  4. Установите флажок рядом с проектом в области справа и нажмите кнопку Установить.

    Sqlite Package

  5. Повторите шаги для поиска 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.

  1. Дважды щелкните файл MainWindow.xaml в Обозревателе решений, чтобы открыть главную форму.

  2. Перейдите на вкладку XAML, чтобы изменить код XAML.

  3. Сразу после открывающего тега 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>
    
  4. Таким образом будет задан один источник для "родительских" категорий, а второй — для продуктов "сведения".

  5. Затем добавьте в код 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>
    
  6. Обратите внимание, что для свойства 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"/>

Представление конструктора должно выглядеть следующим образом:

Screenshot of WPF Designer

Добавление кода, обрабатывающего взаимодействие с данными

Теперь следует добавить в главное окно несколько обработчиков событий.

  1. В окне XAML щелкните элемент <Window>, чтобы выбрать главное окно.

  2. В окне Свойства в правом верхнем углу выберите События, а затем дважды щелкните текстовое поле справа от метки Загружен.

    Main Window Properties

В результате вы перейдете к коду программной части формы, который теперь можно изменить, чтобы использовать 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. Введите имя категории и нажмите клавишу ВВОД, а затем добавьте продукты в нижнюю таблицу. Нажмите кнопку "Сохранить" и посмотрите, обновилась ли таблица с учетом указанных идентификаторов базы данных. Выделите строку и нажмите Удалить, чтобы ее удалить. Сущность будет удалена при нажатии кнопки Сохранить.

Running application

Уведомление об изменении свойства

Этот пример предполагает выполнение четырех шагов для синхронизации сущностей с пользовательским интерфейсом.

  1. При начальном вызове _context.Categories.Load() загружаются данные категорий.
  2. Прокси отложенной загрузки загружают данные зависимых продуктов.
  3. Встроенная функция отслеживания изменений EF Core вносит необходимые изменения в сущности, в том числе вставки и удаления, при вызове метода _context.SaveChanges().
  4. Вызовы DataGridView.Items.Refresh() приводят к принудительной перезагрузке с вновь созданными идентификаторами.

Это подходит для нашего примера для начала работы, в других сценариях может потребоваться дополнительный код. Элементы управления WPF преобразовывают для просмотра пользовательский интерфейс, считывая поля и свойства сущностей. При изменении значения в пользовательском интерфейсе это значение передается в сущность. При изменении значения свойства непосредственно в сущности, например при его загрузке из базы данных, WPF не отражает эти изменения сразу же в пользовательском интерфейсе. Подсистема преобразования для просмотра должна быть уведомлена об изменениях. В данном проекте это выполняется путем вызова Refresh() вручную. Такое уведомление можно легко автоматизировать, реализовав интерфейс INotifyPropertyChanged. Компоненты WPF будут автоматически обнаруживать интерфейс и регистрироваться для получения событий изменения. За создание таких событий отвечает сущность.

Совет

Дополнительные сведения об обработке изменений см. в статье о реализации уведомления об изменении свойств.

Дальнейшие действия

Узнайте больше о настройке DbContext.