C#のWPFでEntity Frameworkを使いSQLiteのDBをCRUD処理するサンプルコード

コンピュータ

DBのテーブルを編集するGUIフォームを作成するにあたり必要な情報を集めてコード化してみました。さらにコード量が多くなりそうなので一旦記事にしてみました。

プロジェクトの作成

dotnet new wpf -n プロジェクト名
cd プロジェクト名
dotnet new wpf
dotnet add package Microsoft.Xaml.Behaviors.Wpf
dotnet add package ReactiveProperty.WPF
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Sqlite.Core
dotnet add package SQLitePCLRaw.bundle_green

WPFでReactiveProperyとBehaviorsが使えるようにパッケージを追加
さらにEntityFrameworkでSQLiteが使えるようにパッケージを追加

ソースコード

ファイル名:DataContext.cs

using Microsoft.EntityFrameworkCore;

namespace WPFCRUDA01;
public class DataContext : DbContext
{
    public DbSet<Item> Items => Set<Item>();
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
        optionsBuilder.UseSqlite($"Data Source=items.db");    
}

Entity FrameworkでSQLiteのデータベースへアクセスする窓口。Itemsがテーブルに相当。
ファイル名:Item.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace WPFCRUDA01;
public class Item
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)] // 値が追加されるときに自動生成
    [Key] // 主キー
    public int Id { get; set; }

    [Required]
    public string Name { get; set; } = "";

    public string Description { get; set; } = "";
}

Entity Framework(SQLite)用、テーブルの定義体
ファイル名:ItemModel.cs

namespace WPFCRUDA01;
public class ItemModel
{
    public int Id { get; set; }

    public string Name { get; set; } = "";

    public string Description { get; set; } = "";
}

WPF用
ファイル名:MainWindow.xaml

<Window
  x:Class="WPFCRUDA01.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:WPFCRUDA01"
  mc:Ignorable="d" Title="タイトル"
  Height="450" Width="800"
  xmlns:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors">
  <Window.DataContext>
    <local:MainWindowViewModel />
  </Window.DataContext>
  <i:Interaction.Behaviors>
    <local:WindowCloseBehavior />
  </i:Interaction.Behaviors>
  <Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/> <!-- 0.左 -->
        <ColumnDefinition Width="*"/> <!-- 1.右 -->
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/><!-- 0行目 -->
        <RowDefinition Height="Auto"/><!-- 1行目 -->
        <RowDefinition Height="Auto"/><!-- 2行目 -->
        <RowDefinition Height="Auto"/><!-- 3行目 -->
        <RowDefinition Height="*"/><!-- 4行目 -->
        <RowDefinition Height="Auto"/><!-- 5行目 -->
    </Grid.RowDefinitions>
    <TextBlock Grid.Row="0"
      Grid.Column="0"
      Text="ID:"
      Margin="5"/>
     <TextBox Grid.Row="0"
      Grid.Column="1"
      Text="{Binding Id.Value, Mode=OneWay}"
      IsEnabled="False"
      Margin="5"/>
    <TextBlock
      Grid.Row="1"
      Grid.Column="0"
      Text="Name:"
      Margin="5"/>
    <TextBox Grid.Row="1"
      Grid.Column="1"
      Text="{Binding Name.Value}"
      Margin="5"/>
    <TextBlock Grid.Row="2"
      Grid.Column="0"
      Text="Description:"
      Margin="5"/>
    <TextBox Grid.Row="2"
      Grid.Column="1"
      Text="{Binding Description.Value}"
      Margin="5"/>
    
    <StackPanel Grid.Row="3"
      Grid.Column="1"
      Orientation="Horizontal"
      Margin="5">

      <Button Command="{Binding AddCommand}"
        Content="Add"
        Margin="5"/>
      <Button Command="{Binding UpdateCommand}"
        Content="Update"
        Margin="5"/>
      <Button Command="{Binding DeleteCommand}"
        Content="Delete"
        Margin="5"/>
      <Button Command="{Binding LoadWithClearCommand}"
        Content="LoadWithClear"
        Margin="5"/>

    </StackPanel>

    <ListBox Grid.Row="4"
      Grid.ColumnSpan="2"
      ItemsSource="{Binding Items}"
      SelectedItem="{Binding SelectedItem.Value, Mode=TwoWay}"
      Margin="5">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Id}"
                      Width="30"/>
                    <TextBlock Text="{Binding Name}"
                      Width="100"/>
                    <TextBlock Text="{Binding Description}"/>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    <TextBlock Grid.Row="5"
      Grid.ColumnSpan="2"
      Text="{Binding Items.Count, StringFormat='Total Items: {0}'}"
      Margin="5"/>


  </Grid>
