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は重複しちゃだめだろうとか、フォームの使い方が直感的でないとか、色々と思うところがありますが、とりあえず動きを確認するサンプルコードと言うことでご了承ください。
コメント