WPFを学ぶのにMVVMを学ばなければ、魂が欠けているようです。では、MVVMとは何でしょうか?なぜMVVMを学ぶべきなのでしょうか?ここでは、シンプルなCRUD(作成、読み取り、更新、削除)の小さな例を通して、MVVMの基本知識とMVVMアーキテクチャを使用したプログラム開発の方法を簡単に説明します。学習と共有のためにのみ使用します。不十分な点があれば、ご指摘ください。
MVVMとは?
MVVMはModel-View-ViewModelの略です。本質的にはMVC(Model-View-Controller)の改良版です。すなわち、モデル-ビュー-ビューモデルです。それぞれの定義は以下の通りです:
- 【モデル】はバックエンドから渡されるデータを指します。
- 【ビュー】は表示されるページを指します。
- 【ビューモデル】はMVVMパターンの核心であり、ビューとモデルを結びつける橋渡し役です。2つの方向性があります:
- 1つは【モデル】を【ビュー】に変換すること、つまりバックエンドから渡されるデータを表示されるページに変換することです。実現方法はデータバインディングです。
- もう1つは【ビュー】を【モデル】に変換すること、つまり表示されるページをバックエンドのデータに変換することです。実現方法はDOMイベントのリッスンです。これらの両方向が実現されているものを、データの双方向バインディングと呼びます。
MVVMの模式図は以下の通りです:

MvvmLightプラグインのインストール
プロジェクト名を右クリック → NuGetパッケージの管理 → MvvmLightを検索 → インストール。以下の通りです:

ライセンス契約の受け入れウィンドウが表示され、【同意する】をクリックします。以下の通りです:

MvvmLightのインストールが成功すると、必要なサードパーティライブラリが自動的に参照され、デフォルトでサンプルコンテンツが生成されます。不要なものは削除する必要があります。以下の通りです:

MVVMサンプルスクリーンショット
主にMVVMを使用してデータのCRUD【追加・削除・変更・検索】基本操作を実装しています。以下の通りです:

