WPF Entity FrameworkチュートリアルのVとVMを分離したい
はじめに
Microsoft Docsにあるチュートリアル: WPF .NET Core のチュートリアル

Viewの裏側、いわゆるコードビハインドにコードが書かれている。
コードビハインドに書かれているコードをViewModelに移し替えたい。
簡単かと思ったら、「Gridをビューモデル側から更新する」操作に手こずった・・・
【前段】まずは元記事の分析
XAML自体の知識が乏しいので、まずは元記事のコードを嚙み砕く
表に表示するリソースを定義する
画面(DataGrid)に表示するリソースは、xamlファイルの冒頭で定義される。
XAMLファイル冒頭のリソース定義
Windowのリソースに2つのCollectionViewSourceが定義されている。
-
categoryViewSourceはコードビハインドで定義される。 -
categoryProductsViewSourceは、categoryViewSourceのProductsコレクションをバインドする- こちらはソースを明示している。
<Window.Resources>
<CollectionViewSource x:Key="categoryViewSource"/>
<CollectionViewSource x:Key="categoryProductsViewSource"
Source="{Binding Products, Source={StaticResource categoryViewSource}}"/>
</Window.Resources>
Sourceを明示するか否かにより、Bindingの参照先が変わるらしい
特に何も指定しない場合、バインディング・ソースは、UI要素のDataContextプロパティに与えられたオブジェクトになる。
一方で、Bindingマークアップ拡張のSourceプロパティを設定することで、バインディング・ソースを明示的に指定することもできる。
(引用元:第5回 WPFの「データ・バインディング」を理解する)
コードビハインド(MainWindow.xaml.cs)
xamlファイル冒頭でx:Keyに割り当てられるcategoryViewSourceがどのように定義されているかみてみる。
// インスタンス変数を宣言
private CollectionViewSource categoryViewSource;
// コンストラクター内
// 変数を初期化
categoryViewSource = (CollectionViewSource)FindResource(nameof(categoryViewSource));
// 画面ロード時のイベント
// ソースプロパティにメモリ内のデータを割り当てる
categoryViewSource.Source = context.Categories.Local.ToObservableCollection();
DbSet に対して LINQ クエリを直接実行すると、常にクエリがデータベースに送信されますが、現在、DbSet. Local プロパティを使用してメモリ内にあるデータにアクセスできます。
(引用元:Docs Entity Framework Entity Framework 6 クエリ データ ローカル データ)
コンストラクター内で変数を初期化するときに、Application.FindResource(Object) メソッドを使ってハンドラーを取得している。
このメソッドはobject型を返すので、キャストが必要となる。
※ categoryViewSource = new CollectionViewSource();で初期化するとうまくいかない。
データグリッド
データを表示する表のこと
表の内容は(許可されていれば)ユーザーが書き換え可能
<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">
| プロパティ | 内容 |
|---|---|
Name |
コントロールを識別するための名前。今回はなくてもいい |
AutoGenerateColumns |
列を手動で追加する場合はfalseにする。(既定値はtrue) |
EnableRowVirtualization |
よくわからない、パフォーマンス向上のため? |
| ItemsSource | 冒頭で定義したリソースをここで使う |
| Margin | 見た目 |
| RowDetailsVisibilityMode | 詳細表示。選択時に可視化されるプロパティとなっているけど、よくわからない |
| RenderTransformOrigin | 描画原点をデフォルトの (0,0) から変更 |
AutoGenerateColumnsをTrueにしてみる
余計な列が出現する。

そのままさらに<DataGrid.Columns>を使って手動で列を追加すると、同じ列名の重複した列が出現する。カオス。

AutoGenerateColumnsをTrueにした状態でColumnsを手動で追加するケースは稀だと思う。
参考: Qiita - WPF DataGridを使ってみた
モデル
Productクラスと Categoryクラスにはvirtualで定義されたナビゲーションプロパティがある。
virtual修飾子を付けないで実行するとエラーになる

