MVVM-クイックガイド
コードを整理するための整然とした、おそらく最も再利用可能な方法は、「MVVM」パターンを使用することです。ザ・Model, View, ViewModel (MVVM pattern) 保守可能、テスト可能、拡張可能なアプリケーションを作成するためにコードを編成および構造化する方法をガイドすることがすべてです。
Model −データを保持するだけで、ビジネスロジックとは何の関係もありません。
ViewModel −モデルとビューの間のリンク/接続として機能し、見栄えを良くします。
View −フォーマットされたデータを保持し、基本的にすべてをモデルに委任します。
個別のプレゼンテーション
アプリケーションロジックをコードビハインドまたはXAMLに配置することによって引き起こされる問題を回避するには、分離表示と呼ばれる手法を使用するのが最善です。これを回避しようとしています。XAMLとコードビハインドがあり、ユーザーインターフェイスオブジェクトを直接操作するために最低限必要なものがあります。ユーザーインターフェイスクラスには、左側の次の図に示すように、複雑な対話動作、アプリケーションロジック、およびその他すべてのコードも含まれています。
個別の表示により、ユーザーインターフェイスクラスははるかに単純になります。もちろんXAMLもありますが、背後にあるコードは実用的ではありません。
アプリケーションロジックは、モデルと呼ばれることが多い別のクラスに属しています。
ただし、これがすべてではありません。ここで停止すると、データバインディングの狂気の道をたどる、非常に一般的な間違いを繰り返す可能性があります。
多くの開発者は、データバインディングを使用して、XAMLの要素をモデルのプロパティに直接接続しようとしています。
今ではこれで問題ない場合もありますが、そうでない場合もあります。問題は、モデルが完全にアプリケーションの動作に関係しており、ユーザーがアプリケーションをどのように操作するかに関係していないことです。
多くの場合、データを表示する方法は、データが内部で構造化されている方法とは多少異なります。
さらに、ほとんどのユーザーインターフェイスには、アプリケーションモデルに属さない状態があります。
たとえば、ユーザーインターフェースでドラッグアンドドロップを使用する場合、ドラッグされているアイテムが現在どこにあるか、可能なドロップターゲット上を移動するときに外観がどのように変化するか、それらのドロップターゲットがどのように変化するかなどを追跡する必要があります。アイテムがそれらの上にドラッグされると変化します。
この種の状態は驚くほど複雑になる可能性があり、徹底的にテストする必要があります。
実際には、通常、ユーザーインターフェイスとモデルの間に他のクラスを配置する必要があります。これには2つの重要な役割があります。
まず、アプリケーションモデルを特定のユーザーインターフェイスビューに適合させます。
第二に、それは重要な相互作用ロジックが存在する場所です。つまり、ユーザーインターフェイスを希望どおりに動作させるために必要なコードを意味します。
MVVMパターンは最終的にはMVCパターンの最新の構造であるため、ドメインロジックとプレゼンテーション層を明確に分離するという主な目標は同じです。MVVMパターンの長所と短所のいくつかを次に示します。
主な利点は、分離とそれを使用することで得られる効率を達成するだけでなく、ビューとモデルを真に分離できることです。つまり、実際には、モデルを変更する必要がある場合、ビューを必要とせずに簡単に変更でき、その逆も可能です。
MVVMの適用から流出する3つの重要な重要事項は次のとおりです。
保守性
さまざまな種類のコードを明確に分離することで、これらのよりきめ細かく焦点を絞った部分の1つまたは複数に簡単にアクセスし、心配することなく変更を加えることができます。
つまり、アジャイルを維持し、新しいリリースにすばやく移行し続けることができます。
テスト容易性
MVVMを使用すると、コードの各部分がより細かくなり、正しく実装されている場合、外部および内部の依存関係は、テストするコアロジックを持つ部分とは別のコードになります。
これにより、コアロジックに対する単体テストの記述がはるかに簡単になります。
書かれたときに正しく機能し、メンテナンスで状況が変わっても機能し続けることを確認してください。
拡張性
明確な分離境界とよりきめ細かいコードのために、保守性と重複することがあります。
これらのパーツのいずれかをより再利用可能にする可能性が高くなります。
また、同様のことを行う新しいコードをアーキテクチャの適切な場所に置き換えたり、追加したりすることもできます。
MVVMパターンの明らかな目的は、コードビハインドのビジネスロジックの量を減らすビューの抽象化です。ただし、以下は他のいくつかの確かな利点です-
- ViewModelは、コードビハインドコードやイベント駆動型コードよりも単体テストが簡単です。
- 厄介なUIの自動化や操作なしでテストできます。
- プレゼンテーション層とロジックは疎結合です。
短所
- 一部の人々は、単純なUIの場合、MVVMはやり過ぎになる可能性があると考えています。
- 同様に、より大きなケースでは、ViewModelを設計するのが難しい場合があります。
- 複雑なデータバインディングがある場合、デバッグは少し難しいでしょう。
MVVMパターンは、Model、View、ViewModelの3つの部分で構成されています。最初の開発者のほとんどは、Model、View、ViewModelに何を含めるべきか、何を含めるべきでないか、そして各部分の責任について少し混乱しています。
この章では、MVVMパターンの各部分の責任を学習して、どのような種類のコードがどこに行くのかを明確に理解できるようにします。次の図に示すように、MVVMは実際にはクライアント側の階層化アーキテクチャです。
プレゼンテーション層はビューで構成されています。
論理レイヤーはビューモデルです。
プレゼンテーション層は、モデルオブジェクトの組み合わせです。
それらを生成および永続化するクライアントサービスは、2層アプリケーションでの直接アクセス、またはアプリケーションへのサービス呼び出しを介してアクセスします。
クライアントサービスは公式にはMVVMパターンの一部ではありませんが、MVVMで使用されることが多く、さらなる分離を実現し、コードの重複を回避します。
モデルの責任
一般に、モデルは最も理解しやすいモデルです。これは、アプリケーションのビューをサポートするクライアント側のデータモデルです。
これは、メモリにデータを含めるためのプロパティといくつかの変数を持つオブジェクトで構成されています。
これらのプロパティの一部は、他のモデルオブジェクトを参照し、全体としてモデルオブジェクトであるオブジェクトグラフを作成する場合があります。
モデルオブジェクトは、プロパティ変更通知を発生させる必要があります。これは、WPFではデータバインディングを意味します。
最後の責任はオプションの検証ですが、INotifyDataErrorInfo / IDataErrorInfoなどのインターフェイスを介してWPFデータバインディング検証機能を使用することにより、モデルオブジェクトに検証情報を埋め込むことができます。
責任を見る
ビューの主な目的と責任は、ユーザーが画面に表示するものの構造を定義することです。構造には、静的パーツと動的パーツを含めることができます。
静的部分は、ビューを構成するコントロールとコントロールのレイアウトを定義するXAML階層です。
動的部分は、ビューの一部として定義されるアニメーションまたは状態変化のようなものです。
MVVMの主な目標は、ビューの背後にコードがないようにすることです。
背後にコードがないことは不可能です。ビューでは、少なくともコンストラクターとコンポーネントを初期化するための呼び出しが必要です。
イベント処理、アクション、およびデータ操作のロジックコードは、Viewの背後にあるコードに含めるべきではないという考え方です。
UI要素への参照が本質的にビューコードである必要があるコードの背後にあるコードに含まれなければならない他の種類のコードもあります。
ViewModelの責任
ViewModelはMVVMアプリケーションの要点です。ViewModelの主な役割は、ビューにデータを提供して、ビューがそのデータを画面に表示できるようにすることです。
また、ユーザーがデータを操作したり、データを変更したりすることもできます。
ViewModelのもう1つの重要な役割は、ビューの対話ロジックをカプセル化することですが、アプリケーションのすべてのロジックをViewModelに入れる必要があるという意味ではありません。
ユーザーまたはビューの変更に基づいて正しいことを実行するために、呼び出しの適切なシーケンスを処理できる必要があります。
ViewModelは、別のビューに移動するタイミングを決定するなど、ナビゲーションロジックも管理する必要があります。
この章では、単純な入力画面にMVVMパターンを使用する方法と、既に慣れているWPFアプリケーションについて学習します。
MVVMアプローチを使用する簡単な例を見てみましょう。
Step 1 −新しいWPFアプリケーションプロジェクトMVVMDemoを作成します。
Step 2 − 3つのフォルダー(Model、ViewModel、およびViews)をプロジェクトに追加します。
Step 3 − ModelフォルダーにStudentModelクラスを追加し、そのクラスに以下のコードを貼り付けます
using System.ComponentModel;
namespace MVVMDemo.Model {
public class StudentModel {}
public class Student : INotifyPropertyChanged {
private string firstName;
private string lastName;
public string FirstName {
get {
return firstName;
}
set {
if (firstName != value) {
firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
}
public string LastName {
get {return lastName; }
set {
if (lastName != value) {
lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
}
public string FullName {
get {
return firstName + " " + lastName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
Step 4 −別のStudentViewModelクラスをViewModelフォルダーに追加し、次のコードを貼り付けます。
using MVVMDemo.Model;
using System.Collections.ObjectModel;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
}
}
Step 5 − [ビュー]フォルダーを右クリックし、[追加]> [新しいアイテム...]を選択して、新しいユーザーコントロール(WPF)を追加します。
Step 6− [追加]ボタンをクリックします。これで、XAMLファイルが表示されます。さまざまなUI要素を含むStudentView.xamlファイルに次のコードを追加します。
<UserControl x:Class = "MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Step 7 −次のコードを使用して、StudentViewをMainPage.xamlファイルに追加します。
<Window x:Class = "MVVMDemo.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:MVVMDemo"
xmlns:views = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Grid>
<views:StudentView x:Name = "StudentViewControl" Loaded = "StudentViewControl_Loaded"/>
</Grid>
</Window>
Step 8 −これはMainPage.xaml.csファイルのLoadedイベントの実装であり、ViewModelからビューを更新します。
using System.Windows;
namespace MVVMDemo {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
}
private void StudentViewControl_Loaded(object sender, RoutedEventArgs e) {
MVVMDemo.ViewModel.StudentViewModel studentViewModelObject =
new MVVMDemo.ViewModel.StudentViewModel();
studentViewModelObject.LoadStudents();
StudentViewControl.DataContext = studentViewModelObject;
}
}
}
Step 9 −上記のコードをコンパイルして実行すると、メインウィンドウに次の出力が表示されます。
理解を深めるために、上記の例を段階的に実行することをお勧めします。
この章では、ビューをViewModelに接続するさまざまな方法について説明します。最初に、XAMLで宣言できるViewfirst構造を見てみましょう。前の章の例で見たように、メインウィンドウからのビューを接続しました。次に、ビューを接続する他の方法を見ていきます。
この章でも同じ例を使用します。以下は、同じModelクラスの実装です。
using System.ComponentModel;
namespace MVVMDemo.Model {
public class StudentModel {}
public class Student : INotifyPropertyChanged {
private string firstName;
private string lastName;
public string FirstName {
get { return firstName; }
set {
if (firstName != value) {
firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
}
public string LastName {
get { return lastName; }
set {
if (lastName != value) {
lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
}
public string FullName {
get {
return firstName + " " + lastName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
これがViewModelクラスの実装です。今回は、LoadStudentsメソッドがデフォルトのコンストラクターで呼び出されます。
using MVVMDemo.Model;
using System.Collections.ObjectModel;
namespace MVVMDemo.ViewModel{
public class StudentViewModel {
public StudentViewModel() {
LoadStudents();
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
}
}
ビューがウィンドウ、ユーザーコントロール、ページのいずれであっても、パーサーは通常、上から下、左から右に機能します。要素に遭遇すると、各要素のデフォルトコンストラクターを呼び出します。ビューを作成するには2つの方法があります。あなたはそれらにどれでも使うことができます。
- XAMLで最初の構築を表示
- コードビハインドで最初の構造を表示
XAMLで最初の構築を表示
1つの方法は、次のコードに示すように、DataContextプロパティのセッターにネストされた要素としてViewModelを追加することです。
<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>
これが完全なViewXAMLファイルです。
<UserControl x:Class="MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
コードビハインドで最初の構造を表示
もう1つの方法は、ビューの最初の構築を取得できることです。インスタンスを使用してDataContextプロパティを設定することにより、ビューの背後にあるコードでビューモデルを自分で構築するだけです。
通常、DataContextプロパティはビューのコンストラクターメソッドで設定されますが、ビューのLoadイベントが発生するまで構築を延期することもできます。
using System.Windows.Controls;
namespace MVVMDemo.Views {
/// <summary>
/// Interaction logic for StudentView.xaml
/// </summary>
public partial class StudentView : UserControl {
public StudentView() {
InitializeComponent();
this.DataContext = new MVVMDemo.ViewModel.StudentViewModel();
}
}
}
XAMLではなくコードビハインドでビューモデルを構築する理由の1つは、ビューモデルコンストラクターがパラメーターを受け取ることですが、XAML解析では、既定のコンストラクターで定義されている場合にのみ要素を構築できます。
この場合、ViewのXAMLファイルは次のコードのようになります。
<UserControl x:Class = "MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300"
d:DesignWidth = "300">
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal"<
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
MainWindow.XAMLファイルに示されているように、このビューをMainWindowで宣言できます。
<Window x:Class = "MVVMDemo.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:MVVMDemo"
xmlns:views = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Grid>
<views:StudentView x:Name = "StudentViewControl"/>
</Grid>
</Window>
上記のコードをコンパイルして実行すると、メインウィンドウに次の出力が表示されます。
理解を深めるために、上記の例を段階的に実行することをお勧めします。
この章では、ViewModelを接続する方法について説明します。これは、Viewの最初の構造について説明した最後の章の続きです。さて、最初の構造の次の形式はmeta-pattern として知られています ViewModelLocator。これは疑似パターンであり、MVVMパターンの上に重ねられます。
MVVMでは、各ビューをそのViewModelに接続する必要があります。
ViewModelLocatorは、コードを一元化し、ビューをさらに分離するためのシンプルなアプローチです。
これは、ViewModelタイプとその構築方法について明示的に知る必要がないことを意味します。
ViewModelLocatorを使用するにはさまざまなアプローチがありますが、ここでは、PRISMフレームワークの一部であるものに最も類似したものを使用します。
ViewModelLocatorは、ViewModelをViewに接続するプロセスを自動化する、View Firstの構築を行うための、標準的で一貫性のある宣言型の疎結合の方法を提供します。次の図は、ViewModelLocatorの高レベルのプロセスを表しています。
Step 1 −どのビュータイプが構築されているかを把握します。
Step 2 −その特定のビュータイプのViewModelを特定します。
Step 3 −そのViewModelを構築します。
Step 4 − ViewsDataContextをViewModelに設定します。
基本的な概念を理解するために、前の章から同じ例を続けて、ViewModelLocatorの簡単な例を見てみましょう。StudentView.xamlファイルを見ると、ViewModelが静的に接続されていることがわかります。
次のプログラムに示すように、これらのXAMLコードにコメントを付けると、コードビハインドからコードも削除されます。
<UserControl x:Class = "MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<!--<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>-->
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
次に、新しいフォルダーVMLを作成し、次のコードに示すように、単一のアタッチされたプロパティ(依存関係プロパティ)AutoHookedUpViewModelを含む新しいパブリッククラスViewModelLocatorを追加しましょう。
public static bool GetAutoHookedUpViewModel(DependencyObject obj) {
return (bool)obj.GetValue(AutoHookedUpViewModelProperty);
}
public static void SetAutoHookedUpViewModel(DependencyObject obj, bool value) {
obj.SetValue(AutoHookedUpViewModelProperty, value);
}
// Using a DependencyProperty as the backing store for AutoHookedUpViewModel.
//This enables animation, styling, binding, etc...
public static readonly DependencyProperty AutoHookedUpViewModelProperty =
DependencyProperty.RegisterAttached("AutoHookedUpViewModel",
typeof(bool), typeof(ViewModelLocator), new PropertyMetadata(false,
AutoHookedUpViewModelChanged));
これで、基本的なアタッチプロパティの定義を確認できます。プロパティに動作を追加するには、ViewModel forViewを接続する自動プロセスを含むこのプロパティの変更されたイベントハンドラーを追加する必要があります。これを行うためのコードは次のとおりです-
private static void AutoHookedUpViewModelChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
if (DesignerProperties.GetIsInDesignMode(d)) return;
var viewType = d.GetType();
string str = viewType.FullName;
str = str.Replace(".Views.", ".ViewModel.");
var viewTypeName = str;
var viewModelTypeName = viewTypeName + "Model";
var viewModelType = Type.GetType(viewModelTypeName);
var viewModel = Activator.CreateInstance(viewModelType);
((FrameworkElement)d).DataContext = viewModel;
}
以下は、ViewModelLocatorクラスの完全な実装です。
using System;
using System.ComponentModel;
using System.Windows;
namespace MVVMDemo.VML {
public static class ViewModelLocator {
public static bool GetAutoHookedUpViewModel(DependencyObject obj) {
return (bool)obj.GetValue(AutoHookedUpViewModelProperty);
}
public static void SetAutoHookedUpViewModel(DependencyObject obj, bool value) {
obj.SetValue(AutoHookedUpViewModelProperty, value);
}
// Using a DependencyProperty as the backing store for AutoHookedUpViewModel.
//This enables animation, styling, binding, etc...
public static readonly DependencyProperty AutoHookedUpViewModelProperty =
DependencyProperty.RegisterAttached("AutoHookedUpViewModel",
typeof(bool), typeof(ViewModelLocator), new
PropertyMetadata(false, AutoHookedUpViewModelChanged));
private static void AutoHookedUpViewModelChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
if (DesignerProperties.GetIsInDesignMode(d)) return;
var viewType = d.GetType();
string str = viewType.FullName;
str = str.Replace(".Views.", ".ViewModel.");
var viewTypeName = str;
var viewModelTypeName = viewTypeName + "Model";
var viewModelType = Type.GetType(viewModelTypeName);
var viewModel = Activator.CreateInstance(viewModelType);
((FrameworkElement)d).DataContext = viewModel;
}
}
}
最初に行うことは、名前空間を追加して、プロジェクトのルートでそのViewModelLocatorタイプにアクセスできるようにすることです。次に、ビュータイプであるroute要素で、AutoHookedUpViewModelプロパティを追加し、trueに設定します。
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
これがStudentView.xamlファイルの完全な実装です。
<UserControl x:Class = "MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<!--<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>-->
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
上記のコードをコンパイルして実行すると、ViewModelLocatorがその特定のビューのViewModelを接続していることがわかります。
これについて注意すべき重要な点は、ビューがViewModelのタイプや、その構築方法に結合されなくなったことです。これはすべて、ViewModelLocator内の中央の場所に移動されました。
この章では、データバインディングがMVVMパターンをサポートする方法を学習します。データバインディングは、MVVMをMVCやMVPなどの他のUI分離パターンと区別する重要な機能です。
データバインディングの場合、ビューまたはUI要素のセットを構築する必要があります。次に、バインディングが指す他のオブジェクトが必要です。
ビューのUI要素は、ViewModelによって公開されるプロパティにバインドされます。
最初にビューについて説明したように、ViewとViewModelが構築される順序は状況によって異なります。
ViewとViewModelが構築され、ViewのDataContextがViewModelに設定されます。
バインディングは、ViewとViewModelの間でデータをやり取りするためのOneWayまたはTwoWayデータバインディングのいずれかです。
同じ例のデータバインディングを見てみましょう。以下はStudentViewのXAMLコードです。
<UserControl x:Class = "MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<!--<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>-->
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
上記のXAMLコードを見ると、ItemsControlがViewModelによって公開されているStudentsコレクションにバインドされていることがわかります。
Studentモデルのプロパティにも独自の個別のバインディングがあり、これらはTextboxesとTextBlockにバインドされていることもわかります。
ビューの全体的なDataContextがViewModelに設定されているため、ItemsControlのItemSourceはStudentsプロパティにバインドできます。
ここでのプロパティの個々のバインディングもDataContextバインディングですが、ItemSourceの動作方法のため、ViewModel自体に対してバインディングしていません。
アイテムソースがそのコレクションにバインドされると、レンダリング時に各アイテムのコンテナがレンダリングされ、そのコンテナのDataContextがアイテムに設定されます。したがって、行内の各テキストボックスとテキストブロックの全体的なDataContextは、コレクション内の個々のStudentになります。また、TextBoxのこれらのバインディングはTwoWayデータバインディングであり、TextBlockの場合はTextBlockを編集できないため、OneWayデータバインディングであることがわかります。
このアプリケーションを再度実行すると、次の出力が表示されます。
ここで、最初の行の2番目のテキストボックスのテキストをAllainからUpstonに変更し、Tabキーを押してフォーカスを失います。TextBlockテキストも更新されていることがわかります。
これは、TextBoxのバインディングがTwoWayに設定されており、モデルも更新され、モデルから再度TextBlockが更新されるためです。
テンプレートは、コントロールの全体的な外観と外観を記述します。コントロールごとに、そのコントロールに外観を与えるデフォルトのテンプレートが関連付けられています。WPFアプリケーションでは、コントロールの視覚的な動作と視覚的な外観をカスタマイズするときに、独自のテンプレートを簡単に作成できます。ロジックとテンプレート間の接続は、データバインディングによって実現できます。
MVVMには、ViewModelの最初の構築として知られている別の主要な形式があります。
ViewModelの最初の構築アプローチは、WPFの暗黙的なデータテンプレートの機能を活用します。
暗黙的なデータテンプレートは、データバインディングを使用する要素について、現在のリソースディクショナリから適切なテンプレートを自動的に選択できます。これは、データバインディングによってレンダリングされるデータオブジェクトのタイプに基づいて行われます。まず、データオブジェクトにバインドしている要素が必要です。
最初にデータテンプレート、特に暗黙的なデータテンプレートを利用してビューモデルを実行する方法を理解する簡単な例をもう一度見てみましょう。これがStudentViewModelクラスの実装です。
using MVVMDemo.Model;
using System.Collections.ObjectModel;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public StudentViewModel() {
LoadStudents();
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
}
}
上記のViewModelが変更されていないことがわかります。前の章と同じ例を続けます。このViewModelクラスは、Studentsコレクションプロパティを公開し、構築時にそれを設定するだけです。StudentView.xamlファイルに移動し、既存の実装を削除して、[リソース]セクションでデータテンプレートを定義しましょう。
<UserControl.Resources>
<DataTemplate x:Key = "studentsTemplate">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
次に、リストボックスを追加し、次のコードに示すように、そのリストボックスをStudentsプロパティにデータバインドします。
<ListBox ItemsSource = "{Binding Students}" ItemTemplate = "{StaticResource studentsTemplate}"/>
リソースセクションでは、DataTemplateにstudentsTemplateのキーがあり、実際にそのテンプレートを使用するには、ListBoxのItemTemplateプロパティを使用する必要があります。これで、リストボックスに特定のテンプレートを使用してそれらの生徒をレンダリングするように指示していることがわかります。以下は、StudentView.xamlファイルの完全な実装です。
<UserControl x:Class = "MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate x:Key = "studentsTemplate">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ListBox
ItemsSource = "{Binding Students}"
ItemTemplate = "{StaticResource studentsTemplate}"/>
</Grid>
</UserControl>
上記のコードをコンパイルして実行すると、次のウィンドウが表示されます。このウィンドウには1つのリストボックスが含まれています。各ListBoxItemには、TextBlockボックスとTextボックスに表示されるStudentクラスのオブジェクトデータが含まれています。
これを暗黙的なテンプレートにするには、次のコードに示すように、リストボックスからItemTemplateプロパティを削除し、テンプレート定義にDataTypeプロパティを追加する必要があります。
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ListBox ItemsSource = "{Binding Students}"/>
</Grid>
DataTemplateでは、x:Typeマークアップ拡張機能は非常に重要であり、XAMLの演算子のタイプと同様です。したがって、基本的には、MVVMDemo.Model名前空間にあるStudentデータ型を指す必要があります。以下は、更新された完全なXAMLファイルです。
<UserControl x:Class="MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:data = "clr-namespace:MVVMDemo.Model"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ListBox ItemsSource = "{Binding Students}"/>
</Grid>
</UserControl>
このアプリケーションを再度実行すると、適切なDataTemplateを見つけることによってレンダリングされるオブジェクトのタイプが自動的にマッピングされるため、データテンプレートを使用したStudentsの同じレンダリングが引き続き得られます。
理解を深めるために、上記の例を段階的に実行することをお勧めします。
この章では、MVVMアプリケーションに対話機能を追加する方法と、ロジックをクリーンに呼び出す方法を学習します。また、これらすべてが、MVVMパターンの中心である疎結合と適切な構造化を維持することによって行われていることがわかります。これらすべてを理解するために、まずコマンドについて学びましょう。
コマンドによるView / ViewModel通信
コマンドパターンは十分に文書化されており、数十年にわたってデザインパターンを頻繁に使用しています。このパターンには、呼び出し側と受信側の2つの主要なアクターがあります。
呼び出し元
呼び出し元は、いくつかの命令型ロジックを実行できるコードの一部です。
通常、これは、UIフレームワークのコンテキストでユーザーが操作するUI要素です。
これは、アプリケーションの他の場所にあるロジックコードの別のチャンクである可能性があります。
レシーバー
レシーバーは、呼び出し元が起動したときに実行することを目的としたロジックです。
MVVMのコンテキストでは、レシーバーは通常、呼び出す必要のあるViewModelのメソッドです。
これら2つの間に、障害物レイヤーがあります。これは、呼び出し側と受信側がお互いを明示的に知る必要がないことを意味します。これは通常、呼び出し側に公開されるインターフェイスの抽象化として表され、そのインターフェイスの具体的な実装は受信者を呼び出すことができます。
コマンドと、それらを使用してViewとViewModelの間で通信する方法を学習する簡単な例を見てみましょう。この章では、前の章と同じ例を続けます。
StudentView.xamlファイルには、ViewModelから学生データをフックするListBoxがあります。次に、リストボックスから生徒を削除するためのボタンを追加しましょう。
重要なことは、ボタンのコマンドはICommandに接続するためのコマンドプロパティを持っているため、非常に簡単に操作できることです。
したがって、次のコードに示すように、ICommandを持ち、ボタンのコマンドプロパティからそれにバインドするViewModelのプロパティを公開できます。
<Button Content = "Delete"
Command = "{Binding DeleteCommand}"
HorizontalAlignment = "Left"
VerticalAlignment = "Top"
Width = "75" />
プロジェクトに新しいクラスを追加してみましょう。これにより、ICommandインターフェイスが実装されます。以下は、ICommandインターフェイスの実装です。
using System;
using System.Windows.Input;
namespace MVVMDemo {
public class MyICommand : ICommand {
Action _TargetExecuteMethod;
Func<bool> _TargetCanExecuteMethod;
public MyICommand(Action executeMethod) {
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action executeMethod, Func<bool> canExecuteMethod){
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged() {
CanExecuteChanged(this, EventArgs.Empty);
}
bool ICommand.CanExecute(object parameter) {
if (_TargetCanExecuteMethod != null) {
return _TargetCanExecuteMethod();
}
if (_TargetExecuteMethod != null) {
return true;
}
return false;
}
// Beware - should use weak references if command instance lifetime
is longer than lifetime of UI objects that get hooked up to command
// Prism commands solve this in their implementation
public event EventHandler CanExecuteChanged = delegate { };
void ICommand.Execute(object parameter) {
if (_TargetExecuteMethod != null) {
_TargetExecuteMethod();
}
}
}
}
ご覧のとおり、これはICommandの単純な委任実装であり、executeMethod用とcanExecuteMethod用の2つの委任があり、構築時に渡すことができます。
上記の実装では、2つのオーバーロードされたコンストラクターがあります。1つはexecuteMethodのみ用で、もう1つはexecuteMethodとI canExecuteMethodの両方用です。
StudentViewModelクラスにMyICommandタイプのプロパティを追加しましょう。次に、StudentViewModelでインスタンスを作成する必要があります。2つのパラメーターを受け取るMyICommandのオーバーロードされたコンストラクターを使用します。
public MyICommand DeleteCommand { get; set;}
public StudentViewModel() {
LoadStudents();
DeleteCommand = new MyICommand(OnDelete, CanDelete);
}
次に、OnDeleteメソッドとCanDeleteメソッドの実装を追加します。
private void OnDelete() {
Students.Remove(SelectedStudent);
}
private bool CanDelete() {
return SelectedStudent != null;
}
また、ユーザーがリストボックスから選択したアイテムを削除できるように、新しいSelectedStudentを追加する必要があります。
private Student _selectedStudent;
public Student SelectedStudent {
get {
return _selectedStudent;
}
set {
_selectedStudent = value;
DeleteCommand.RaiseCanExecuteChanged();
}
}
以下は、ViewModelクラスの完全な実装です。
using MVVMDemo.Model;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public MyICommand DeleteCommand { get; set;}
public StudentViewModel() {
LoadStudents();
DeleteCommand = new MyICommand(OnDelete, CanDelete);
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
private Student _selectedStudent;
public Student SelectedStudent {
get {
return _selectedStudent;
}
set {
_selectedStudent = value;
DeleteCommand.RaiseCanExecuteChanged();
}
}
private void OnDelete() {
Students.Remove(SelectedStudent);
}
private bool CanDelete() {
return SelectedStudent != null;
}
}
}
StudentView.xamlで、SelectStudentプロパティにバインドするSelectedItemプロパティをListBoxに追加する必要があります。
<ListBox ItemsSource = "{Binding Students}" SelectedItem = "{Binding SelectedStudent}"/>
以下は完全なxamlファイルです。
<UserControl x:Class = "MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:data = "clr-namespace:MVVMDemo.Model"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<StackPanel Orientation = "Horizontal">
<ListBox ItemsSource = "{Binding Students}"
SelectedItem = "{Binding SelectedStudent}"/>
<Button Content = "Delete"
Command = "{Binding DeleteCommand}"
HorizontalAlignment = "Left"
VerticalAlignment = "Top"
Width = "75" />
</StackPanel>
</Grid>
</UserControl>
上記のコードをコンパイルして実行すると、次のウィンドウが表示されます。
削除ボタンが無効になっていることがわかります。いずれかの項目を選択すると有効になります。
アイテムを選択して削除を押したとき。選択したアイテムリストが削除され、削除ボタンが再び無効になっていることがわかります。
理解を深めるために、上記の例を段階的に実行することをお勧めします。
MVVMアプリケーションを構築する場合、通常、情報の複雑な画面を親ビューと子ビューのセットに分解します。子ビューは、パネルまたはコンテナーコントロールの親ビュー内に含まれ、使用の階層を形成します。
複雑なビューを分解した後、独自のXAMLファイルに分離する子コンテンツのすべての部分が必ずしもMVVMビューである必要があるという意味ではありません。
コンテンツのチャンクは、画面に何かをレンダリングするための構造を提供するだけであり、そのコンテンツに対するユーザーによる入力や操作をサポートしていません。
個別のViewModelは必要ないかもしれませんが、親のViewModelによって公開されたプロパティに基づいてレンダリングするチャンクXAMLである可能性があります。
最後に、ViewsとViewModelsの階層がある場合、親ViewModelを通信のハブにして、各子ViewModelを他の子ViewModelsおよびその親から可能な限り分離したままにすることができます。
異なるビュー間の単純な階層を定義する例を見てみましょう。新しいWPFアプリケーションプロジェクトを作成しますMVVMHierarchiesDemo
Step 1 − 3つのフォルダー(Model、ViewModel、およびViews)をプロジェクトに追加します。
Step 2 −次の図に示すように、ModelフォルダーにCustomerクラスとOrderクラスを追加し、ViewsフォルダーにCustomerListViewとOrderViewを追加し、ViewModelフォルダーにCustomerListViewModelとOrderViewModelを追加します。
Step 3−CustomerListViewとOrderViewの両方にテキストブロックを追加します。これがCustomerListView.xamlファイルです。
<UserControl x:Class="MVVMHierarchiesDemo.Views.CustomerListView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<TextBlock Text = "Customer List View"/>
</Grid>
</UserControl>
以下は、OrderView.xamlファイルです。
<UserControl x:Class = "MVVMHierarchiesDemo.Views.OrderView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d ="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views" mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<TextBlock Text = "Order View"/>
</Grid>
</UserControl>
ここで、これらのビューをホストするための何かが必要です。これは単純なアプリケーションであるため、MainWindowにそのための適切な場所が必要です。ビューを配置してナビゲーション方式で切り替えることができるコンテナコントロールが必要です。この目的のために、MainWindow.xamlファイルにContentControlを追加する必要があり、そのcontentプロパティを使用して、それをViewModel参照にバインドします。
次に、リソースディクショナリの各ビューのデータテンプレートを定義します。以下はMainWindow.xamlファイルです。各データテンプレートがデータ型(ViewModel型)を対応するビューにマップする方法に注意してください。
<Window x:Class = "MVVMHierarchiesDemo.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:MVVMHierarchiesDemo"
xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views"
xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
<views:CustomerListView/>
</DataTemplate>
<DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
<views:OrderView/>
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content = "{Binding CurrentView}"/>
</Grid>
</Window>
現在のビューモデルがCustomerListViewModelのインスタンスに設定されている場合は常に、ViewModelが接続された状態でCustomerListViewをレンダリングします。これはOrderViewModelであり、OrderViewなどをレンダリングします。
ここで、CurrentViewModelプロパティと、プロパティ内でViewModelの現在の参照を切り替えることができるいくつかのロジックとコマンドを持つViewModelが必要です。
MainWindowViewModelと呼ばれるこのMainWindowのViewModelを作成しましょう。XAMLからViewModelのインスタンスを作成し、それを使用してウィンドウのDataContextプロパティを設定するだけです。このために、ViewModelのINotifyPropertyChangedの実装をカプセル化する基本クラスを作成する必要があります。
このクラスの背後にある主なアイデアは、INotifyPropertyChanged実装をカプセル化し、派生クラスにヘルパーメソッドを提供して、適切な通知を簡単にトリガーできるようにすることです。以下は、BindableBaseクラスの実装です。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo {
class BindableBase : INotifyPropertyChanged {
protected virtual void SetProperty<T>(ref T member, T val,
[CallerMemberName] string propertyName = null) {
if (object.Equals(member, val)) return;
member = val;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(string propertyName) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
}
次に、CurrentViewModelプロパティを使用して実際にビューの切り替えを開始します。このプロパティの設定を駆動する方法が必要です。そして、エンドユーザーが顧客リストまたは注文ビューに移動するように命令できるようにします。まず、ICommandインターフェイスを実装する新しいクラスをプロジェクトに追加します。以下は、ICommandインターフェイスの実装です。
using System;
using System.Windows.Input;
namespace MVVMHierarchiesDemo {
public class MyICommand<T> : ICommand {
Action<T> _TargetExecuteMethod;
Func<T, bool> _TargetCanExecuteMethod;
public MyICommand(Action<T> executeMethod) {
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod) {
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged() {
CanExecuteChanged(this, EventArgs.Empty);
}
#region ICommand Members
bool ICommand.CanExecute(object parameter) {
if (_TargetCanExecuteMethod != null) {
T tparm = (T)parameter;
return _TargetCanExecuteMethod(tparm);
}
if (_TargetExecuteMethod != null) {
return true;
}
return false;
}
// Beware - should use weak references if command instance lifetime is
longer than lifetime of UI objects that get hooked up to command
// Prism commands solve this in their implementation
public event EventHandler CanExecuteChanged = delegate { };
void ICommand.Execute(object parameter) {
if (_TargetExecuteMethod != null) {
_TargetExecuteMethod((T)parameter);
}
}
#endregion
}
}
次に、これらへのトップレベルのナビゲーションをViewModelに設定する必要があり、その切り替えのロジックはMainWindowViewModel内に属する必要があります。このために、文字列の宛先を取得してCurrentViewModelプロパティを返すnavigateで呼び出されるメソッドを使用します。
private void OnNav(string destination) {
switch (destination) {
case "orders":
CurrentViewModel = orderViewModelModel;
break;
case "customers":
default:
CurrentViewModel = custListViewModel;
break;
}
}
これらの異なるビューをナビゲートするには、MainWindow.xamlファイルに2つのボタンを追加する必要があります。以下は、完全なXAMLファイルの実装です。
<Window x:Class = "MVVMHierarchiesDemo.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:MVVMHierarchiesDemo"
xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views"
xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
<views:CustomerListView/>
</DataTemplate>
<DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
<views:OrderView/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "*" />
</Grid.RowDefinitions>
<Grid x:Name = "NavBar">
<Grid.ColumnDefinitions>
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
</Grid.ColumnDefinitions>
<Button Content = "Customers"
Command = "{Binding NavCommand}"
CommandParameter = "customers"
Grid.Column = "0" />
<Button Content = "Order"
Command = "{Binding NavCommand}"
CommandParameter = "orders"
Grid.Column = "2" />
</Grid>
<Grid x:Name = "MainContent" Grid.Row = "1">
<ContentControl Content = "{Binding CurrentViewModel}" />
</Grid>
</Grid>
</Window>
以下は、MainWindowViewModelの完全な実装です。
using MVVMHierarchiesDemo.ViewModel;
using MVVMHierarchiesDemo.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo {
class MainWindowViewModel : BindableBase {
public MainWindowViewModel() {
NavCommand = new MyICommand<string>(OnNav);
}
private CustomerListViewModel custListViewModel = new CustomerListViewModel();
private OrderViewModel orderViewModelModel = new OrderViewModel();
private BindableBase _CurrentViewModel;
public BindableBase CurrentViewModel {
get {return _CurrentViewModel;}
set {SetProperty(ref _CurrentViewModel, value);}
}
public MyICommand<string> NavCommand { get; private set; }
private void OnNav(string destination) {
switch (destination) {
case "orders":
CurrentViewModel = orderViewModelModel;
break;
case "customers":
default:
CurrentViewModel = custListViewModel;
break;
}
}
}
}
BindableBaseクラスからすべてのViewModelを取得します。上記のコードをコンパイルして実行すると、次の出力が表示されます。
ご覧のとおり、メインウィンドウに2つのボタンとCurrentViewModelのみを追加しました。いずれかのボタンをクリックすると、その特定のビューに移動します。[顧客]ボタンをクリックすると、CustomerListViewが表示されます。
理解を深めるために、上記の例を段階的に実行することをお勧めします。
この章では、検証について学習します。また、WPFバインディングがすでにサポートしているものを検証し、それをMVVMコンポーネントに結び付けるクリーンな方法についても見ていきます。
MVVMでの検証
アプリケーションがエンドユーザーからのデータ入力の受け入れを開始したら、その入力の検証を検討する必要があります。
全体的な要件に準拠していることを確認してください。
WPFには、入力を検証するためのバインディングシステムにいくつかの優れたビルドと機能があり、MVVMを実行するときにこれらすべての機能を引き続き活用できます。
検証をサポートし、ビュー自体ではなく、モデルまたはビューモデルの一部である必要があるプロパティに存在するルールを定義するロジックに注意してください。
-を含むWPFデータバインディングでサポートされている検証を表現するすべての方法を引き続き使用できます。
- プロパティに例外をスローするように設定されています。
- IDataErrorInfoインターフェイスの実装。
- INotifyDataErrorInfoを実装します。
- WPF検証ルールを使用します。
一般に、INotifyDataErrorInfoが推奨され、WPF .net 4.5に導入されました。これは、プロパティに関連するエラーのオブジェクトのクエリをサポートし、他のすべてのオプションのいくつかの欠陥も修正します。具体的には、非同期検証が可能です。これにより、プロパティに複数のエラーを関連付けることができます。
検証の追加
入力ビューに検証サポートを追加する例を見てみましょう。大規模なアプリケーションでは、おそらくこれがアプリケーションの多くの場所で必要になります。ビュー、ViewModel、およびこれらのヘルパーオブジェクトには、モデルオブジェクトのラッパーがあります。
さまざまなシナリオから継承できる共通の基本クラスに検証サポートを配置することをお勧めします。
基本クラスはINotifyDataErrorInfoをサポートするため、プロパティが変更されたときに検証がトリガーされます。
ValidatableBindableBaseという新しいクラスを作成して追加します。プロパティ変更処理の基本クラスがすでにあるので、そこから基本クラスを派生させ、INotifyDataErrorInfoインターフェイスも実装しましょう。
以下は、ValidatableBindableBaseクラスの実装です。
using System;
using System.Collections.Generic;
using System.ComponentModel;
//using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
namespace MVVMHierarchiesDemo {
public class ValidatableBindableBase : BindableBase, INotifyDataErrorInfo {
private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
public event EventHandler<DataErrorsChangedEventArgs>
ErrorsChanged = delegate { };
public System.Collections.IEnumerable GetErrors(string propertyName) {
if (_errors.ContainsKey(propertyName))
return _errors[propertyName];
else
return null;
}
public bool HasErrors {
get { return _errors.Count > 0; }
}
protected override void SetProperty<T>(ref T member, T val,
[CallerMemberName] string propertyName = null) {
base.SetProperty<T>(ref member, val, propertyName);
ValidateProperty(propertyName, val);
}
private void ValidateProperty<T>(string propertyName, T value) {
var results = new List<ValidationResult>();
//ValidationContext context = new ValidationContext(this);
//context.MemberName = propertyName;
//Validator.TryValidateProperty(value, context, results);
if (results.Any()) {
//_errors[propertyName] = results.Select(c => c.ErrorMessage).ToList();
} else {
_errors.Remove(propertyName);
}
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
}
}
次に、AddEditCustomerViewとAddEditCustomerViewModelをそれぞれのフォルダーに追加します。以下は、AddEditCustomerView.xamlのコードです。
<UserControl x:Class = "MVVMHierarchiesDemo.Views.AddEditCustomerView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
</Grid.RowDefinitions>
<Grid x:Name = "grid1"
HorizontalAlignment = "Left"
DataContext = "{Binding Customer}"
Margin = "10,10,0,0"
VerticalAlignment = "Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width = "Auto" />
<ColumnDefinition Width = "Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
</Grid.RowDefinitions>
<Label Content = "First Name:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "0"
VerticalAlignment = "Center" />
<TextBox x:Name = "firstNameTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "0"
Text = "{Binding FirstName, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
<Label Content = "Last Name:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "1"
VerticalAlignment = "Center" />
<TextBox x:Name = "lastNameTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "1"
Text = "{Binding LastName, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
<Label Content = "Email:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "2"
VerticalAlignment = "Center" />
<TextBox x:Name = "emailTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "2"
Text = "{Binding Email, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
<Label Content = "Phone:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "3"
VerticalAlignment = "Center" />
<TextBox x:Name = "phoneTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "3"
Text = "{Binding Phone, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
</Grid>
<Grid Grid.Row = "1">
<Button Content = "Save"
Command = "{Binding SaveCommand}"
HorizontalAlignment = "Left"
Margin = "25,5,0,0"
VerticalAlignment = "Top"
Width = "75" />
<Button Content = "Add"
Command = "{Binding SaveCommand}"
HorizontalAlignment = "Left"
Margin = "25,5,0,0"
VerticalAlignment = "Top"
Width = "75" />
<Button Content = "Cancel"
Command = "{Binding CancelCommand}"
HorizontalAlignment = "Left"
Margin = "150,5,0,0"
VerticalAlignment = "Top"
Width = "75" />
</Grid>
</Grid>
</UserControl>
以下は、AddEditCustomerViewModelの実装です。
using MVVMHierarchiesDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.ViewModel {
class AddEditCustomerViewModel : BindableBase {
public AddEditCustomerViewModel() {
CancelCommand = new MyIcommand(OnCancel);
SaveCommand = new MyIcommand(OnSave, CanSave);
}
private bool _EditMode;
public bool EditMode {
get { return _EditMode; }
set { SetProperty(ref _EditMode, value);}
}
private SimpleEditableCustomer _Customer;
public SimpleEditableCustomer Customer {
get { return _Customer; }
set { SetProperty(ref _Customer, value);}
}
private Customer _editingCustomer = null;
public void SetCustomer(Customer cust) {
_editingCustomer = cust;
if (Customer != null) Customer.ErrorsChanged -= RaiseCanExecuteChanged;
Customer = new SimpleEditableCustomer();
Customer.ErrorsChanged += RaiseCanExecuteChanged;
CopyCustomer(cust, Customer);
}
private void RaiseCanExecuteChanged(object sender, EventArgs e) {
SaveCommand.RaiseCanExecuteChanged();
}
public MyIcommand CancelCommand { get; private set; }
public MyIcommand SaveCommand { get; private set; }
public event Action Done = delegate { };
private void OnCancel() {
Done();
}
private async void OnSave() {
Done();
}
private bool CanSave() {
return !Customer.HasErrors;
}
}
}
以下は、SimpleEditableCustomerクラスの実装です。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.Model {
public class SimpleEditableCustomer : ValidatableBindableBase {
private Guid _id;
public Guid Id {
get { return _id; }
set { SetProperty(ref _id, value); }
}
private string _firstName;
[Required]
public string FirstName {
get { return _firstName; }
set { SetProperty(ref _firstName, value); }
}
private string _lastName;
[Required]
public string LastName {
get { return _lastName; }
set { SetProperty(ref _lastName, value); }
}
private string _email;
[EmailAddress]
public string Email {
get { return _email; }
set { SetProperty(ref _email, value); }
}
private string _phone;
[Phone]
public string Phone {
get { return _phone; }
set { SetProperty(ref _phone, value); }
}
}
}
上記のコードをコンパイルして実行すると、次のウィンドウが表示されます。
[顧客の追加]ボタンを押すと、次のビューが表示されます。ユーザーがフィールドを空のままにすると、フィールドが強調表示され、保存ボタンが無効になります。
この章では、依存性注入について簡単に説明します。ビューとViewModelを相互に分離し、通信のもう一方の端で何が起こっているかを明示的に知らなくても通信できるようにするデータバインディングについては、すでに説明しました。
ここで、ViewModelをクライアントサービスから切り離すのと同様のものが必要です。
オブジェクト指向プログラミングの初期の頃、開発者はアプリケーションでクラスのインスタンスを作成および取得するという問題に直面していました。この問題に対して様々な解決策が提案されてきた。
過去数年間、依存性注入と制御の反転(IoC)は開発者の間で人気を博し、シングルトンパターンなどのいくつかの古いソリューションよりも優先されてきました。
依存性注入/ IoCコンテナ
IoCと依存性注入は密接に関連する2つのデザインパターンであり、コンテナーは基本的に、これらのパターンの両方を実行するインフラストラクチャコードのチャンクです。
IoCパターンは、構築の責任を委任することであり、依存性注入パターンは、すでに構築されているオブジェクトに依存関係を提供することです。
これらは両方とも、構築への2段階のアプローチとして扱うことができます。コンテナを使用する場合、コンテナには次のようないくつかの責任があります。
- 要求されたときにオブジェクトを構築します。
- コンテナは、そのオブジェクトが何に依存するかを決定します。
- それらの依存関係を構築します。
- 構築中のオブジェクトにそれらを注入します。
- プロセスを再帰的に実行します。
依存性注入を使用して、ViewModelとクライアントサービス間の分離を解除する方法を見てみましょう。それに関連する依存性注入を使用して、保存処理のAddEditCustomerViewModelフォームを接続します。
まず、プロジェクトのServicesフォルダーに新しいインターフェイスを作成する必要があります。プロジェクトにサービスフォルダーがない場合は、最初にそれを作成し、サービスフォルダーに次のインターフェイスを追加します。
using MVVMHierarchiesDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.Services {
public interface ICustomersRepository {
Task<List<Customer>> GetCustomersAsync();
Task<Customer> GetCustomerAsync(Guid id);
Task<Customer> AddCustomerAsync(Customer customer);
Task<Customer> UpdateCustomerAsync(Customer customer);
Task DeleteCustomerAsync(Guid customerId);
}
}
以下は、ICustomersRepositoryの実装です。
using MVVMHierarchiesDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq; using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.Services {
public class CustomersRepository : ICustomersRepository {
ZzaDbContext _context = new ZzaDbContext();
public Task<List<Customer>> GetCustomersAsync() {
return _context.Customers.ToListAsync();
}
public Task<Customer> GetCustomerAsync(Guid id) {
return _context.Customers.FirstOrDefaultAsync(c => c.Id == id);
}
public async Task<Customer> AddCustomerAsync(Customer customer){
_context.Customers.Add(customer);
await _context.SaveChangesAsync();
return customer;
}
public async Task<Customer> UpdateCustomerAsync(Customer customer) {
if (!_context.Customers.Local.Any(c => c.Id == customer.Id)) {
_context.Customers.Attach(customer);
}
_context.Entry(customer).State = EntityState.Modified;
await _context.SaveChangesAsync();
return customer;
}
public async Task DeleteCustomerAsync(Guid customerId) {
var customer = _context.Customers.FirstOrDefault(c => c.Id == customerId);
if (customer != null) {
_context.Customers.Remove(customer);
}
await _context.SaveChangesAsync();
}
}
}
保存処理を行う簡単な方法は、AddEditCustomerViewModelにICustomersRepositoryの新しいインスタンスを追加し、AddEditCustomerViewModelおよびCustomerListViewModelコンストラクターをオーバーロードすることです。
private ICustomersRepository _repo;
public AddEditCustomerViewModel(ICustomersRepository repo) {
_repo = repo;
CancelCommand = new MyIcommand(OnCancel);
SaveCommand = new MyIcommand(OnSave, CanSave);
}
次のコードに示すように、OnSaveメソッドを更新します。
private async void OnSave() {
UpdateCustomer(Customer, _editingCustomer);
if (EditMode)
await _repo.UpdateCustomerAsync(_editingCustomer);
else
await _repo.AddCustomerAsync(_editingCustomer);
Done();
}
private void UpdateCustomer(SimpleEditableCustomer source, Customer target) {
target.FirstName = source.FirstName;
target.LastName = source.LastName;
target.Phone = source.Phone;
target.Email = source.Email;
}
以下は完全なAddEditCustomerViewModelです。
using MVVMHierarchiesDemo.Model;
using MVVMHierarchiesDemo.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.ViewModel {
class AddEditCustomerViewModel : BindableBase {
private ICustomersRepository _repo;
public AddEditCustomerViewModel(ICustomersRepository repo) {
_repo = repo;
CancelCommand = new MyIcommand(OnCancel);
SaveCommand = new MyIcommand(OnSave, CanSave);
}
private bool _EditMode;
public bool EditMode {
get { return _EditMode; }
set { SetProperty(ref _EditMode, value); }
}
private SimpleEditableCustomer _Customer;
public SimpleEditableCustomer Customer {
get { return _Customer; }
set { SetProperty(ref _Customer, value); }
}
private Customer _editingCustomer = null;
public void SetCustomer(Customer cust) {
_editingCustomer = cust;
if (Customer != null) Customer.ErrorsChanged -= RaiseCanExecuteChanged;
Customer = new SimpleEditableCustomer();
Customer.ErrorsChanged += RaiseCanExecuteChanged;
CopyCustomer(cust, Customer);
}
private void RaiseCanExecuteChanged(object sender, EventArgs e) {
SaveCommand.RaiseCanExecuteChanged();
}
public MyIcommand CancelCommand { get; private set; }
public MyIcommand SaveCommand { get; private set; }
public event Action Done = delegate { };
private void OnCancel() {
Done();
}
private async void OnSave() {
UpdateCustomer(Customer, _editingCustomer);
if (EditMode)
await _repo.UpdateCustomerAsync(_editingCustomer);
else
await _repo.AddCustomerAsync(_editingCustomer);
Done();
}
private void UpdateCustomer(SimpleEditableCustomer source, Customer target) {
target.FirstName = source.FirstName;
target.LastName = source.LastName;
target.Phone = source.Phone;
target.Email = source.Email;
}
private bool CanSave() {
return !Customer.HasErrors;
}
private void CopyCustomer(Customer source, SimpleEditableCustomer target) {
target.Id = source.Id;
if (EditMode) {
target.FirstName = source.FirstName;
target.LastName = source.LastName;
target.Phone = source.Phone;
target.Email = source.Email;
}
}
}
}
上記のコードをコンパイルして実行すると、同じ出力が表示されますが、ViewModelsはより緩く分離されています。
[顧客の追加]ボタンを押すと、次のビューが表示されます。ユーザーがフィールドを空のままにすると、フィールドが強調表示され、保存ボタンが無効になります。
イベントは、状態の変化に反応して、通知用に登録されているエンドポイントに通知するプログラミング構造です。主に、イベントはマウスとキーボードを介してユーザー入力を通知するために使用されますが、その有用性はそれに限定されません。状態の変化が検出されるたびに、おそらくオブジェクトがロードまたは初期化されたときに、イベントを発生させて、関心のあるサードパーティに警告することができます。
MVVM(Model-View-ViewModel)デザインパターンを使用するWPFアプリケーションでは、ビューモデルは、アプリケーションのプレゼンテーションロジックと状態の処理を担当するコンポーネントです。
ビューの分離コードファイルには、ButtonやComboBoxなどのユーザーインターフェイス(UI)要素から発生するイベントを処理するコードを含めたり、ドメイン固有のロジックを含めたりしないでください。
理想的には、ビューのコードビハインドには、InitializeComponentメソッドを呼び出すコンストラクターと、複雑なアニメーションなど、XAMLで表現するのが困難または非効率的なビューレイヤーを制御または操作するための追加コードのみが含まれます。
アプリケーションのボタンクリックイベントの簡単な例を見てみましょう。以下は、2つのボタンが表示されるMainWindow.xamlファイルのXAMLコードです。
<Window x:Class = "MVVMHierarchiesDemo.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:MVVMHierarchiesDemo"
xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views"
xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
<views:CustomerListView/>
</DataTemplate>
<DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
<views:OrderView/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "*" />
</Grid.RowDefinitions>
<Grid x:Name = "NavBar">
<Grid.ColumnDefinitions>
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
</Grid.ColumnDefinitions>
<Button Content = "Customers"
Command = "{Binding NavCommand}"
CommandParameter = "customers"
Grid.Column = "0" />
<Button Content = "Order"
Command = "{Binding NavCommand}"
CommandParameter = "orders"
Grid.Column = "2" />
</Grid>
<Grid x:Name = "MainContent" Grid.Row = "1">
<ContentControl Content = "{Binding CurrentViewModel}" />
</Grid>
</Grid>
</Window>
上記のXAMLファイルではボタンのClickプロパティは使用されていませんが、ボタンが押されたときにCommandプロパティとCommandParameterプロパティを使用して異なるビューをロードしていることがわかります。次に、MainWindowViewModel.csファイルでコマンドの実装を定義する必要がありますが、Viewファイルでは定義する必要はありません。以下は、MainWindowViewModelの完全な実装です。
using MVVMHierarchiesDemo.ViewModel;
using MVVMHierarchiesDemo.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo {
class MainWindowViewModel : BindableBase {
public MainWindowViewModel() {
NavCommand = new MyICommand<string>(OnNav);
}
private CustomerListViewModel custListViewModel = new CustomerListViewModel();
private OrderViewModel orderViewModelModel = new OrderViewModel();
private BindableBase _CurrentViewModel;
public BindableBase CurrentViewModel {
get { return _CurrentViewModel; }
set { SetProperty(ref _CurrentViewModel, value); }
}
public MyICommand<string> NavCommand { get; private set; }
private void OnNav(string destination) {
switch (destination) {
case "orders":
CurrentViewModel = orderViewModelModel;
break;
case "customers":
default:
CurrentViewModel = custListViewModel;
break;
}
}
}
}
BindableBaseクラスからすべてのViewModelを取得します。上記のコードをコンパイルして実行すると、次の出力が表示されます。
ご覧のとおり、メインウィンドウに2つのボタンとCurrentViewModelのみを追加しました。ここで、任意のボタンをクリックすると、その特定のビューに移動します。[顧客]ボタンをクリックすると、CustomerListViewが表示されます。
理解を深めるために、上記の例を段階的に実行することをお勧めします。
単体テストの背後にある考え方は、コードの個別のチャンク(ユニット)を取得し、期待される方法でコードを使用するテストメソッドを記述してから、期待される結果が得られるかどうかをテストすることです。
コード自体であるため、単体テストはプロジェクトの他の部分と同じようにコンパイルされます。
また、テスト実行ソフトウェアによって実行されます。このソフトウェアは、各テストを高速化して、テストが成功したか失敗したかを示すために、親指を上または下に効果的に上げます。
以前に作成した例を見てみましょう。以下は、StudentModelの実装です。
using System.ComponentModel;
namespace MVVMDemo.Model {
public class StudentModel {}
public class Student : INotifyPropertyChanged {
private string firstName;
private string lastName;
public string FirstName {
get { return firstName; }
set {
if (firstName != value) {
firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
}
public string LastName {
get { return lastName; }
set {
if (lastName != value) {
lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
}
public string FullName {
get {
return firstName + " " + lastName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
以下はStudentViewの実装です。
<UserControl x:Class="MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:data = "clr-namespace:MVVMDemo.Model"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<StackPanel Orientation = "Horizontal">
<ListBox ItemsSource = "{Binding Students}"
SelectedItem = "{Binding SelectedStudent}"/>
<Button Content = "Delete"
Command = "{Binding DeleteCommand}"
HorizontalAlignment = "Left"
VerticalAlignment = "Top"
Width = "75" />
</StackPanel>
</Grid>
</UserControl>
以下はStudentViewModelの実装です。
using MVVMDemo.Model;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public MyICommand DeleteCommand { get; set;}
public StudentViewModel() {
LoadStudents();
DeleteCommand = new MyICommand(OnDelete, CanDelete);
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
private Student _selectedStudent;
public Student SelectedStudent {
get {
return _selectedStudent;
}
set {
_selectedStudent = value;
DeleteCommand.RaiseCanExecuteChanged();
}
}
private void OnDelete() {
Students.Remove(SelectedStudent);
}
private bool CanDelete() {
return SelectedStudent != null;
}
public int GetStudentCount() {
return Students.Count;
}
}
}
以下はMainWindow.xamlファイルです。
<Window x:Class = "MVVMDemo.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:MVVMDemo"
xmlns:views = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Grid>
<views:StudentView x:Name = "StudentViewControl"/>
</Grid>
</Window>
以下は、ICommandインターフェイスを実装するMyICommand実装です。
using System;
using System.Windows.Input;
namespace MVVMDemo {
public class MyICommand : ICommand {
Action _TargetExecuteMethod;
Func<bool> _TargetCanExecuteMethod;
public MyICommand(Action executeMethod) {
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action executeMethod, Func<bool> canExecuteMethod) {
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged() {
CanExecuteChanged(this, EventArgs.Empty);
}
bool ICommand.CanExecute(object parameter) {
if (_TargetCanExecuteMethod != null) {
return _TargetCanExecuteMethod();
}
if (_TargetExecuteMethod != null) {
return true;
}
return false;
}
// Beware - should use weak references if command instance lifetime
is longer than lifetime of UI objects that get hooked up to command
// Prism commands solve this in their implementation
public event EventHandler CanExecuteChanged = delegate { };
void ICommand.Execute(object parameter) {
if (_TargetExecuteMethod != null) {
_TargetExecuteMethod();
}
}
}
}
上記のコードをコンパイルして実行すると、メインウィンドウに次の出力が表示されます。
上記の例の単体テストを作成するために、新しいテストプロジェクトをソリューションに追加しましょう。
[参照]を右クリックして、プロジェクトへの参照を追加します。
既存のプロジェクトを選択し、[OK]をクリックします。
次のコードに示すように、学生数をチェックする簡単なテストを追加しましょう。
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MVVMDemo.ViewModel;
namespace MVVMTest {
[TestClass]
public class UnitTest1 {
[TestMethod]
public void TestMethod1() {
StudentViewModel sViewModel = new StudentViewModel();
int count = sViewModel.GetStudentCount();
Assert.IsTrue(count == 3);
}
}
}
このテストを実行するには、[テスト]→[実行]→[すべてのテスト]メニューオプションを選択します。
StudentViewModelには3人の学生が追加されているため、テストエクスプローラーでテストに合格したことがわかります。次のコードに示すように、カウント条件を3から4に変更します。
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MVVMDemo.ViewModel;
namespace MVVMTest {
[TestClass]
public class UnitTest1 {
[TestMethod] public void TestMethod1() {
StudentViewModel sViewModel = new StudentViewModel();
int count = sViewModel.GetStudentCount();
Assert.IsTrue(count == 4);
}
}
}
テスト計画を再度実行すると、学生数が4に等しくないため、テストが失敗したことがわかります。
理解を深めるために、上記の例を段階的に実行することをお勧めします。
この章では、利用可能なMVVMツールキットまたはフレームワークについて説明します。これらのフレームワークを使用して、MVVMパターンを自分で実装するために多数の反復コードを作成する必要がないようにすることもできます。最も人気のあるフレームワークのいくつかを次に示します-
- Prism
- MVVMライト
- カリバーンマイクロ
プリズム
Prismは、サンプルとドキュメントの形式でガイダンスを提供し、リッチで柔軟性があり、保守が容易なWindows Presentation Foundation(WPF)デスクトップアプリケーションを簡単に設計および構築するのに役立ちます。Microsoft SilverlightブラウザプラグインとWindowsアプリケーションで構築されたリッチインターネットアプリケーション(RIA)。
Prismは、関心の分離や疎結合などの重要なアーキテクチャ設計原則を具体化する設計パターンを使用します。
Prismは、独立して進化できるが、アプリケーション全体に簡単かつシームレスに統合できる疎結合コンポーネントを使用して、アプリケーションを設計および構築するのに役立ちます。
これらのタイプのアプリケーションは、複合アプリケーションとして知られています。
Prismには、すぐに使用できる多くの機能があります。以下は、Prismの重要な機能の一部です。
MVVMパターン
PrismはMVVMパターンをサポートしています。これには、前の章で実装されたものと同様のBindablebaseクラスがあります。
規則がある柔軟なViewModelLocatorがありますが、これらの規則をオーバーライドして、疎結合の方法でビューとViewModelを宣言的に接続できます。
モジュール性
これは、コードを完全に疎結合されたクラスライブラリに分割し、実行時にそれらをまとめてエンドユーザー向けのまとまりのある全体にする機能です。コードは完全に分離されたままです。
UI構成/地域
これは、プラグインを実行するビューなしでビューをコンテナーにプラグインする機能であり、UIコンテナー自体への明示的な参照が必要です。
ナビゲーション
Prismには、前方ナビゲーションと後方ナビゲーション、ビューモデルがナビゲーションプロセスに直接参加できるナビゲーションスタックなど、リージョンの上にレイヤー化されるナビゲーション機能があります。
コマンド
Prismにはコマンドがあるため、前の章で使用したMyICommandと非常によく似たデリゲートコマンドがありますが、メモリリークから保護するための堅牢性が追加されています。
Pub / Subイベント
Prismは、Pub / Subイベントもサポートしています。これらは疎結合のイベントであり、パブリッシャーとサブスクライバーは異なる存続期間を持つことができ、イベントを介して通信するために相互に明示的に参照する必要はありません。
MVVMライト
MVVMLightはLaurentBugnionによって作成されており、ビューをモデルから分離するのに役立ちます。これにより、よりクリーンで、保守と拡張が容易なアプリケーションが作成されます。
また、テスト可能なアプリケーションを作成し、はるかに薄いユーザーインターフェイスレイヤーを使用できるようにします(自動的にテストするのはより困難です)。
このツールキットは、Blendユーザーがデータコントロールを操作するときに「何かを見る」ことができるようにする設計時データの作成など、Blendへのユーザーインターフェイスを開いて編集することに特に重点を置いています。
カリバーンマイクロ
これは、MVVMパターンの実装を支援し、すぐに使用できる多くの機能をサポートする、もう1つの小さなオープンソースフレームワークです。
Caliburn Microは、小さいながらも強力なフレームワークであり、すべてのXAMLプラットフォームでアプリケーションを構築するために設計されています。
Caliburn Microは、MVVMおよびその他の実績のあるUIパターンを強力にサポートしているため、コードの品質やテスト容易性を犠牲にすることなく、ソリューションを迅速に構築できます。