Windows フォームについて

このステップバイステップ チュートリアルでは、SQLite データベースを利用する単純な Windows フォーム (WinForms) アプリケーションを構築する方法について説明します。 このアプリケーションでは、Entity Framework Core (EF Core) を使ってデータベースからデータを読み込み、そのデータに加えられた変更を追跡し、それらの変更をデータベースに保持します。

このチュートリアルのスクリーンショットとコード リストは、Visual Studio 2022 17.3.0 から取得されています。

ヒント

この記事のサンプルは GitHub で確認できます。

前提条件

このチュートリアルを完了するには、Visual Studio 2022 17.3 以降がインストールされ、.NET デスクトップ ワークロードが選ばれている必要があります。 Visual Studio の最新バージョンのインストールの詳細については、「Visual Studio のインストール」を参照してください。

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

  1. Visual Studio を開きます

  2. スタート ウィンドウで、[新しいプロジェクトの作成] を選択します。

  3. [Windows フォーム アプリケーション] を選び、[次へ] を選びます。

    新しい Windows フォーム プロジェクトを作成する

  4. 次の画面で、プロジェクトの名前を指定し (例: GetStartedWinForms)、[次へ] を選びます。

  5. 次の画面で、使う .NET バージョンを選びます。 このチュートリアルは .NET 7 を使って作成されましたが、それ以降のバージョンでも機能するはずです。

  6. [作成] を選択します。

EF Core の NuGet パッケージをインストールする

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

     ソリューションの NuGet パッケージの管理

  2. [参照] タブを選び、「Microsoft.EntityFrameworkCore.Sqlite」を検索します。

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

  4. 右ペインのプロジェクト GetStartedWinForms を確認します。

  5. 最新バージョンを選択します。 プレリリース バージョンを使う場合は、必ず [プレリリースを含める] をオンにします。

  6. [インストール]をクリックします。

    Microsoft.EntityFrameworkCore.Sqlite パッケージをインストールする

Note

Microsoft.EntityFrameworkCore.Sqlite は、SQLite データベースで EF Core を使うための "データベース プロバイダー" パッケージです。 他のデータベース システムでも同様のパッケージを使用できます。 データベース プロバイダー パッケージをインストールすると、そのデータベース システムで EF Core を使うために必要なすべての依存関係が自動的に導入されます。 これには Microsoft.EntityFrameworkCore ベース パッケージが含まれます。

モデルを定義する

このチュートリアルでは、"Code First" を使ってモデルを実装します。 つまり、定義した C# クラスに基づいて、EF Core によりデータベース テーブルとスキーマが作成されます。 代わりに既存のデータベースを使う方法については、データベース スキーマの管理に関する記事を参照してください。

  1. プロジェクトを右クリックして [追加] を選び、[クラス] を選んで新しいクラスを追加します。

    新しいクラスを追加する

  2. ファイル名 Product.cs を使い、クラスのコードを次の内容に置き換えます。

    using System.ComponentModel;
    
    namespace GetStartedWinForms;
    
    public class Product
    {
        public int ProductId { get; set; }
    
        public string? Name { get; set; }
    
        public int CategoryId { get; set; }
        public virtual Category Category { get; set; } = null!;
    }
    
  3. 次のコードを使った Category.cs の作成を繰り返します。

    using Microsoft.EntityFrameworkCore.ChangeTracking;
    
    namespace GetStartedWinForms;
    
    public class Category
    {
        public int CategoryId { get; set; }
    
        public string? Name { get; set; }
    
        public virtual ObservableCollectionListSource<Product> Products { get; } = new();
    }
    

Category クラスの Products プロパティと、Product クラスの Category プロパティは、"ナビゲーション" と呼ばれます。 EF Core では、ナビゲーションによって 2 つのエンティティ型間のリレーションシップを定義します。 この場合、Product.Category ナビゲーションは、指定した製品が属するカテゴリを参照します。 同様に、Category.Products コレクション ナビゲーションには、指定したカテゴリのすべての製品が含まれています。

ヒント

Windows フォームを使う場合、IListSource を実装する ObservableCollectionListSource はコレクション ナビゲーションに使用できます。 これは必須ではありませんが、双方向のデータ バインディング エクスペリエンスが向上します。

