Поделиться через


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

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

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

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

Необходимые условия

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

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

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

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

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

    Управление пакетами NuGet

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

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

  4. Проверьте проект в правой области и нажмите кнопку "Установить"

    Пакет Sqlite

  5. Повторите шаги для поиска entityframeworkcore.proxies и установки Microsoft.EntityFrameworkCore.Proxies.

Замечание

При установке пакета Sqlite он автоматически загрузил связанный базовый пакет Microsoft.EntityFrameworkCore. Пакет Microsoft.EntityFrameworkCore.Proxies обеспечивает поддержку "отложенной загрузки" данных. Это означает, что при наличии сущностей с дочерними сущностями только родители загружаются при начальной загрузке. Прокси-серверы определяют, когда выполняется попытка доступа к дочерним сущностям и автоматически загружает их по запросу.

Определение модели

В этом пошаговом руководстве вы реализуете модель с помощью "code first". Это означает, что 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# должны быть сопоставлены с базой данных.
  • Существует множество способов настройки EF Core DbContext. Их можно прочитать в: «Настройка DbContext».
  • В этом примере параметр OnConfiguring используется для указания файла данных Sqlite.
  • Вызов UseLazyLoadingProxies сообщает EF Core о реализации отложенной загрузки, поэтому дочерние сущности автоматически загружаются при доступе из родительского объекта.

Нажмите клавиши CTRL+SHIFT+B или перейдите к Построение > Построить решение, чтобы скомпилировать проект.

Подсказка

Узнайте о различных способах поддержания синхронизации вашей базы данных и моделей EF Core: Управление схемами баз данных.

Отложенная загрузка

Свойство Products для класса Category и свойства Category в классе Product — это свойства навигации. В Entity Framework Core свойства навигации предоставляют способ навигации между двумя типами сущностей.

EF Core позволяет автоматически загружать связанные сущности из базы данных при первом доступе к свойству навигации. При таком типе загрузки (называемой отложенной загрузкой), помните, что при первом доступе к каждому свойству навигации отдельный запрос будет выполняться в базе данных, если содержимое еще не находится в контексте.

При использовании типов сущностей "Обычный старый объект C#" (POCO) EF Core достигает отложенной загрузки путем создания экземпляров производных прокси-типов в процессе выполнения и переопределения виртуальных свойств в ваших классах для добавления механизма загрузки. Чтобы получить отложенную загрузку связанных объектов, необходимо объявить методы получения свойств навигации как общедоступные и виртуальные (переопределяемые в Visual Basic), а класс не должен быть запечатан (NotOverridable в 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 и прикрепите событие нажатия к Button_Click.

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

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

Снимок экрана: конструктор WPF

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

Пришло время добавить некоторые обработчики событий в главное окно.

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

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

    Свойства главного окна

Это приведет вас к коду формы, теперь мы отредактируем код, чтобы использовать ProductContext для выполнения доступа к данным. Обновите код, как показано ниже.

Код объявляет долговременную инстанцию ProductContext. Объект ProductContext используется для запроса и сохранения данных в базе данных. Затем метод Dispose() вызывается на экземпляре ProductContext из переопределенного метода OnClosing. Примечания кода объясняют, что делает каждый шаг.

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

Выполнение приложения

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

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

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

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

Подсказка

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

Дальнейшие шаги

Дополнительные сведения о настройке DbContext.