WinForms とのデータバインド

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

このモデルでは、一対多のリレーションシップに関係する 2 つの型 Category (principal\master) と Product (dependent\detail) が定義されています。 次に、Visual Studio ツールを使用して、モデルで定義されている型を WinForms コントロールにバインドします。 WinForms データ バインディング フレームワークを使用すると、関連するオブジェクト間のナビゲーションが可能になります。マスター ビューで行を選択すると、対応する子データで詳細ビューが更新されます。

このチュートリアルのスクリーンショットとコード リストは、Visual Studio 2013 から取得されていますが、このチュートリアルを完了するには、Visual Studio 2012 または Visual Studio 2010 が必要です。

前提条件

このチュートリアルを実行するには、Visual Studio 2013、Visual Studio 2012 または Visual Studio 2010 がインストールされている必要があります。

Visual Studio 2010 を使っている場合は、NuGet もインストールする必要があります。 詳細については、NuGet のインストールを参照してください。

アプリケーションを作成する

  • Visual Studio を開きます
  • [ファイル] -> [新規] -> [プロジェクト]
  • 左側のウィンドウで [Windows] を選択し、右側のウィンドウで [Windows フォーム アプリケーション] を選択します。
  • 名前として「WinFormswithEFSample」と入力します。
  • [OK] を選択します。

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

  • ソリューション エクスプローラーで WinFormswithEFSample プロジェクトを右クリックします
  • [NuGet パッケージの管理] を選択します
  • [NuGet パッケージの管理] ダイアログで [オンライン] タブを選択し、[EntityFramework] パッケージを選択します。
  • [インストール]をクリックします。

    Note

    EntityFramework アセンブリに加えて、System.ComponentModel.DataAnnotations への参照も追加されます。 プロジェクトに System.Data.Entity への参照が含まれている場合は、EntityFramework パッケージのインストール時に削除されます。 Entity Framework 6 アプリケーションでは、System.Data.Entity アセンブリは使用されなくなりました。

コレクションの IListSource の実装

コレクション プロパティでは、Windows フォームを使用するときに並べ替えに対応した双方向のデータバインディングを有効にするために、IListSource インターフェイスを実装する必要があります。 これを行うには、ObservableCollection を拡張して IListSource 機能を追加します。

  • ObservableListSource クラスをプロジェクトに追加します。
    • プロジェクト名を右クリックします。
    • [追加] -> [新しい項目] の順に選択します。
    • [クラス] を選択し、クラス名に「ObservableListSource」と入力します。
  • 既定で生成されるコードを次のコードに置き換えます。

このクラスは、双方向のデータ バインディングと並べ替えを有効にします。 クラスは ObservableCollection<T> から派生し、IListSource の明示的な実装を追加します。 IListSource の GetList() メソッドは、ObservableCollection との同期を維持した状態の IBindingList 実装を返すように実装します。 ToBindingList によって生成される IBindingList 実装は、並べ替えをサポートします。 ToBindingList 拡張メソッドは EntityFramework アセンブリで定義されています。

    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Diagnostics.CodeAnalysis;
    using System.Data.Entity;

    namespace WinFormswithEFSample
    {
        public class ObservableListSource<T> : ObservableCollection<T>, IListSource
            where T : class
        {
            private IBindingList _bindingList;

            bool IListSource.ContainsListCollection { get { return false; } }

            IList IListSource.GetList()
            {
                return _bindingList ?? (_bindingList = this.ToBindingList());
            }
        }
    }

モデルを定義する

このチュートリアルでは、Code First または EF デザイナーを使ってモデルを実装することを選ぶことができます。 次の 2 つのセクションのいずれかを実行します。

オプション 1: Code First を使ってモデルを定義する

このセクションでは、Code First を使ってモデルとそれに関連付けられたデータベースを作成する方法について説明します。 Database First を使って、EF デザイナーでデータベースからモデルをリバース エンジニアリングする場合は、次のセクション (オプション 2: Database First を使ってモデルを定義する) に進んでください