DbContext を定義する

EF Core では、DbContext から派生したクラスは、モデル内のエンティティ型を構成し、データベースと対話するためのセッションとして機能するために使われます。 最も単純なケースでは、DbContext クラスです。

  • モデル内のエンティティ型ごとに DbSet プロパティを含みます。
  • OnConfiguring メソッドをオーバーライドして、使うデータベース プロバイダーと接続文字列を構成します。 詳細については DbContext の構成に関する記事を参照してください。

この場合、DbContext クラスも OnModelCreating メソッドをオーバーライドし、アプリケーションに一部のサンプル データを提供します。

次のようなコードを使って新しい ProductsContext.cs クラスをプロジェクトに追加します。

using Microsoft.EntityFrameworkCore;

namespace GetStartedWinForms;

public class ProductsContext : 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");

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Category>().HasData(
            new Category { CategoryId = 1, Name = "Cheese" },
            new Category { CategoryId = 2, Name = "Meat" },
            new Category { CategoryId = 3, Name = "Fish" },
            new Category { CategoryId = 4, Name = "Bread" });

        modelBuilder.Entity<Product>().HasData(
            new Product { ProductId = 1, CategoryId = 1, Name = "Cheddar" },
            new Product { ProductId = 2, CategoryId = 1, Name = "Brie" },
            new Product { ProductId = 3, CategoryId = 1, Name = "Stilton" },
            new Product { ProductId = 4, CategoryId = 1, Name = "Cheshire" },
            new Product { ProductId = 5, CategoryId = 1, Name = "Swiss" },
            new Product { ProductId = 6, CategoryId = 1, Name = "Gruyere" },
            new Product { ProductId = 7, CategoryId = 1, Name = "Colby" },
            new Product { ProductId = 8, CategoryId = 1, Name = "Mozzela" },
            new Product { ProductId = 9, CategoryId = 1, Name = "Ricotta" },
            new Product { ProductId = 10, CategoryId = 1, Name = "Parmesan" },
            new Product { ProductId = 11, CategoryId = 2, Name = "Ham" },
            new Product { ProductId = 12, CategoryId = 2, Name = "Beef" },
            new Product { ProductId = 13, CategoryId = 2, Name = "Chicken" },
            new Product { ProductId = 14, CategoryId = 2, Name = "Turkey" },
            new Product { ProductId = 15, CategoryId = 2, Name = "Prosciutto" },
            new Product { ProductId = 16, CategoryId = 2, Name = "Bacon" },
            new Product { ProductId = 17, CategoryId = 2, Name = "Mutton" },
            new Product { ProductId = 18, CategoryId = 2, Name = "Pastrami" },
            new Product { ProductId = 19, CategoryId = 2, Name = "Hazlet" },
            new Product { ProductId = 20, CategoryId = 2, Name = "Salami" },
            new Product { ProductId = 21, CategoryId = 3, Name = "Salmon" },
            new Product { ProductId = 22, CategoryId = 3, Name = "Tuna" },
            new Product { ProductId = 23, CategoryId = 3, Name = "Mackerel" },
            new Product { ProductId = 24, CategoryId = 4, Name = "Rye" },
            new Product { ProductId = 25, CategoryId = 4, Name = "Wheat" },
            new Product { ProductId = 26, CategoryId = 4, Name = "Brioche" },
            new Product { ProductId = 27, CategoryId = 4, Name = "Naan" },
            new Product { ProductId = 28, CategoryId = 4, Name = "Focaccia" },
            new Product { ProductId = 29, CategoryId = 4, Name = "Malted" },
            new Product { ProductId = 30, CategoryId = 4, Name = "Sourdough" },
            new Product { ProductId = 31, CategoryId = 4, Name = "Corn" },
            new Product { ProductId = 32, CategoryId = 4, Name = "White" },
            new Product { ProductId = 33, CategoryId = 4, Name = "Soda" });
    }
}

この時点で、必ずソリューションをビルドしてください。

フォームへのコントロールの追加