MVVM開発手順
- Model層の作成
この例では主に学生情報の追加・削除・変更・検索を行うため、Studentモデルクラスを作成します。以下の通りです:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WpfApp3.Models
{
/// <summary>
/// 学生类
/// </summary>
public class Student
{
/// <summary>
/// 唯一标识
/// </summary>
public int Id { get; set; }
/// <summary>
/// 学生姓名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 年龄
/// </summary>
public int Age { get; set; }
/// <summary>
/// 班级
/// </summary>
public string Classes { get; set; }
}
}
- DAL層の作成
サンプルを簡略化するために、データベース操作をシミュレートし、基本データを構築します。以下の通りです:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WpfApp3.Models;
namespace WpfApp3.DAL
{
public class LocalDb
{
private List<Student> students;
public LocalDb() {
init();
}
/// <summary>
/// 初始化数据
/// </summary>
private void init() {
students = new List<Student>();
for (int i = 0; i < 30; i++)
{
students.Add(new Student()
{
Id=i,
Name=string.Format("学生{0}",i),
Age=new Random(i).Next(0,100),
Classes=i%2==0?"一班":"二班"
});
}
}
/// <summary>
/// 查询数据
/// </summary>
/// <returns></returns>
public List<Student> Query()
{
return students;
}
/// <summary>
/// 按名字查询
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public List<Student> QueryByName(string name)
{
return students.Where((t) => t.Name.Contains(name)).ToList();//FindAll((t) => t.Name.Contains(name));
}
public Student QueryById(int Id)
{
var student = students.FirstOrDefault((t) => t.Id == Id);
if (student != null)
{
return new Student() {
Id=student.Id,
Name=student.Name,
Age=student.Age,
Classes=student.Classes
};
}
return null;
}
/// <summary>
/// 新增学生
/// </summary>
/// <param name="student"></param>
public void AddStudent(Student student)
{
if (student != null)
{
students.Add(student);
}
}
/// <summary>
/// 删除学生
/// </summary>
/// <param name="Id"></param>
public void DelStudent(int Id)
{
var student = students.FirstOrDefault((t) => t.Id == Id); //students.Find((t) => t.Id == Id);
if (student != null)
{
students.Remove(student);
}
}
}
}
- View層の作成
View層はユーザーとの対話、ユーザーデータの表示、イベントの応答を行います。この例では、View層は主にデータのクエリ表示、新規作成および編集ページで構成されています。
View層では、主にコマンドのバインドとデータのバインドを行います。
- DataGridTextColumnでは、
Binding="{Binding Id}"の形式で表示する列のプロパティ名をバインドします。 - Buttonでは、
Command="{Binding AddCommand}"の形式で応答するコマンドをバインドします。 - TextBoxでは、
Text="{Binding Search}"の形式で検索条件のプロパティをバインドします。
データ表示ウィンドウは以下の通りです:
<Window
x:Class="WpfApp3.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:WpfApp3"
mc:Ignorable="d"
Title="MainWindow"
Height="450"
Width="800"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="80"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<StackPanel
Orientation="Horizontal"
Grid.Row="0"
Margin="5"
VerticalAlignment="Center"
>
<TextBlock Text="姓名:" Margin="10" Padding="5"></TextBlock>
<TextBox
x:Name="sname"
Text="{Binding Search}"
Width="120"
Margin="10"
Padding="5"
></TextBox>
<button
x:Name="btnQuery"
Content="查询"
Margin="10"
Padding="5"
Width="80"
Command="{Binding QueryCommand}"
></button>
<button
x:Name="btnReset"
Content="重置"
Margin="10"
Padding="5"
Width="80"
Command="{Binding ResetCommand}"
></button>
<button
x:Name="btnAdd"
Content="创建"
Margin="10"
Padding="5"
Width="80"
Command="{Binding AddCommand}"
></button>
</StackPanel>
<DataGrid
x:Name="dgInfo"
Grid.Row="1"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserSortColumns="False"
Margin="10"
ItemsSource="{Binding GridModelList}"
>
<DataGrid.Columns>
<DataGridTextColumn
Header="Id"
Width="100"
Binding="{Binding Id}"
></DataGridTextColumn>
<DataGridTextColumn
Header="姓名"
Width="100"
Binding="{Binding Name}"
></DataGridTextColumn>
<DataGridTextColumn
Header="年龄"
Width="100"
Binding="{Binding Age}"
></DataGridTextColumn>
<DataGridTextColumn
Header="班级"
Width="100"
Binding="{Binding Classes}"
></DataGridTextColumn>
<DataGridTemplateColumn Header="操作" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel
Orientation="Horizontal"
VerticalAlignment="Center"
HorizontalAlignment="Center"
>
<button
x:Name="edit"
Content="编辑"
Width="60"
Margin="3"
Height="25"
CommandParameter="{Binding Id}"
Command="{Binding DataContext.EditCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}"
></button>
<button
x:Name="delete"
Content="删除"
Width="60"
Margin="3"
Height="25"
CommandParameter="{Binding Id}"
Command="{Binding DataContext.DeleteCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=DataGrid}}"
></button>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
新規作成および編集ページは以下の通りです:
<Window
x:Class="WpfApp3.Views.StudentWindow"
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:WpfApp3.Views"
mc:Ignorable="d"
Title="StudentWindow"
Height="440"
Width="500"
AllowsTransparency="False"
WindowStartupLocation="CenterScreen"
WindowStyle="None"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="60"></RowDefinition>
<RowDefinition></RowDefinition>
<RowDefinition Height="60"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock FontSize="30" Margin="10">修改学生信息</TextBlock>
<StackPanel Grid.Row="1" Orientation="Vertical">
<TextBlock FontSize="20" Margin="10" Padding="5">姓名</TextBlock>
<TextBox
x:Name="txtName"
FontSize="20"
Padding="5"
Text="{Binding Model.Name}"
></TextBox>
<TextBlock FontSize="20" Margin="10" Padding="5">年龄</TextBlock>
<TextBox
x:Name="txtAge"
FontSize="20"
Padding="5"
Text="{Binding Model.Age}"
></TextBox>
<TextBlock FontSize="20" Margin="10" Padding="5">班级</TextBlock>
<TextBox
x:Name="txtClasses"
FontSize="20"
Padding="5"
Text="{Binding Model.Classes}"
></TextBox>
</StackPanel>
<StackPanel
Grid.Row="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
>
<button
x:Name="btnSave"
Content="保存"
Margin="10"
FontSize="20"
Width="100"
Click="btnSave_Click"
></button>
<button
x:Name="btnCancel"
Content="取消"
Margin="10"
FontSize="20"
Width="100"
Click="btnCancel_Click"
></button>
</StackPanel>
</Grid>
</Window>
- ViewModel層の作成
ViewModel層はMVVMの核心であり、橋渡しの役割を果たします。ViewModelはGalaSoft.MvvmLight.ViewModelBase基底クラスを継承する必要があります。
ViewModelのプロパティはデータバインディングを実現し、コマンドはユーザーインタラクションへの応答を実現します。以下の通りです:
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using WpfApp3.DAL;
using WpfApp3.Models;
using WpfApp3.Views;
namespace WpfApp3.ViewModel
{
/// <summary>
///
/// </summary>
public class MainViewModel : ViewModelBase
{
#region 属性及构造函数
private LocalDb localDb;
private ObservableCollection<Student> gridModelList;
public ObservableCollection<Student> GridModelList
{
get { return gridModelList; }
set
{
gridModelList = value;
RaisePropertyChanged();
}
}
/// <summary>
/// 查询条件
/// </summary>
private string search;
public string Search
{
get { return search; }
set
{
search = value;
RaisePropertyChanged();
}
}
/// <summary>
///
/// </summary>
public MainViewModel()
{
localDb = new LocalDb();
QueryCommand = new RelayCommand(this.Query);
ResetCommand = new RelayCommand(this.Reset);
EditCommand = new RelayCommand<int>(this.Edit);
DeleteCommand = new RelayCommand<int>(this.Delete);
AddCommand = new RelayCommand(this.Add);
}
#endregion
#region command
/// <summary>
/// 查询命令
/// </summary>
public RelayCommand QueryCommand { get; set; }
/// <summary>
/// 重置命令
/// </summary>
public RelayCommand ResetCommand { get; set; }
/// <summary>
/// 编辑
/// </summary>
public RelayCommand<int> EditCommand { get; set; }
/// <summary>
/// 删除
/// </summary>
public RelayCommand<int> DeleteCommand { get; set; }
/// <summary>
/// 新增
/// </summary>
public RelayCommand AddCommand { get; set; }
#endregion
public void Query()
{
List<Student> students;
if (string.IsNullOrEmpty(search))
{
students = localDb.Query();
}
else
{
students = localDb.QueryByName(search);
}
GridModelList = new ObservableCollection<Student>();
if (students != null)
{
students.ForEach((t) =>
{
GridModelList.Add(t);
});
}
}
/// <summary>
/// 重置
/// </summary>
public void Reset()
{
this.Search = string.Empty;
this.Query();
}
/// <summary>
/// 编辑
/// </summary>
/// <param name="Id"></param>
public void Edit(int Id)
{
var model = localDb.QueryById(Id);
if (model != null)
{
StudentWindow view = new StudentWindow(model);
var r = view.ShowDialog();
if (r.Value)
{
var newModel = GridModelList.FirstOrDefault(t => t.Id == model.Id);
if (newModel != null)
{
newModel.Name = model.Name;
newModel.Age = model.Age;
newModel.Classes = model.Classes;
}
this.Query();
}
}
}
/// <summary>
/// 删除
/// </summary>
/// <param name="Id"></param>
public void Delete(int Id)
{
var model = localDb.QueryById(Id);
if (model != null)
{
var r = MessageBox.Show($"确定要删除吗【{model.Name}】?","提示",MessageBoxButton.YesNo);
if (r == MessageBoxResult.Yes)
{
localDb.DelStudent(Id);
this.Query();
}
}
}
/// <summary>
/// 新增
/// </summary>
public void Add()
{
Student model = new Student();
StudentWindow view = new StudentWindow(model);
var r = view.ShowDialog();
if (r.Value)
{
model.Id = GridModelList.Max(t => t.Id) + 1;
localDb.AddStudent(model);
this.Query();
}
}
}
}
- データコンテキスト
各層がそれぞれ作成されたら、どのように関連付けるのでしょうか?答えはDataContext【データコンテキスト】です。
クエリページのコンテキストは以下の通りです:
namespace WpfApp3
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MainViewModel viewModel = new MainViewModel();
viewModel.Query();
this.DataContext = viewModel;
}
}
}
新規作成ページのコンテキストは以下の通りです:
namespace WpfApp3.Views
{
/// <summary>
/// StudentWindow.xaml 的交互逻辑
/// </summary>
public partial class StudentWindow : Window
{
public StudentWindow(Student student)
{
InitializeComponent();
this.DataContext = new
{
Model = student
};
}
private void btnSave_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = true;
}
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
}
}
}
まとめ
MVVMには、低結合、再利用性、テスト容易性、独立した開発といった利点があります。核心となる要素は次の2つです:
- プロパティが変更されたときの通知により、データのリアルタイム更新が可能になります。
- コマンドは、ユーザーとプログラムの間でデータとアルゴリズムを橋渡しするものです。
備考
この記事はMVVMの簡単な入門サンプルとして、皆さんと一緒に学び、共に成長することを目的としています。WPFの他の入門知識について詳しくない場合は、他のブログ記事を参照してください。
玉楼春·别后不知君远近
欧阳修 〔宋代〕
别后不知君远近,触目凄凉多少闷。渐行渐远渐无书,水阔鱼沉何处问。
夜深风竹敲秋韵,万叶千声皆是恨。故攲单枕梦中寻,梦又不成灯又烬。注:攲(yǐ)