</Window>

ViewとなるXAML。コードビハインドのMainWindow.xaml.csは変更なし

ファイル名:MainWindowViewModel.cs

using System.Diagnostics;
using System;
using System.ComponentModel;
using Reactive.Bindings;
using Reactive.Bindings.Extensions;
using System.Reactive.Disposables;
using System.Reactive.Linq;

namespace WPFCRUDA01;

public class MainWindowViewModel : INotifyPropertyChanged, IDisposable
{
    // INotifyPropertyChanged
    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged(string name) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    // IDisposable
    private CompositeDisposable Disposable { get; } = [];
    /**************************************************************************
    * プロパティ
    **************************************************************************/
    public ReactiveProperty<int> Id {get;set;} = new (-1);
    public ReactiveProperty<string> Name {get;set;} = new ("");
    public ReactiveProperty<string> Description {get;set;} = new ("");

    public AsyncReactiveCommand AddCommand {get;} = new();
    public AsyncReactiveCommand UpdateCommand {get;} = new();
    public AsyncReactiveCommand DeleteCommand {get;} = new();
    public AsyncReactiveCommand LoadWithClearCommand {get;} = new();

    public ReactiveCollection<ItemModel> Items {get; set;} = [];
    public ReactiveProperty<ItemModel> SelectedItem {get; set;} = new();

    private SQLiteDB _db = new();
    public MainWindowViewModel()
    {
        Id.AddTo(this.Disposable);
        Name.AddTo(this.Disposable);
        Description.AddTo(this.Disposable);

        AddCommand.WithSubscribe(async ()=>
        {
            System.Diagnostics.Debug.Print("Add");

            var item = new Item()
            {
                Name = Name.Value,
                Description = Description.Value,
            };
            await _db.AddAsync(item);

            LoadWithClearCommand.Execute();
        }).AddTo(this.Disposable);
        UpdateCommand.WithSubscribe(async () =>
        {
            System.Diagnostics.Debug.Print("Update");

            if (Id.Value <= 0) return;

            var item = new Item()
            {
                Id = Id.Value,
                Name = Name.Value,
                Description = Description.Value,
            };
            await _db.UpdateAsync(item);

            LoadWithClearCommand.Execute();
        }).AddTo(this.Disposable);
        DeleteCommand.WithSubscribe(async ()=>
        {
            System.Diagnostics.Debug.Print("Delete");

            if (Id.Value <= 0) return;
            await _db.DeleteAsync(Id.Value);

            LoadWithClearCommand.Execute();
        }).AddTo(this.Disposable);
        LoadWithClearCommand.WithSubscribe(async ()=>
        {
            System.Diagnostics.Debug.Print("Load");
            Task<Item[]> getAllAsyncTask = _db.GetAllAsync();

            var tcs = new TaskCompletionSource<bool>();
            using var v = Items.ObserveResetChanged<int>().Subscribe(x=>
            {
                // クリアが完了すると呼ばれるイベント。
                tcs.SetResult(true);    // 結果にtureをセット
            });
            Items.ClearOnScheduler();
            await tcs.Task;  // tcsの終了を待つ
            System.Diagnostics.Debug.Print("Clear");

            Item[] items =  await getAllAsyncTask;

            foreach(var item in items)
            {
                Items.AddOnScheduler(new()
                {
                    Id = item.Id,
                    Name = item.Name,
                    Description = item.Description,
                });
            }

            Id.Value = 0;
            Name.Value = "";
            Description.Value = "";
        }).AddTo(this.Disposable);

        Items.AddTo(this.Disposable);
        SelectedItem.Subscribe(e=>
        {
            if (e is null) return;
            Id.Value = e.Id;
            Name.Value = e.Name;
            Description.Value = e.Description;
        }).AddTo(this.Disposable);

        _db.AddTo(this.Disposable);



        LoadWithClearCommand.Execute();
    }
    /// <summary>
    /// Dispose
    /// </summary>
    public void Dispose() =>Disposable.Dispose();
}

MainWindow.xamlと紐づくViewModel
ファイル名:SQLiteDB.cs


using Microsoft.EntityFrameworkCore;

namespace WPFCRUDA01;
public class SQLiteDB : IDisposable
{
    DataContext _context;