このアプリケーションには、カテゴリの一覧と製品の一覧が表示されます。 最初の一覧でカテゴリが選ばれると、2 つ目の一覧はそのカテゴリの製品を表示するように変更されます。 これらの一覧を変更して製品とカテゴリの追加、削除、または編集を行うことができます。また、[保存] ボタンをクリックすることで、SQLite データベースにこれらに変更を保存できます。

  1. メイン フォームの名前を Form1 から MainForm に変更します。

    Form1 の名前を MainForm に変更する

  2. 次に、タイトルを「製品とカテゴリ」に変更します。

  3. Toolbox を使って、2 つの DataGridView コントロールを追加し、並べて配置します。

    DataGridView を追加する

  4. 最初の DataGridView[プロパティ] で、[名前]dataGridViewCategories に変更します。

  5. 2 つ目の DataGridView[プロパティ] で、[名前]dataGridViewProducts に変更します。

  6. また、Toolbox を使って、Button コントロールを追加します。

  7. ボタンに buttonSave という名前を付け、「保存」というテキストを指定します。 フォームは次のようになります。

    フォームのレイアウト

データ バインディング

次の手順は、モデルから ProductCategory の型を DataGridView コントロールに接続することです。 そうすると、EF Core によって読み込まれたデータはコントロールにバインドされ、EF Core によって追跡されるエンティティは、コントロールに表示されるものと同期が保たれるようになります。

  1. 1 つ目の DataGridView に対してデザイナー アクション グリフをクリックします。 これは、コントロールの右上隅にある小さなボタンです。

    デザイナー アクション グリフ

  2. これで [アクション] 一覧が開きます。そこから [データ ソースの選択] ドロップダウンにアクセスできます。 まだデータ ソースを作成していないので、一番下に移動し、[新しいオブジェクト データ ソースの追加] を選びます。

    [新しいオブジェクト データ ソースの追加]

  3. [カテゴリ] を選んで、カテゴリのオブジェクト データ ソースを作成し、[OK] をクリックします。

    Category データ ソース型の選択

    ヒント

    ここにデータ ソースの種類が表示されない場合は、Product.csCategory.csProductsContext.cs がプロジェクトに追加されていることと "とソリューションがビルドされていること" を確認します。

  4. これで、[データ ソースの選択] ドロップダウンに、先ほど作成したオブジェクト データ ソースが表示されるようになります。 [その他のデータ ソース] を展開してから、[プロジェクト データ ソース] を選び、[カテゴリ] を選びます。

    カテゴリのデータ ソースを選ぶ

    2 つ目の DataGridView は製品にバインドされます。 ただし、最上位レベルの Product 型にバインドされるのではなく、1 つ目の DataGridViewCategory バインドから Products ナビゲーションにバインドされます。 これは、1 つ目のビューでカテゴリが選ばれると、そのカテゴリの製品が自動的に 2 つ目のビューで使われることを意味します。

  5. 2 つ目の DataGridView に対してデザイナー アクション グリフを使って、[データ ソースの選択] を選び、categoryBindingSource を展開し、Products を選びます。

    製品のデータ ソースを選ぶ

表示内容を構成する

既定では、バインドされた型を持つプロパティごとに、DataGridView に列が作成されます。 また、これらの各プロパティの値はユーザーが編集できます。 ただし、主キー値などの一部の値は概念的に読み取り専用であるため、編集しないでください。 また、CategoryId 外部キー プロパティや Category ナビゲーションなど、一部のプロパティはユーザーにとって役に立たないので、非表示にする必要があります。

ヒント

実際のアプリケーションでは、主キーのプロパティを非表示にするのが一般的です。 背後で EF Core が行っている処理を簡単に確認できるように、ここでは表示したままにしています。

  1. 1 つ目の DataGridView を右クリックして、[列の編集] を選びます。

    DataGridView 列を編集する

  2. 主キーを表す CategoryId 列を読み取り専用にし、[OK] をクリックします。

    CategoryId 列を読み取り専用にする

  3. 2 つ目の DataGridView を右クリックし、[列の編集] を選びます。ProductId 列を読み取り専用にし、CategoryIdCategory の列を削除して、[OK] をクリックします。

    ProductId 列を読み取り専用にし、CategoryId と Category の列を削除する

EF Core への接続

