WPF の概要

このステップ バイ ステップ チュートリアルでは、"メイン - 詳細" フォームで POCO 型を WPF コントロールにバインドする方法について説明します。 アプリケーションでは、Entity Framework API を使用して、データベースのデータをオブジェクトに設定し、変更を追跡し、データベースにデータを保持します。

このモデルでは、一対多のリレーションシップに関係する 2 つの型 Category (principal\main) と Product (dependent\detail) が定義されています。 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)、[作成] を選択します。

Entity Framework NuGet パッケージをインストールする

  1. ソリューションを右クリックして、[ソリューションの NuGet パッケージの管理...] を選択します。

    Manage NuGet Packages

  2. 検索ボックスに、「entityframeworkcore.sqlite」と入力します。

  3. Microsoft.EntityFrameworkCore.Sqlite パッケージを選択します。

  4. 右側のペインでプロジェクトを確認し、[インストール] をクリックします

    Sqlite Package

  5. 同じようにして、entityframeworkcore.proxies を検索し、Microsoft.EntityFrameworkCore.Proxies をインストールします。

Note

Sqlite パッケージをインストールすると、関連する Microsoft.EntityFrameworkCore 基本パッケージが自動的にプルされます。 Microsoft.EntityFrameworkCore.Proxies パッケージでは、データの "遅延読み込み" がサポートされています。 これは、子エンティティを持つエンティティがある場合、最初の読み込みでは親のみがフェッチされることを意味します。 子エンティティにアクセスしようとすると、プロキシによって検出され、必要に応じて自動的に読み込まれます。

モデルを定義する

このチュートリアルでは、"Code First" を使用してモデルを実装します。つまり、定義した C# クラスに基づいて、EF Core によりデータベース テーブルとスキーマが作成されます。

新しいクラスを追加します。 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>();
    }
}

Category クラスの Products プロパティと、Product クラスの Category プロパティは、ナビゲーション プロパティです。 Entity Framework では、ナビゲーション プロパティによって、2 つのエンティティ型間のリレーションシップをナビゲートする手段が提供されます。

エンティティを定義するだけでなく、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 により、データベースにマップする必要のある C# エンティティが EF Core に示されます。
  • EF Core の DbContext を構成するには、さまざまな方法があります。 詳細については、「DbContext の構成」を参照してください。
  • この例では、OnConfiguring のオーバーライドを使用して、Sqlite データ ファイルを指定します。
  • UseLazyLoadingProxies の呼び出しにより、遅延読み込みを実装するよう EF Core に指示されるため、子エンティティは親からアクセスされた時点で自動的に読み込まれます。

Ctrl + Shift + B キーを押すか、[ビルド] > [ソリューションのビルド] に移動して、プロジェクトをコンパイルします。

ヒント

データベースと EF Core モデルの同期を維持するためのさまざまな方法について確認します: データベース スキーマを管理する

遅延読み込み

Category クラスの Products プロパティと、Product クラスの Category プロパティは、ナビゲーション プロパティです。 Entity Framework Core では、ナビゲーション プロパティによって、2 つのエンティティ型間のリレーションシップをナビゲートする手段が提供されます。

EF Core を使用すると、ナビゲーション プロパティに初めてアクセスしたときに、関連エンティティをデータベースから自動的に読み込むことができます。 この種類の読み込み (遅延読み込みと呼ばれます) では、各ナビゲーション プロパティに初めてアクセスしたときに、コンテンツがコンテキスト内に存在しない場合、データベースに対して別のクエリが実行されることに注意してください。

"単純な従来の C# オブジェクト" (POCO) エンティティ型を使用すると、EF Core では、実行中に派生プロキシ型のインスタンスを作成してから、クラス内の仮想プロパティをオーバーライドして読み込みフックを追加することにより、遅延読み込みが実現されます。 関連オブジェクトの遅延読み込みを行うには、ナビゲーション プロパティ ゲッターを public および virtual (Visual Basic では Overridable) として宣言する必要があり、クラスを sealed (Visual Basic では NotOverridable) にしないようにする必要があります。 Database First を使用すると、遅延読み込みを有効にするために、ナビゲーション プロパティは自動的に virtual にされます。