通常、Code First の開発を使用するときはまず、概念 (ドメイン) モデルを定義する .NET Framework のクラスを記述します。

  • プロジェクトに新しい Product クラスを追加します。
  • 既定で生成されるコードを次のコードに置き換えます。
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace WinFormswithEFSample
    {
        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 クラスを追加します。
  • 既定で生成されるコードを次のコードに置き換えます。
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;

    namespace WinFormswithEFSample
    {
        public class Category
        {
            private readonly ObservableListSource<Product> _products =
                    new ObservableListSource<Product>();

            public int CategoryId { get; set; }
            public string Name { get; set; }
            public virtual ObservableListSource<Product> Products { get { return _products; } }
        }
    }

エンティティを定義するだけでなく、DbContext から派生して DbSet<TEntity> プロパティを公開するクラスを定義する必要があります。 DbSet プロパティにより、モデルに含める型をコンテキストに認識させることができます。 DbContext および DbSet 型は EntityFramework アセンブリで定義されています。

DbContext の派生型のインスタンスによって、実行時にエンティティ オブジェクトが管理されます。これには、オブジェクトへのデータベースのデータの設定、変更の追跡、データベースへのデータの保持が含まれます。

  • プロジェクトに新しい ProductContext クラスを追加します。
  • 既定で生成されるコードを次のコードに置き換えます。
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;
    using System.Text;

    namespace WinFormswithEFSample
    {
        public class ProductContext : DbContext
        {
            public DbSet<Category> Categories { get; set; }
            public DbSet<Product> Products { get; set; }
        }
    }

プロジェクトをコンパイルします。

オプション 2: Database First を使ってモデルを定義する

このセクションでは、Database First を使って、EF デザイナーでデータベースからモデルをリバース エンジニアリングする方法について説明します。 前のセクション (オプション 1: Code First を使ってモデルを定義する) を完了している場合は、このセクションをスキップし、遅延読み込みセクションに直接移動します。

既存のデータベースを作成する

通常、既存のデータベースを対象とする場合、データベースは既に作成されていますが、このチュートリアルでは、アクセスするデータベースを作成する必要があります。

Visual Studio と共にインストールされるデータベース サーバーは、インストールされている Visual Studio のバージョンによって異なります。

  • Visual Studio 2010 を使用している場合は、SQL Express データベースを作成します。
  • Visual Studio 2012 を使っている場合は、LocalDB データベースを作成します。

それではデータベースを生成しましょう。

  • [表示] -> [サーバー エクスプローラー]

  • [データ接続] を右クリックし、[接続の追加] を選択します。

  • これまでサーバー エクスプローラーからデータベースに接続したことがない場合は、データ ソースとして Microsoft SQL Server を選択する必要があります。

    Change Data Source

  • インストールされているものに応じて、LocalDB または SQL Express のいずれかに接続し、データベース名に「Products」と入力します。

    Add Connection LocalDB

    Add Connection Express

  • [OK] を選択すると、新しいデータベースを作成するかどうかを確認するメッセージが表示されます。[はい] を選択します。

    Create Database

  • サーバー エクスプローラーに新しいデータベースが表示されたら、それを右クリックし、[新しいクエリ] を選択します。

  • 次の SQL を新しいクエリにコピーし、クエリを右クリックして [実行] を選択します。

    CREATE TABLE [dbo].[Categories] (
        [CategoryId] [int] NOT NULL IDENTITY,
        [Name] [nvarchar](max),
        CONSTRAINT [PK_dbo.Categories] PRIMARY KEY ([CategoryId])
    )

    CREATE TABLE [dbo].[Products] (
        [ProductId] [int] NOT NULL IDENTITY,
        [Name] [nvarchar](max),
        [CategoryId] [int] NOT NULL,
        CONSTRAINT [PK_dbo.Products] PRIMARY KEY ([ProductId])
    )

    CREATE INDEX [IX_CategoryId] ON [dbo].[Products]([CategoryId])

    ALTER TABLE [dbo].[Products] ADD CONSTRAINT [FK_dbo.Products_dbo.Categories_CategoryId] FOREIGN KEY ([CategoryId]) REFERENCES [dbo].[Categories] ([CategoryId]) ON DELETE CASCADE

モデルをリバース エンジニアリングする

Visual Studio の一部として含まれている Entity Framework Designer を使用してモデルを作成します。

  • [プロジェクト] -> [新しい項目の追加...] を選択します。

  • 左側のメニューから [データ] を選び、[ADO.NET Entity Data Model] を選びます

  • 名前に「ProductModel」と入力し、[OK] をクリックします

  • これにより、Entity Data Model ウィザードが起動します。

  • [データベースから生成] を選択し、[次へ] をクリックします。

    Choose Model Contents

  • 最初のセクションで作成したデータベースへの接続を選び、接続文字列の名前に「ProductContext」と入力して、[次へ] をクリックします

    Choose Your Connection

  • [テーブル] の横のチェック ボックスをオンにして、すべてのテーブルをインポートし、[完了] をクリックします

    Choose Your Objects

リバース エンジニアリング プロセスが完了すると、新しいモデルがプロジェクトに追加され、Entity Framework Designer で表示できるように開かれます。 データベースの接続の詳細と共に、App.config ファイルもプロジェクトに追加されています。

Visual Studio 2010 での追加の手順

Visual Studio 2010 で作業している場合は、EF6 コード生成を使うように EF デザイナーを更新する必要があります。

  • EF Designer でモデルの空いている場所を右クリックし、[コード生成項目の追加] を選択します。
  • 左側のメニューから [オンライン テンプレート] を選び、DbContext を検索します。
  • [C# 用 EF 6.x DbContext ジェネレーター] を選び、名前に「ProductsModel」と入力して、[追加] をクリックします。

データ バインディングのコード生成の更新

EF により、T4 テンプレートを使ってモデルからコードが生成されます。 Visual Studio に含まれるテンプレートまたは Visual Studio ギャラリーからダウンロードしたテンプレートは、汎用的に使用することを目的としています。 つまり、これらのテンプレートから生成されたエンティティには、単純な ICollection<T> プロパティがあります。 ただし、データ バインディングを実行する場合は、IListSource を実装するコレクション プロパティを設定することをお勧めします。 上記の ObservableListSource クラスを作成したのはそのためで、このクラスを使用するようにテンプレートを変更します。

  • ソリューション エクスプローラーを開き、ProductModel.edmx ファイルを見つけます

  • ProductModel.edmx ファイルに入れ子にする ProductModel.tt ファイルを見つけます

    Product Model Template

  • ProductModel.tt ファイルをダブルクリックして Visual Studio のエディターで開きます

  • "ICollection" の 2 つの出現箇所を検索し、"ObservableListSource" に置き換えます。 これらは、296 行目付近と 484 行目付近にあります。

  • "HashSet" の最初の出現箇所を検索し、"ObservableListSource" に置き換えます。 出現箇所は、50 行目付近にあります。 コードの後半にある HashSet の 2 番目の出現箇所を置き換えないでください

  • ProductModel.tt ファイルを保存します。 これにより、エンティティのコードが再生成されます。 コードが自動的に再生成されない場合は、ProductModel.tt を右クリックし、[カスタム ツールの実行] を選択します。

Category.cs ファイル (ProductModel.tt の下に入れ子になっています) を開くと、Products コレクションの型が ObservableListSource<Product> であることがわかります。

プロジェクトをコンパイルします。

遅延読み込み

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

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

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

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

モデルで定義されているクラスを、この WinForms アプリケーションのデータ ソースとして追加します。

  • メイン メニューから、[プロジェクト] -> [新しいデータ ソースの追加] を選びます (Visual Studio 2010 では、[データ] -> [新しいデータ ソースの追加] を選ぶ必要があります)

  • [データ ソースの種類を選択] ウィンドウで、[オブジェクト] を選択して [次へ] をクリックします。

  • [データ オブジェクトの選択] ダイアログで、WinFormswithEFSample を 2 回展開し、[Category] を選択します Product データ ソースを選択する必要はありません。これは、Category データ ソースでの Product のプロパティを介して取得されるからです。

    Data Source

  • [完了] をクリックします。[データ ソース] ウィンドウが表示されていない場合は、[表示] -> [その他のウィンドウ] -> [データ ソース] を選択します

  • [データ ソース] ウィンドウが自動的に非表示にならないように、ピン アイコンを押します。 ウィンドウが既に表示されている場合は、更新ボタンを押す必要がある場合があります。

    Data Source 2

  • ソリューション エクスプローラーで、Form1.cs ファイルをダブルクリックして、デザイナーでメイン フォームを開きます。

  • Category データ ソースを選び、フォームにドラッグします。 既定では、新しい DataGridView (categoryDataGridView) コントロールとナビゲーション ツールバー コントロールがデザイナーに追加されます。 これらのコントロールは、同じように作成される BindingSource (categoryBindingSource) コンポーネントと Binding Navigator (categoryBindingNavigator) コンポーネントにバインドされます。

  • categoryDataGridView の列を編集します。 CategoryId 列を読み取り専用に設定します。 CategoryId プロパティの値は、データを保存した後にデータベースによって生成されます。

    • DataGridView コントロールを右クリックし、[列の編集...] を選択します。
    • CategoryId 列を選択し、ReadOnly を True に設定します。
    • [OK] を押します。
  • Category データソースの下にある [Products] を選択し、フォームにドラッグします。 productDataGridView と productBindingSource がフォームに追加されます。

  • productDataGridView の列を編集します。 CategoryId 列と Category 列を非表示にし、ProductId を読み取り専用に設定します。 ProductId プロパティの値は、データを保存した後にデータベースによって生成されます。

    • DataGridView コントロールを右クリックし、[列の編集...] を選択します。
    • ProductId 列を選択し、ReadOnlyTrue に設定します。
    • CategoryId 列を選択し、[削除] ボタンを押します。 Category 列でも同じ操作を行います。
    • OK をクリックします。

    ここまでは、デザイナーで DataGridView コントロールを BindingSource コンポーネントに関連付けました。 次のセクションでは、コードを分離コードに追加して、現在 DbContext によって追跡されているエンティティのコレクションに categoryBindingSource.DataSource を設定します。 Category の下から Products フォームをドラッグアンドドロップした場合、WinForms で productsBindingSource.DataSource プロパティが categoryBindingSource に設定され、productsBindingSource.DataMember プロパティが Products に設定されました。 このバインディングにより、現在選択されている Category に属する製品だけが productDataGridView に表示されます。

  • マウスの右ボタンをクリックして [有効] を選択し、ナビゲーション ツールバーの [保存] ボタンを有効にします。

    Form 1 Designer

  • ボタンをダブルクリックして [保存] ボタンのイベント ハンドラーを追加します。 これにより、イベント ハンドラーが追加され、フォームの分離コードに移動します。 次のセクションでは、categoryBindingNavigatorSaveItem_Click イベント ハンドラーのコードを追加します。

データ操作を処理するコードを追加する

次に、ProductContext を使用してデータ アクセスを実行するコードを追加します。 次に示すように、メイン フォーム ウィンドウのコードを更新します。

このコードでは、ProductContext の実行時間の長いインスタンスが宣言されています。 データをクエリしてデータベースに保存するには、ProductContext オブジェクトを使います。 その後、ProductContext インスタンスの Dispose() メソッドが、オーバーライドされた OnClosing メソッドから呼び出されます。 コードのコメントは、コードの動作に関する詳細を提供します。

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Forms;
    using System.Data.Entity;

    namespace WinFormswithEFSample
    {
        public partial class Form1 : Form
        {
            ProductContext _context;
            public Form1()
            {
                InitializeComponent();
            }

            protected override void OnLoad(EventArgs e)
            {
                base.OnLoad(e);
                _context = new ProductContext();

                // Call the Load method to get the data for the given DbSet
                // from the database.
                // The data is materialized as entities. The entities are managed by
                // the DbContext instance.
                _context.Categories.Load();

                // Bind the categoryBindingSource.DataSource to
                // all the Unchanged, Modified and Added Category objects that
                // are currently tracked by the DbContext.
                // Note that we need to call ToBindingList() on the
                // ObservableCollection<TEntity> returned by
                // the DbSet.Local property to get the BindingList<T>
                // in order to facilitate two-way binding in WinForms.
                this.categoryBindingSource.DataSource =
                    _context.Categories.Local.ToBindingList();
            }

            private void categoryBindingNavigatorSaveItem_Click(object sender, EventArgs e)
            {
                this.Validate();

                // Currently, the Entity Framework doesn’t mark the entities
                // that are removed from a navigation property (in our example the Products)
                // as deleted in the context.
                // The following code uses LINQ to Objects against the Local collection
                // to find all products and marks any that do not have
                // a Category reference as deleted.
                // The ToList call is required because otherwise
                // the collection will be modified
                // by the Remove call while it is being enumerated.
                // In most other situations you can do LINQ to Objects directly
                // against the Local property without using ToList first.
                foreach (var product in _context.Products.Local.ToList())
                {
                    if (product.Category == null)
                    {
                        _context.Products.Remove(product);
                    }
                }

                // Save the changes to the database.
                this._context.SaveChanges();

                // Refresh the controls to show the values         
                // that were generated by the database.
                this.categoryDataGridView.Refresh();
                this.productsDataGridView.Refresh();
            }

            protected override void OnClosing(CancelEventArgs e)
            {
                base.OnClosing(e);
                this._context.Dispose();
            }
        }
    }

Windows フォーム アプリケーションをテストする

  • アプリケーションをコンパイルして実行すると、機能をテストできます。

    Form 1 Before Save

  • 保存すると、ストアによって生成されたキーが画面に表示されます。

    Form 1 After Save

  • Code First を使用した場合は、WinFormswithEFSample.ProductContext データベースが作成されていることもわかります。

    Server Object Explorer