このアプリケーションで EF Core をデータ バインド コントロールに接続するには、コードが少し必要です。

  1. ファイルを右クリックし、[コードの表示] を選んで、MainForm コードを開きます。

    [コードの表示]

  2. セッションのために DbContext を保持するプライベート フィールドを追加し、OnLoadOnClosing のメソッドのオーバーライドを追加します。 コードは、次のようになります。

using Microsoft.EntityFrameworkCore;
using System.ComponentModel;

namespace GetStartedWinForms
{
    public partial class MainForm : Form
    {
        private ProductsContext? dbContext;

        public MainForm()
        {
            InitializeComponent();
        }

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            this.dbContext = new ProductsContext();

            // Uncomment the line below to start fresh with a new database.
            // this.dbContext.Database.EnsureDeleted();
            this.dbContext.Database.EnsureCreated();

            this.dbContext.Categories.Load();

            this.categoryBindingSource.DataSource = dbContext.Categories.Local.ToBindingList();
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            base.OnClosing(e);

            this.dbContext?.Dispose();
            this.dbContext = null;
        }
    }
}

OnLoad メソッドは、フォームが読み込まれるときに呼び出されます。 この時点で

  • ProductsContext のインスタンスが作成されます。これは、アプリケーションに表示される製品とカテゴリの変更を読み込み、追跡するために使われます。
  • DbContext に対して EnsureCreated が呼び出され、SQLite データベースがまだ存在しない場合に作成されます。 アプリケーションのプロトタイプを作成したりテストしたりするときに、この方法でデータベースを簡単に作成できます。 ただし、モデルが変更された場合は、データベースを削除して、再度作成できるようにする必要があります (アプリケーションの実行時にデータベースを簡単に削除して再作成するために、EnsureDeleted 行のコメントアウトを解除することができます)。代わりに [EF Core Migrations] (EF Core の移行) を使うと、データを失うことなく、データベース スキーマの変更と更新を行うことができます。
  • また、EnsureCreatedProductsContext.OnModelCreating メソッドで定義されたデータを新しいデータベースに設定します。
  • Load 拡張メソッドは、データベースから DbContext にすべてのカテゴリを読み込むために使われます。 これらのエンティティは DbContext によって追跡されるようになります。その結果、ユーザーがカテゴリを編集したときに加えたすべての変更が検出されます。
  • categoryBindingSource.DataSource プロパティは DbContext によって追跡されるカテゴリに初期化されます。 これは、Categories DbSet プロパティに対して Local.ToBindingList() を呼び出すことで行われます。 Local によって、追跡されるカテゴリのローカル ビューへのアクセス権が付与され、イベントがフックされ、ローカル データから表示データとその逆方向の同期状態が確実に保たれます。 ToBindingList() はこのデータを IBindingList として公開します。これは、Windows フォーム データ バインディングによって認識されます。

OnClosing メソッドは、フォームが閉じられたときに呼び出されます。 この時点で DbContext は破棄されます。これにより、すべてのデータベース リソースは確実に解放され、dbContext フィールドは NULL に設定され、再使用できなくなります。

製品ビューの設定

この時点でアプリケーションを起動すると、次のような外観になります。

アプリケーションの最初の実行

カテゴリはデータベースから読み込まれましたが、products テーブルは空のままであることに注意してください。 また、[保存] ボタンは機能しません。

products テーブルに設定するために、EF Core は選んだカテゴリの製品をデータベースから読み込む必要があります。 具体的な手順は次のとおりです。

  1. メイン フォームのデザイナーで、カテゴリに DataGridView を選びます。

  2. DataGridView[プロパティ] で、イベント (稲妻ボタン) を選び、SelectionChanged イベントをダブルクリックします。

    SelectionChanged イベントを追加する

    これにより、カテゴリの選択が変わるたびに発生するイベントのスタブがメイン フォーム コードに作成されます。

  3. イベントのコードを入力します。

private void dataGridViewCategories_SelectionChanged(object sender, EventArgs e)
{
    if (this.dbContext != null)
    {
        var category = (Category)this.dataGridViewCategories.CurrentRow.DataBoundItem;

        if (category != null)
        {
            this.dbContext.Entry(category).Collection(e => e.Products).Load();
        }
    }
}