オブジェクトをコントロールにバインドする

モデルで定義されているクラスを、この 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. これにより、"親" カテゴリのソースと、"詳細" 製品の 2 番目のソースが設定されます。

  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"/>

デザイン ビューは次のようになります。

Screenshot of WPF Designer

データのやり取りを処理するコードを追加する

次に、いくつかのイベント ハンドラーをメイン ウィンドウに追加します。

  1. XAML ウィンドウで、<Window> 要素をクリックして、メイン ウィンドウを選択します。

  2. [プロパティ] ウィンドウで右上の [イベント] をクリックし、[Loaded] ラベルの右にあるテキスト ボックスをダブルクリックします。

    Main Window Properties

これにより、フォームのコード ビハインドが表示されます。ProductContext を使用してデータ アクセスを実行するように、コードを編集します。 次に示すようにコードを更新します。

このコードでは、ProductContext の実行時間の長いインスタンスが宣言されています。 データをクエリしてデータベースに保存するには、ProductContext オブジェクトを使用します。 その後、ProductContext インスタンスの Dispose() メソッドを、オーバーライドされた 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);
        }
    }
}

Note

このコードでは、EnsureCreated() の呼び出しを使用して、最初の実行時にデータベースを構築します。 これはデモでは許されますが、運用アプリでは、移行を調べてスキーマを管理する必要があります。 また、このコードは、ローカルの SQLite データベースを使用しているため、同期的に実行されます。 リモート サーバーが含まれるのが一般的である運用シナリオでは、Load および SaveChanges メソッドの非同期バージョンを使用することを検討します。

WPF アプリケーションをテストする

F5 キーを押すか、[デバッグ] > [デバッグの開始] を選択し、アプリケーションをコンパイルして実行します。 products.db という名前のファイルでデータベースが自動的に作成されるはずです。 カテゴリ名を入力して Enter キーを押し、下のグリッドに製品を追加します。 [保存] をクリックし、データベースで指定された ID でグリッドが更新されるのを確認します。 行を強調表示にして Delete キーを押し、行を削除します。 [保存] をクリックすると、エンティティが削除されます。

Running application

プロパティの変更通知

この例では、4 つのステップでエンティティと UI を同期させています。

  1. 最初の _context.Categories.Load() の呼び出しで、カテゴリ データが読み込まれます。
  2. 遅延読み込みプロキシにより、依存製品データが読み込まれます。
  3. _context.SaveChanges() を呼び出すと、EF Core に組み込まれている変更追跡により、挿入や削除など、エンティティに対して必要な変更が行われます。
  4. DataGridView.Items.Refresh() を呼び出すと、新しく生成された ID で強制的に再読み込みが行われます。

これは、この作業開始サンプルには使用できますが、他のシナリオでは追加のコードが必要になる場合があります。 WPF コントロールにより、エンティティのフィールドとプロパティが読み取られて、UI がレンダリングされます。 ユーザー インターフェイス (UI) で値を編集すると、その値がエンティティに渡されます。 データベースから読み込むなどして、エンティティのプロパティの値を直接変更すると、WPF によって変更が UI にすぐには反映されません。 レンダリング エンジンに変更が通知される必要があります。 このプロジェクトでは、手動で Refresh() を呼び出すことによってこれを行いました。 この通知を自動化する簡単な方法は、INotifyPropertyChanged インターフェイスを実装することです。 WPF のコンポーネントにより、自動的にインターフェイスが検出されて、変更イベントに登録されます。 エンティティでは、これらのイベントを発生させる必要があります。

ヒント

変更を処理する方法の詳細については、「プロパティ変更通知の実装方法」を参照してください。

次のステップ

DbContext の構成の詳細について確認します。