    public SQLiteDB()
    {
        _context = new();
        _context.Database.EnsureCreated();
    }

    public void Dispose()
    {
        _context?.Dispose();
    }

    public async Task AddAsync(Item item)
    {
        await _context.Items.AddAsync(item);
        await _context.SaveChangesAsync();
    }
    public async Task<Item[]> GetAllAsync() => await _context.Items.ToArrayAsync();
    public async Task DeleteAsync(int id)
    {
        var f = await _context.Items.SingleAsync(x => x.Id == id);
        if (f is null) return;

        _context.Items.Remove(f);
        await _context.SaveChangesAsync();
    }
    public async Task UpdateAsync(Item item)
    {
        var f = await _context.Items.SingleAsync(x => x.Id == item.Id);
        if (f is null) return;

        f.Description = item.Description;
        f.Name = item.Name;

        await _context.SaveChangesAsync();
    }
}

Entity FrameworkでSQLiteのデータベースへアクセスする部分
ファイル名:WindowCloseBehavior.cs

using Microsoft.Xaml.Behaviors;
using System.Windows;

namespace WPFCRUDA01;

public class WindowCloseBehavior : Behavior<Window>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        this.AssociatedObject.Closed += this.WindowClosed;
    }

    private void WindowClosed(object? sender, EventArgs e)
    {
        (this.AssociatedObject.DataContext as IDisposable)?.Dispose();
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        this.AssociatedObject.Closed -= this.WindowClosed;
    }
}

こちらをXAMLのWindowにセットすると、ウィンドウが閉じた際紐づけられたDataContext(ViewModel)がIDisposableだった場合、Dispose()してくれるようになります。

実行

dotnet run

CRUD

CRUDとは、「Create(作成)」、「Read(読み取り)」、「Update(更新)」、「Delete(削除)」らしいですが、サンプルコードではSQLiteDBクラス(SQLiteDB.cs)に機能を集約してみました。

「Create(作成)」はコンストラクタの以下のコードでデータベースを作成しています。

_context.Database.EnsureCreated();

「Read(読み取り)」はGetAllAsync()
サンプルコードではListBoxの表示用。

「Update(更新)」はAddAsync()UpdateAsync()

「Delete(削除)」DeleteAsync()

CURDの各機能をメソッドに割り当てて見ました。

EntityFrameworkは便利ですね。SQLを書かなくともDBへアクセスできてしまいます。
EntityFrameworkを使わない場合、SQLを書いてDBへアクセスするわけですが、レコードの取得時の絞り込み条件やInsertやUpdateなどでレコードにセットする値などC#の値をSQLの文字列にセットする必要があったり、取得したレコードのデータをC#で扱えるように変数にセットしたりする必要があります。そのあたりEntityFrameworkを使うと省略する事ができ、基本c#で完結することが出来るようになっています。
また、テーブルを含むデータベースの作成なども自動的に行なってくれるなども便利です。

細かな制御が難しいのがデメリットで、例えばサンプルではC#で定義したItemsがDBのテーブル名になりますが、C#側とDBのテーブル名を異なる名前にしたい場合などこちらのサンプルコードでは未対応です。方法はいくつかあるようですが、既存のDBと接続したい場合などDB側のテーブル名に合わせると、C#内で既に使われている名称の可能性があったりします。(特にItemsなんてざっくりした名前をつけると高確率で。。。)

<

h2>Keyの自動採番(AUTOINCREMENT)/h2>
Item.csの以下の部分が自動採番(AUTOINCREMENT)の定義部分

    [DatabaseGenerated(DatabaseGeneratedOption.Identity)] // 値が追加されるときに自動生成
    [Key] // 主キー
    public int Id { get; set; }

レコードを追加する際、Idに0以外の数値をセットするとその数値がテーブルに反映される。ItemのIdのデフォルト値を-1にしていたら自動採番されずに困りました。ちなみに採番される開始番号は1~のようです。

感想

ItemとItemModelの関係とかを考えるとインターフェイスを噛ませるべきか?とか、DBアクセスしているのにエラー処理が書かれていないとか、入力項目にバリデーションがないとか、await/asyncにしているのにGUI側でいつでもボタンが押せるようになっているとか、Nameは重複しちゃだめだろうとか、フォームの使い方が直感的でないとか、色々と思うところがありますが、とりあえず動きを確認するサンプルコードと言うことでご了承ください。

コメント