このコードでは、アクティブな (非 NULL の) DbContext セッションがある場合、DataViewGrid の現在選ばれている行にバインドされている Category インスタンスを取得します (これは、ビューの最後の行が選ばれている場合、新しいカテゴリを作成するために使われる null である可能性があります)。選ばれたカテゴリがある場合、DbContext はそのカテゴリに関連付けられた製品を読み込むよう指示されます。 このためには、次のことを行います。

  • Category インスタンスの EntityEntry の取得 (dbContext.Entry(category))
  • その CategoryProducts コレクション ナビゲーションで操作することを EF Core に知らせる (.Collection(e => e.Products))
  • 最後に、データベースから製品のそのコレクションを読み込むことを EF Core に伝える (.Load();)

ヒント

Load が呼び出されると、EF Core は、まだ読み込まれていない場合にのみ、データベースにアクセスして製品を読み込みます。

次にアプリケーションを再実行する場合は、カテゴリが選ばれるたびに適切な製品を読み込む必要があります。

製品が読み込まれた

変更を保存しています

最後に、[保存] ボタンを EF Core に接続すると、製品とカテゴリに加えられたすべての変更がデータベースに保存されるようになります。

  1. メイン フォームのデザイナーで、[保存] ボタンを選びます。

  2. Button[プロパティ] で、イベント (稲妻ボタン) を選び、Click イベントをダブルクリックします。

    [保存] の Click イベントを追加する

  3. イベントのコードを入力します。

private void buttonSave_Click(object sender, EventArgs e)
{
    this.dbContext!.SaveChanges();

    this.dataGridViewCategories.Refresh();
    this.dataGridViewProducts.Refresh();
}

このコードでは、DbContext に対して SaveChanges を呼び出し、SQLite データベースに加えられたすべての変更が保存されます。 変更がなければ、これは操作なしであり、データベースの呼び出しは行われません。 保存後、DataGridView コントロールは更新されます。 これは、データベースから新しい製品とカテゴリ用に生成された主キー値を EF Core が読み取るためです。 Refresh を呼び出すと、これらの生成された値で表示は更新されます。

最終的なアプリケーション

メイン フォームの完全なコードは次のとおりです。

using Microsoft.EntityFrameworkCore;
using System.ComponentModel;

namespace GetStartedWinForms
{
    public partial class MainForm : Form
    {
        private ProductsContext? dbContext;

        public MainForm()
        {
            InitializeComponent();
        }

        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            this.dbContext = new ProductsContext();

            // Uncomment the line below to start fresh with a new database.
            // this.dbContext.Database.EnsureDeleted();
            this.dbContext.Database.EnsureCreated();

            this.dbContext.Categories.Load();

            this.categoryBindingSource.DataSource = dbContext.Categories.Local.ToBindingList();
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            base.OnClosing(e);

            this.dbContext?.Dispose();
            this.dbContext = null;
        }

        private void dataGridViewCategories_SelectionChanged(object sender, EventArgs e)
        {
            if (this.dbContext != null)
            {
                var category = (Category)this.dataGridViewCategories.CurrentRow.DataBoundItem;

                if (category != null)
                {
                    this.dbContext.Entry(category).Collection(e => e.Products).Load();
                }
            }
        }

        private void buttonSave_Click(object sender, EventArgs e)
        {
            this.dbContext!.SaveChanges();

            this.dataGridViewCategories.Refresh();
            this.dataGridViewProducts.Refresh();
        }
    }
}

これでアプリケーションを実行できるようになりました。製品とカテゴリを追加、削除、編集することができます。 アプリケーションを閉じる前に [保存] ボタンをクリックすると、加えられた変更内容はデータベースに格納され、アプリケーションの再起動時に再度読み込まれることに注意してください。 [保存] をクリックしない場合、アプリケーションの再起動時に変更内容は失われます。

ヒント

コントロールの下部にある空の行を使って、新しいカテゴリまたは製品を DataViewControl に追加できます。 行を削除するには、行を選んで Del キーを押します。

保存前

[保存] をクリックする前の実行中のアプリケーション

保存後

[保存] をクリックした後の実行中のアプリケーション

[保存] をクリックすると、追加されたカテゴリと製品の主キー値が設定されることに注意してください。

詳細情報