ナビゲーション プロパティ ゲッターを public および virtualとして宣言する必要があり、クラスを sealed (Visual Basic では NotOverridable) にしないようにする必要があります。
(引用元:Docs Entity Framework Entity Framework Core 作業の開始 WPF .NET Core のチュートリアル)
遅延読み込みとは、これらのプロパティにアクセスしようとしたときに、それらのプロパティの内容がデータベースから自動的に読み込まれることを意味します。
(引用元:Docs Entity Framework Entity Framework 6 モデルを作成する Code First を使用する ワークフロー 新しいデータベースの場合
)
1カテゴリーに対して、プロダクトは多の関係である。
カテゴリーのほうはProductsコレクションをnewしている。
newを無くすとうまくいかない。
public virtual Category Category { get; set; }
// 右辺に new がある
public virtual ICollection<Product> Products { get; private set; }
= new ObservableCollection<Product>();
//--------------------------------------------------------------------
// 個人的な好みで、宣言とインスタンス生成は分けたい
// 1. 宣言
public virtual ICollection<Product> Products { get; private set; }
public Category() // <---Categoryクラスのコンストラクター
{ // 2. コンストラクター内でインスタンス生成
this.Products = new ObservableCollection<Product>();
}
「クラス名 + Id」と命名すると、自動でキーとして認識してくれる。
Code First の規則の1つは、暗黙的なキープロパティです。Code First は、"Id" という名前のプロパティ、またはクラス名と "Id" の組み合わせ ("ブログ Id" など) を検索します。 このプロパティは、データベースの主キー列にマップされます。
(引用元:Docs Entity Framework Entity Framework 6 モデルを作成する Code First を使用する データの注釈)
WPF XAMLのバインディングを使う場合は、コレクションの型としてObservableCollectionを使う
WPF を使用してデータバインディングを実行する場合は、コレクションのプロパティに system.collections.objectmodel.observablecollection を使用することをお勧めします。これにより、wpf がコレクションに加えられた変更を追跡できるようになります。
(引用元:Docs Entity Framework Entity Framework 6 基礎 データ バインディング WPF)
Categoryクラスに紐づくProductコレクションは人が手入力するものではないので、コンストラクター内でインスタンスを生成する
public class Category
{
public virtual ObservableCollection<Product> Products { get; private set; } // 外部からはsetされない
public Category() // コンストラクター
{
this.Products = new ObservableCollection<Product>();
}
}
【本題】ViewModelに移行する
元コードがだいたい分かったので、コードビハインドに書かれている内容をViewModelに移し替える。
新たに2つのファイルをプロジェクトに追加する
| フォルダー | ファイル |
|---|---|
View |
TestWindow.xaml |
ViewModel |
TestWindowViewModel.cs |
コードビハインドは初期状態のままにする
この記事の目的に即して、コードビハインドには何も書かない
public partial class TestWindow : Window
{
// コンストラクターにメソッド1つだけ
public TestWindow()
{
InitializeComponent();
}
}
画面に表示するコレクションリソースをつくる
ViewModelの変更
CollectionViewSourceの代わりにObservableCollectionをつかう
// クラスのプライベート変数を宣言して
private CollectionViewSource categoryViewSource;
// コンストラクター内で初期化
categoryViewSource = (CollectionViewSource)FindResource(nameof(categoryViewSource));
↓ ↓ ↓
// クラスのプロパティを宣言して
public ObservableCollection<Category> CategoryViewSource { get; }
// コンストラクター内でデータベースの値をとってきて
_context.Database.EnsureCreated();
_context.Categories.Load();
// プロパティに代入
CategoryViewSource = new ();
CategoryViewSource = _context.Categories.Local.ToObservableCollection();
Viewの変更
<Window.Resources>
- <CollectionViewSource x:Key="categoryViewSource"/>
+ <CollectionViewSource x:Key="categoryViewSource" Source="{Binding CategoryViewSource}"/>
<CollectionViewSource x:Key="categoryProductsViewSource" Source="{Binding Products, Source={StaticResource categoryViewSource}}"/>
</Window.Resources>
ボタンクリックのコマンドをつくる
ここに一番時間がかかった
まず元コード
元コードは、いたってシンプル
ボタンが押されたら、コードビハインドにあるvoid Button_Clickメソッドが呼ばれる
<Button Content="Save" (中略) Click="Button_Click" />
ボタンが押されたら、データベースの内容を更新し、画面も更新する
private void Button_Click(object sender, RoutedEventArgs e)
{
// ユーザーが入力した表の変更をデータベースに保存
_context.SaveChanges();
// ビューの各DataGridを更新する
categoryDataGrid.Items.Refresh();
productsDataGrid.Items.Refresh();
}
MVVMに書き換える
新たに書き換えたコードは、ごちゃごちゃしている
2個あるGridコントロールを更新(Refresh)しなくてはいけないので、ボタンコマンドに2個パラメーターを与えている
<Button Content="Save" (中略) Command="{Binding Button_Click}">
<Button.CommandParameter>
<MultiBinding Converter="{StaticResource Converter}">
<Binding ElementName="productsDataGrid"/>
<Binding ElementName="categoryDataGrid"/>
</MultiBinding>
</Button.CommandParameter>
</Button>
マルチバインディングとコンバーターの考え方は知らなかった
以下参考にしたサイト
ビューモデルでボタンの動作を定義する
// クラス内でプロパティ宣言
public ReactiveCommand Button_Click { get; }
// コンストラクター内
// インスタンス生成
Button_Click = new ReactiveCommand();
// 動作定義
Button_Click.Subscribe(x => {
_context.SaveChanges(); // ここは元コードと同じ
// 各DataGridを更新(Rifresh)する
foreach (var xx in (Array)x)
((System.Windows.Controls.DataGrid)xx).Items.Refresh();
});
xはObject型なので、Arrayでキャストし、2つあるDataGridを漏れなく処理する
コンバーター
マルチバインディングしたコマンドパラメーターをどう扱うかは、Converterメソッドによって決められる
<!-- ビューモデルを使うので、Windowで定義 -->
<Window (中略)
xmlns:viewmodels="clr-namespace:DatabeseViewerWPF.ViewModels"
>
<!-- Windowのリソースにビューモデルのクラスを登録 -->
<Window.Resources>
(中略)
<viewmodels:MyMultiValueConverter x:Key="Converter"/>
</Window.Resources>
Converterの実装
マルチバインディングで流れてくる2つのGridをClone()してそのまま返している
public class MyMultiValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values.Clone();
}
// こっちは何も変えていない
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Windowが閉じられたときの動作
元コードでは、丁寧にデータベースコンテキストをDispposeして
かつ親のメソッドを呼び出している
protected override void OnClosing(CancelEventArgs e)
{
_context.Dispose();
base.OnClosing(e);
}
MVVMに書き換える場合、WindowのClosingイベントにコマンドを割り当てる。
古い記事だと、Windowのxmlns:iの内容が違うので注意する。
詳しく説明されている記事:System.Windows.Interactivity.dll から Xaml.Behaviors.Wpf へ
<Window ...(略)
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
>
<i:Interaction.Triggers>
<i:EventTrigger EventName="Closing">
<i:InvokeCommandAction Command="{Binding Closing_Command}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
...
</Window>
// クラスのプロパティ
public ReactiveCommand Closing_Command { get; }
// コンストラクター内でインスタンス生成
Closing_Command = new ReactiveCommand();
// さらに動作を割り当て
Closing_Command.Subscribe(() =>
{
_context.Dispose();
Debug.WriteLine("ウィンドウを閉じます");
});
ウィンドウを閉じたときに、ちゃんとコマンドの動作に入っていることがわかる

おわりに
ケースバイケースでコードビハインドも使うべし
Discussion